class BaseProfile(AuthDocument): manager = ProfileManager allow_inheritance = True collection = "BaseProfiles" account_id = fields.ObjectIdField() first_name = fields.StringField() last_name = fields.StringField() age = fields.NumField() sex = fields.StringField() location = fields.StringField() seniority = fields.StringField() assigned_labels = fields.ListField(fields.ObjectIdField()) date_of_birth = fields.StringField() attached_data = fields.DictField() products = fields.ListField(fields.StringField()) actor_num = AutoIncrementField(counter_name='ActorCounter', db_field='ar') created_at = fields.DateTimeField(default=now) linked_profile_ids = fields.ListField(fields.StringField()) indexes = ['actor_num', 'linked_profile_ids'] @property def linked_profiles(self): from solariat_bottle.db.user_profiles.user_profile import UserProfile return UserProfile.objects(id__in=self.linked_profile_ids)[:] def get_profile_of_type(self, typename): if not isinstance(typename, basestring): typename = typename.__name__ for profile in self.linked_profiles: if profile.__class__.__name__ == typename: return profile def add_profile(self, profile): new_id = str(profile.id) if new_id not in self.linked_profile_ids: self.linked_profile_ids.append(new_id) self.update(addToSet__linked_profile_ids=new_id) def get_age(self): # Best guess we can make is by date of birth if present and properly formatted if self.date_of_birth: try: dob = datetime.strptime(self.date_of_birth, AGE_FORMAT) return relativedelta(datetime.now(), dob).years except Exception, ex: LOGGER.error(ex) # Next, if actual age is present, use that but also store updated dob if self.age: dob = datetime.now() - relativedelta(years=self.age) self.date_of_birth = dob.strftime(AGE_FORMAT) self.save() return self.age return None
class EventTag(ABCPredictor): indexes = [('account_id', 'is_multi', ), ] display_name = fields.StringField() account_id = fields.ObjectIdField() status = fields.StringField(default="Active") description = fields.StringField() created = fields.DateTimeField() channels = fields.ListField(fields.ObjectIdField()) manager = EventTagManager default_threshold = 0.49 @property def inclusion_threshold(self): return self.default_threshold def save(self): self.packed_clf = self.clf.packed_model super(EventTag, self).save() def match(self, event): assert isinstance(event, Event), "EventTag expects Event objects" if self.score(event) > self.inclusion_threshold: return True return False def score(self, event): assert isinstance(event, Event), "EventTag expects Event objects" return super(EventTag, self).score(event) def accept(self, event): assert isinstance(event, Event), "EventTag expects Event objects" return super(EventTag, self).accept(event) def reject(self, event): assert isinstance(event, Event), "EventTag expects Event objects" return super(EventTag, self).reject(event) def check_preconditions(self, event): if self.precondition: return eval(self.precondition) return self.feature_extractor.check_preconditions(event, self.features_metadata) def rule_based_match(self, event): if self.acceptance_rule: return eval(self.acceptance_rule) return False def to_dict(self, fields_to_show=None): result_dict = super(EventTag, self).to_dict() result_dict.pop('counter') result_dict.pop('packed_clf') result_dict['channels'] = [str(c) for c in result_dict['channels']] return result_dict
class EventSequenceStatsMixin(object): account_id = fields.ObjectIdField(db_field='aid') channels = fields.ListField(fields.ObjectIdField(), db_field='chs') stage_sequence_names = fields.ListField(fields.StringField(), db_field='sseqnm') status = fields.NumField(db_field='ss', choices=JourneyStageType.STATUSES, default=JourneyStageType.IN_PROGRESS) smart_tags = fields.ListField(fields.ObjectIdField(), db_field='sts') journey_tags = fields.ListField(fields.ObjectIdField(), db_field='jts') journey_type_id = fields.ObjectIdField(db_field='jt') journey_attributes = fields.DictField(db_field='jyas') def __get_journey_type(self): if hasattr(self, '_f_journey_type'): return self._f_journey_type else: self._f_journey_type = JourneyType.objects.get( self.journey_type_id) return self._f_journey_type def __set_journey_type(self, journey_type): self._f_journey_type = journey_type journey_type = property(__get_journey_type, __set_journey_type) @classmethod def translate_static_key_name(cls, key_name): # translate any static key, leave anything else the same if key_name == cls.status.db_field: return 'status' return key_name @classmethod def translate_static_key_value(cls, key_name, key_value): # translate any static key, leave anything else the same if key_name == cls.status.db_field: return JourneyStageType.STATUS_TEXT_MAP[key_value] return key_value @property def full_journey_attributes(self): # Dynamic defined plus any static defined attributes worth considering in facets or analysis from copy import deepcopy base_attributes = deepcopy(self.journey_attributes) base_attributes['status'] = self.status return base_attributes @property def account(self): # TODO Check this for performance. Should cache. return Account.objects.get(self.account_id) event_id = EventIdField().to_mongo(event_id) event_id = EventIdField().to_mongo(event_id)
class StreamLog(Document): """Created on streamref creation, updated on stream stops""" accounts = fields.ListField(fields.ObjectIdField()) channels = fields.ListField(fields.ObjectIdField()) stream_ref_id = fields.BytesField() started_at = fields.DateTimeField(null=True) stopped_at = fields.DateTimeField(null=True) indexes = [('accounts', ), ('channels', ), ('stream_ref_id', )]
class Action(AuthDocument): name = fields.StringField() tags = fields.ListField(fields.ObjectIdField()) channels = fields.ListField(fields.ObjectIdField()) account_id = fields.ObjectIdField() type = fields.StringField() def to_dict(self, fields_to_show=None): return dict(id=str(self.id), account_id=str(self.account_id), name=str(self.name))
class Funnel(AuthDocument): """ """ name = fields.StringField(required=True, unique=True) description = fields.StringField() journey_type = fields.ObjectIdField() steps = fields.ListField(fields.ObjectIdField(), required=True) owner = fields.ReferenceField(User) created = fields.DateTimeField(default=datetime.now) def to_dict(self, fields_to_show=None): rv = super(Funnel, self).to_dict() rv['steps'] = map(str, self.steps) return rv
class JourneyStage(ABCPredictor): collection = 'JourneyStage' manager = JourneyStageManager journey_id = fields.ObjectIdField(db_field='jo') stage_type_id = fields.ObjectIdField( db_field='st') # Will be reference to a JourneyStageType stage_name = fields.StringField(db_field='sn') effort_info = fields.DictField( db_field='ef' ) # Embedded doc with any effort info we're going to track reward_info = fields.DictField( db_field='ri' ) # Embedded doc with any reward info we're going to track start_date = fields.DateTimeField(db_field='sd') end_date = fields.DateTimeField(db_field='ed') last_updated = fields.DateTimeField( db_field='lu' ) # We're probably going to want to know when was the last even from this stage directly last_event = fields.EventIdField( db_field='le') # Keep track of the event itself def check_preconditions(self, event): if hasattr(event, 'stage_id') and event.stage_id: # If 'stage_id' exists on event, and it's not None, that will be a precondition and acceptance rule return str(self.id) == event.stage_id if hasattr(event, 'journeys') and event.journeys: # If a specific set of journeys was passed with the event, the journey for this stage return self.journey_id in event.journeys return True def rule_based_match(self, object): if hasattr(object, 'stage_id') and object.stage_id: # If 'stage_id' exists on event, and it's not None, that will be a precondition and acceptance rule return str(self.id) == object.stage_id return False def process_event(self, event): update_dict = dict(set__last_event=event.data['_id'], set__last_updated=event.datetime_from_id) self.update(**update_dict) def get_journey_stage_type(self): from solariat_bottle.db.journeys.journey_type import JourneyStageType return JourneyStageType.objects.get(self.stage_type_id)
class BaseScore(AuthDocument): created = fields.DateTimeField(default=now) matching_engine = fields.ObjectIdField() model_id = fields.ObjectIdField(null=True) counter = fields.NumField(default=1) cumulative_latency = fields.NumField(required=True) indexes = [ ('matching_engine', 'created'), ] @property def latency(self): return 1.0 * self.cumulative_latency / (self.counter or 1)
class FacetCache(AuthDocument): collection = "FacetCache" hashcode = fields.StringField(db_field='hc', required=True) page_type = fields.StringField(db_field='pe', required=True) account_id = fields.ObjectIdField(db_field='aid', required=True) value = fields.StringField(db_field='ve', required=True) created_at = fields.DateTimeField(db_field='ct', required=True) def is_up_to_date(self): delta = datetime.now() - self.created_at return delta.total_seconds() < get_var( 'MONGO_CACHE_EXPIRATION') # 30 mins @classmethod def upsert_cache_record(cls, hashcode, data, page_type, account_id): now = datetime.now() if 'time_stats' in data: del data['time_stats'] if 'pipelines' in data: del data['pipelines'] cache_records = FacetCache.objects(hashcode=hashcode, account_id=account_id, page_type=page_type) cache_records_num = cache_records.count() if cache_records_num >= 2: raise Exception('Too many cache records') elif cache_records_num == 1: cache = cache_records[0] cache.value = json.dumps(data) cache.created_at = datetime.now() cache.save() else: cache = FacetCache(hashcode=hashcode, value=json.dumps(data), account_id=account_id, page_type=page_type, created_at=now) cache.save() @classmethod def get_cache(cls, params, account_id, page_type): hash_arg = params.copy() # import ipdb; ipdb.set_trace() for field in ['force_recompute', 'range_alias']: if field in hash_arg: del hash_arg[field] hashcode = md5(str(hash_arg)).hexdigest() cache_candidates = FacetCache.objects(hashcode=hashcode, account_id=account_id, page_type=page_type) if not cache_candidates: return hashcode, None elif 1 == len(cache_candidates): return hashcode, cache_candidates[0] else: for cache in cache_candidates: cache.remove() return hashcode, None
class DynamicImportedProfile(AuthDocument): id = fields.CustomIdField() actor_num = AutoIncrementField(counter_name='ActorCounter', db_field='ar') linked_profile_ids = fields.ListField(fields.StringField()) account_id = fields.ObjectIdField() @property def linked_profiles(self): from solariat_bottle.db.user_profiles.user_profile import UserProfile return UserProfile.objects(id__in=self.linked_profile_ids)[:] def get_profile_of_type(self, typename): if not isinstance(typename, basestring): typename = typename.__name__ for profile in self.linked_profiles: if profile.__class__.__name__ == typename: return profile def add_profile(self, platform_profile): self.linked_profile_ids.append(str(platform_profile.id)) self.save() def has_linked_profile(self, platform_profile): return str(platform_profile.id) in self.linked_profile_ids def to_dict(self, **kw): base_dict = super(DynamicImportedProfile, self).to_dict(**kw) for key, val in base_dict.iteritems(): if len(str(val)) > 100: base_dict[key] = FIELD_TOO_LONG return base_dict
class PredictorModelData(SonDocument): """Embedded model information to be used in Predictor """ model_id = fields.ObjectIdField() # reference to PredictorModel # denormalized from PredictorModel display_name = fields.StringField() weight = fields.NumField() task_data = fields.EmbeddedDocumentField(TaskData) @staticmethod def _get_model_data(model): return dict(model_id=model.id, display_name=model.display_name, weight=model.weight, task_data=model.task_data) @classmethod def init_with_model(cls, model): return cls(**cls._get_model_data(model)) def sync_with_model_instance(self, model): self.__dict__.update(self._get_model_data(model)) def __eq__(self, other): return isinstance(other, self.__class__) and other.model_id == self.model_id def __hash__(self): return hash(str(self.model_id))
class FacebookRateLimitInfo(Document): access_token = fields.StringField() failed_request_time = fields.DateTimeField() error_code = fields.NumField(null=True, choices=FB_RATE_LIMIT_ERRORS + [None]) path = fields.StringField() wait_until = fields.DateTimeField() channel = fields.StringField() log_item = fields.ObjectIdField() indexes = [('token', 'error_code')] manager = FacebookRateLimitInfoManager LIMITS_CONFIG = { THROTTLING_USER: BackOffStrategy(30*60, 30*60, 1.0), THROTTLING_APP: BackOffStrategy(225, 60*60, 2.0), ERROR_MISUSE: BackOffStrategy(60 * 60, 24 * 60 * 60, 3.0), THROTTLING_API_PATH: BackOffStrategy(60, 60*60, 2.0) } @property def wait_time(self): return (utc(self.wait_until) - utc(self.failed_request_time)).total_seconds() @property def remaining_time(self): return (utc(self.wait_until) - now()).total_seconds() @property def exc(self): return FacebookRateLimitError( code=self.error_code, remaining_time=self.remaining_time, path=self.path)
class BaseEventType(ArchivingAuthDocument): # or still Document? collection = 'EventType' manager = BaseEventTypeManager allow_inheritance = True SEP = ' -> ' platform = fields.StringField(required=True) # TODO: check uniqueness name = fields.StringField(required=True) # unique=True account_id = fields.ObjectIdField(required=True) @property def display_name(self): return self.SEP.join((self.platform, self.name)) @staticmethod def parse_display_name(display_name): platform, name = display_name.split(BaseEventType.SEP) return platform, name def to_dict(self, fields2show=None): data = super(BaseEventType, self).to_dict(fields2show) data['display_name'] = self.display_name return data
class ABCMultiClassPredictor(AuthDocument): collection = 'ABCMultiPreditor' abc_predictors = fields.ListField( fields.ObjectIdField()) # Just a grouping of binary predictors inclusion_threshold = fields.NumField(default=0.25) is_dirty = fields.BooleanField() __classes = None @property def classes(self): if not self.__classes: options = [ ABCPredictor.objects.get(o_id) for o_id in self.abc_predictors ] self.__classes = options return self.__classes def to_dict(self, fields_to_show=None): base_dict = super(ABCMultiClassPredictor, self).to_dict(fields_to_show=fields_to_show) base_dict['classes'] = [seg.to_dict() for seg in self.classes] return base_dict def score(self, customer_profile): scores = [] for option in self.classes: scores.append( (option.display_name, option.score(customer_profile))) return scores def match(self, customer_profile): max_score = 0 best_option = None for option in self.classes: option_score = option.score(customer_profile) if option_score > max_score: best_option = option max_score = option_score if max_score > self.inclusion_threshold: return True, best_option return False, None def accept(self, customer_profile, accepted_option): for option in self.classes: if option.id == accepted_option.id: option.accept(customer_profile) else: option.reject(customer_profile) self.is_dirty = True self.save() def reject(self, customer_profile, rejected_option): rejected_option.reject(customer_profile) self.is_dirty = True self.save()
class FollowerTrackingStatus(Document): channel = fields.ObjectIdField(db_field='cl') twitter_handle = fields.StringField(db_field='th') followers_count = fields.NumField(default=0, db_field='fc') followers_synced = fields.NumField(default=0, db_field='fs') sync_status = fields.StringField(default='idle', db_field='sy', choices=('idle', 'sync')) indexes = [Index(('channel', 'twitter_handle'), unique=True)]
class JourneyTypeStagePair(SonDocument): journey_type_id = fields.ObjectIdField('t') journey_stage_id = fields.ObjectIdField('s') @property def journey_type(self): from solariat_bottle.db.journeys.journey_type import JourneyType return JourneyType.objects.get(id=self.journey_type_id) @property def journey_stage(self): from solariat_bottle.db.journeys.journey_type import JourneyStageType return JourneyStageType.objects.get(self.journey_stage_id) @property def id(self): return str(self.journey_type_id), str(self.journey_stage_id)
class BaseProfileLabel(AuthDocument, ClassifierMixin): allow_inheritance = True collection = 'ProfileLabel' account_id = fields.ObjectIdField() display_name = fields.StringField() _feature_index = fields.NumField() @property def feature_index(self): if self._feature_index is None: self._feature_index = NumberSequences.advance( str(self.account_id) + '__' + self.__class__.__name__) self.save() return self._feature_index @classmethod def get_match(cls, profile): matches = [] for label in cls.objects(account_id=profile.account_id): if label.match(profile): matches.append(label) if not matches: LOGGER.warning("Found no match for profile %s and class %s" % (profile, cls)) return None if len(matches) > 1: LOGGER.warning( "Found more than one match for profile %s and class %s" % (profile, cls)) return matches[0] def save(self): self.packed_clf = self.clf.packed_model super(BaseProfileLabel, self).save() def make_profile_vector(self, profile): return { "content": profile.assigned_labels + [profile.location] + [str(profile.age)] } def match(self, profile): if self.id in profile.assigned_labels: return True if self.clf.score( self.make_profile_vector(profile)) > self.inclusion_threshold: return True return False def accept(self, profile): self.clf.train([self.make_profile_vector(profile)], [1]) def reject(self, profile): self.clf.train([self.make_profile_vector(profile)], [0])
class BaseFeedback(AuthDocument): created = fields.DateTimeField(default=now) action = fields.DictField() context = fields.DictField() matching_engine = fields.ObjectIdField() model_id = fields.ObjectIdField(null=True) reward = fields.NumField() # predicted score est_reward = fields.NumField() context_vector = fields.DictField() action_vector = fields.DictField() # scoring latency in ms score_runtime = fields.NumField() # time taken in millisecond to compute score # scoring error % score_diff = fields.NumField() # (reward - score) / reward indexes = [('matching_engine', 'created'), ]
class FacebookRequestLog(Document): channel = fields.ObjectIdField(null=True, db_field='cl') access_token = fields.StringField(db_field='tok') path = fields.StringField(db_field='uri') method = fields.StringField(db_field='m') args = fields.StringField(db_field='arg') post_args = fields.StringField(db_field='parg') start_time = fields.DateTimeField(db_field='ts') end_time = fields.DateTimeField(db_field='et') elapsed = fields.NumField(db_field='el') error = fields.StringField(db_field='er', null=True) indexes = [('start_time', 'access_token', 'path')] manager = FacebookRequestLogManager
class PostState(Document): INITIALIZED = False STATES = ARRIVED_IN_BOT, ARRIVED_IN_RECOVERY, ADDED_TO_WORKER_QUEUE, \ REMOVED_FROM_WORKER_QUEUE, DELIVERED_TO_TANGO, DELIVERED_TO_GSE_QUEUE, \ FETCHED_FROM_GSE_QUEUE, CONFIRMED_FROM_GSE_QUEUE = \ 'ARRIVED_IN_BOT', 'ARRIVED_IN_RECOVERY', 'ADDED_TO_WORKER_QUEUE', \ 'REMOVED_FROM_WORKER_QUEUE', 'DELIVERED_TO_TANGO', 'DELIVERED_TO_GSE_QUEUE', \ 'FETCHED_FROM_GSE_QUEUE', 'CONFIRMED_FROM_GSE_QUEUE' channel_id = fields.ObjectIdField() post_id = fields.StringField() state = fields.StringField(choices=STATES) indexes = [('post_id', ), ('channel_id', )]
class AuthToken(Document): """ Temporary key for user authentication """ manager = AuthTokenManager collection = 'authtoken' VALID_PERIOD = get_var('TOKEN_VALID_PERIOD', 24) # hours user = fields.ReferenceField(User) digest = fields.StringField(unique=True) app_key = fields.ObjectIdField(required=False) @property def is_valid(self): # return True if the token is not expired deadline = datetime.utcnow() - timedelta(hours=self.VALID_PERIOD) return deadline < self.created def to_dict(self): # Return dict for HTTP API return {'token': self.digest}
class DashboardWidget(Document): ''' Internal Structure representing the integartion data structure with a data stream provider. ''' created = fields.DateTimeField(db_field='c', default=datetime.now) settings = fields.DictField(db_field='s') order = fields.NumField(db_field='o') title = fields.StringField(db_field='t', required=True) user = fields.ReferenceField(User, db_field='u') dashboard_id = fields.ObjectIdField(required=True) manager = DashboardWidgetManager def to_dict(self): base_dict = dict(title=self.title, order=self.order, id=str(self.id), dashboard_id=str(self.dashboard_id)) base_dict.update(self.settings) return base_dict def copy_to(self, dashboard): new_widget_data = { 'title': self.title, 'user': dashboard.owner, 'dashboard_id': dashboard.id, } new_widget_data.update(self.settings) widget = DashboardWidget.objects.create_by_user(**new_widget_data) return widget def delete(self): dashboard = Dashboard.objects.get_by_user(self.user, id=self.dashboard_id) dashboard._remove_widget(self) super(DashboardWidget, self).delete() def __repr__(self): return "<DashboardWidget: %s; id: %s>" % (self.title, self.id)
class PredictorModel(Document): collection = 'PredictorModel' allow_inheritance = True version = fields.NumField() predictor = fields.ReferenceField('BasePredictor') parent = fields.ObjectIdField() weight = fields.NumField() display_name = fields.StringField() description = fields.StringField() # is_active = fields.BooleanField(default=False) task_data = fields.EmbeddedDocumentField(TaskData) last_run = fields.DateTimeField() context_features = fields.ListField(fields.DictField()) action_features = fields.ListField(fields.DictField()) train_data_percentage = fields.NumField(default=80) n_rows = fields.NumField() min_samples_thresould = fields.NumField(default=1) from_dt = fields.DateTimeField() to_dt = fields.DateTimeField() def score(self, *args, **kwargs): pass def feedback(self, *args, **kwargs): pass def search(self, *args, **kwargs): pass def to_json(self, *args, **kwargs): from solariat_bottle.db.predictors.base_predictor import PredictorConfigurationConversion data = super(PredictorModel, self).to_json(*args, **kwargs) data = PredictorConfigurationConversion.python_to_json(data) return data
class ChannelStats(ChannelAuthDocument): "Store stats for month, day and hour" manager = ChannelStatsManager channel = fields.ObjectIdField(required=True, unique_with='time_slot', db_field='cl') # The time slot is a numeric encoding of elapsed time # see utils.timeslot for details time_slot = fields.NumField(required=True, db_field="ts") number_of_posts = fields.NumField(default=0, db_field='nop') feature_counts = fields.DictField(db_field="fc") number_of_rejected_posts = fields.NumField(default=0, db_field='norp') number_of_starred_posts = fields.NumField(default=0, db_field='nosp') number_of_discarded_posts = fields.NumField(default=0, db_field='nodp') number_of_highlighted_posts = fields.NumField(default=0, db_field='nohp') number_of_actionable_posts = fields.NumField(default=0, db_field="noaep") number_of_assigned_posts = fields.NumField(default=0, db_field="noadp") number_of_replied_posts = fields.NumField(default=0, db_field="noalp") number_of_accepted_posts = fields.NumField(default=0, db_field="noacp") number_of_false_negative = fields.NumField(default=0, db_field="nofn") number_of_true_positive = fields.NumField(default=0, db_field="notp") number_of_false_positive = fields.NumField(default=0, db_field="nofp") # Quality Measures cumulative_relevance = fields.NumField(default=0.0, db_field="cr") cumulative_intention = fields.NumField(default=0.0, db_field="ci") # Outbound Statistics number_of_impressions = fields.NumField(default=0, db_field="noi") number_of_clicks = fields.NumField(default=0, db_field="noc") indexes = [('channel', ), ('time_slot')] @property def level(self): return decode_timeslot(self.time_slot)[1] @property def mean_relevance(self): if self.number_of_posts: return self.cumulative_relevance / self.number_of_posts return 0.0 @property def mean_intention(self): if self.number_of_actionable_posts: return self.cumulative_intention / self.number_of_actionable_posts return 0.0 def to_dict(self, fields2show=None): result = ChannelAuthDocument.to_dict(self, fields2show) del result['cumulative_relevance'] del result['cumulative_intention'] result['mean_relevance'] = self.mean_relevance result['mean_intention'] = self.mean_intention result['level'] = self.level return result def __str__(self): "String repr" return str(self.time_slot) @classmethod def _new_bulk_operation(cls, ordered=False): """ Allocates a new bulk DB operation (only available in PyMongo 2.7+) """ coll = cls.objects.coll if ordered: return coll.initialize_ordered_bulk_op() else: return coll.initialize_unordered_bulk_op() def inc(self, field_name, value, bulk=None): """ Issue an update DB operation that increments a specified field in the corresponding document. bulk -- an optional <BulkOperationBuilder> instance to store the postponed $inc operation instead of doing it right away (only available in PyMongo 2.7+) """ if not isinstance(value, (int, float)): raise AppException("%s must be integer or float" % value) query = self.__class__.get_query(time_slot=self.time_slot, channel=str(self.channel)) update = {'$inc': {self.fields[field_name].db_field: value}} if bulk is None: # sending a DB request right away coll = self.objects.coll return coll.update(query, update, upsert=True) else: # adding a postponed DB request to the bulk set return bulk.find(query).upsert().update_one(update) def set(self, field_name, value, bulk=None): """ Issue an update DB operation that sets a specified field in the corresponding document. bulk -- an optional <BulkOperationBuilder> instance to store the postponed $set operation instead of doing it right away (only available in PyMongo 2.7+) """ if not isinstance(value, (int, float)): raise AppException("%s must be integer or float" % value) query = self.__class__.get_query(time_slot=self.time_slot, channel=str(self.channel)) update = {'$set': {self.fields[field_name].db_field: value}} if bulk is None: # sending a DB request right away coll = self.objects.coll return coll.update(query, update, upsert=True) else: # adding a postponed DB request to the bulk set return bulk.find(query).upsert().update_one(update) def inc_feature_counts(self, speech_acts, bulk=None): """ Update SpeechAct stats. Issue an update DB operation that increments SpeechAct counters in the corresponding document. bulk -- an optional <BulkOperationBuilder> instance to store the postponed $inc operation instead of doing it right away (only available in PyMongo 2.7+) """ increments = {} field_name = self.fields['feature_counts'].db_field def add_increment(int_id): key = '%s.%s' % (field_name, int_id) increments[key] = 1 for sa in speech_acts: int_id = sa['intention_type_id'] if int_id: add_increment(int_id) if increments: # if there is at least one intention -- increment a counter for ALL also add_increment(ALL_INTENTIONS.oid) query = self.__class__.get_query(time_slot=self.time_slot, channel=str(self.channel)) update = {'$inc': increments} if bulk is None: # sending a DB request right away coll = self.objects.coll return coll.update(query, update, upsert=True) else: # adding a postponed DB request to the bulk set return bulk.find(query).upsert().update_one(update) def reload(self): source = ChannelStats.objects.find_one(time_slot=self.time_slot, channel=self.channel) if source is None: LOGGER.warning( "ChannelStats.reload() could not find a document for: channel=%s, time_slot=%s", self.channel, self.time_slot) #LOGGER.warning("Found instead only:") #for s in ChannelStats.objects(): # LOGGER.warning(' - %s %s', s.channel, s.time_slot) else: self.data = source.data
class Dashboard(AuthDocument): collection = 'Dashboard' manager = DashboardManager type_id = fields.ObjectIdField(required=True) title = fields.StringField(required=True) description = fields.StringField() owner = fields.ReferenceField(User) author = fields.ReferenceField(User) widgets = fields.ListField(fields.ObjectIdField()) shared_to = fields.ListField(fields.ObjectIdField()) filters = fields.DictField() created = fields.DateTimeField(default=datetime.now) admin_roles = {STAFF, ADMIN, REVIEWER, ANALYST} def to_dict(self, fields_to_show=None): rv = super(Dashboard, self).to_dict() rv['widgets'] = map(str, self.widgets) rv['shared_to'] = map(str, self.shared_to) rv['owner_name'] = '%s %s' % (self.owner.first_name or '', self.owner.last_name or '') rv['author_name'] = '%s %s' % (self.author.first_name or '', self.author.last_name or '') rv['owner_email'] = self.owner.email rv['author_email'] = self.author.email rv['account_id'] = str(self.owner.account.id) rv['type'] = DashboardType.objects.get(self.type_id).type return rv def __repr__(self): return "<Dashboard: %s; id: %s>" % (self.title, self.id) def _add_widget(self, widget): """ """ self.widgets.append(widget.id) self.save() def _remove_widget(self, widget): """ widget is not automatically deleted. To delete, use `.delete_widget()` instead. `widget.dashboard_id` will still point to this dashboard. """ self.widgets.remove(widget.id) self.save() def delete_widget(self, widget): if isinstance(widget, (basestring, fields.ObjectId)): widget = DashboardWidget.objects.get(widget) widget.delete() def delete(self): for widget_id in self.widgets: self.delete_widget(widget_id) super(Dashboard, self).delete_by_user(self.owner) def copy_to(self, user, title=None, description=None): dashboard_data = { 'type_id': self.type_id, 'title': title or self.title, 'description': description or self.description, 'author': self.owner, 'owner': user, 'widgets': [], 'shared_to': [], 'filters': self.filters, } # FIX: create_by_user is having role error dashboard = Dashboard.objects.create_by_user(user, **dashboard_data) #dashboard = Dashboard.objects.create(**dashboard_data) for widget_id in self.widgets: widget = DashboardWidget.objects.get(widget_id) widget.copy_to(dashboard) return dashboard
class InsightsAnalysis(Document): KEY_WEIGHT = 'discriminative_weight' KEY_RANK = 'rank' KEY_SCORE = 'score' KEY_VALUES = 'values' KEY_CROSSTAB = 'crosstab_results' KEY_VALUE_TYPE = 'value_type' KEY_PIE = 'pie' KEY_BAR = 'bar' KEY_BOX = 'boxplot' KEY_SCATTER = 'scatter' CLASSIFICATION_TYPE = 'classification' REGRESSION_TYPE = 'regression' BOOLEAN_METRIC = 'Boolean' NUMERIC_METRIC = 'Numeric' LABEL_METRIC = 'Label' METRIC_CONVERTED = "converted" METRIC_ABANDONED = "abandoned" METRIC_STUCK = "stuck" IDX_UNKNOWN = -1 IDX_SKIP = -2 NUM_TIMERANGE_SLOTS = 7 user = fields.ObjectIdField(db_field='usr') title = fields.StringField(db_field='te') created_at = fields.NumField(db_field='ca') account_id = fields.ObjectIdField(db_field='ac') filters = fields.DictField(db_field='ft', required=True) analysis_type = fields.StringField(choices=[CLASSIFICATION_TYPE, REGRESSION_TYPE], db_field='at') application = fields.StringField(db_field='an') # e.g. application which's used for the analysis analyzed_metric = fields.StringField(db_field='me') metric_type = fields.StringField(choices=[BOOLEAN_METRIC, NUMERIC_METRIC, LABEL_METRIC], db_field='mt') metric_values = fields.ListField(fields.StringField(), db_field='mv') metric_values_range = fields.ListField(fields.NumField(), db_field='mvr') # e.g. min/max Numeric values or unique labels progress = fields.NumField(db_field='pg', default=0) _results = fields.StringField(db_field='rt') _timerange_results = fields.StringField(db_field='trt') status_message = fields.StringField(db_field='msg') _cached_from_date = None _cached_to_date = None time_increment = None @property def status_progress(self): if self.progress == PROGRESS_STOPPED: return STATUS_STOPPED, 0 elif self.progress == 0: return STATUS_QUEUE, self.progress elif self.progress == PROGRESS_DONE: return STATUS_DONE, self.progress elif self.progress == PROGRESS_ERROR: return STATUS_ERROR, 0 else: return STATUS_IN_PROGRESS, self.progress def is_stopped(self): return self.progress == PROGRESS_STOPPED def compute_class_names(self): import json metric_names = [] try: if self.analyzed_metric == "stage-paths": for metric in self.metric_values: metric_info = json.loads(metric) metric_names.append("%s at step %s" % (metric_info['stage'], metric_info['step'])) return metric_names if self.analyzed_metric == "paths-comparison": for metric in self.metric_values: metric_info = json.loads(metric) metric_names.append( "%s %s (%s)" % (metric_info['measure'], metric_info['path'], metric_info['metric_value'])) return metric_names if self.metric_type == self.NUMERIC_METRIC and self.analysis_type == self.CLASSIFICATION_TYPE: metric_values = [ '%s(%s:%s)' % (self.analyzed_metric, self.metric_values_range[0], self.metric_values[0]), "%s(%s:%s)" % (self.analyzed_metric, self.metric_values[0], self.metric_values[1]), "%s(%s:%s)" % (self.analyzed_metric, self.metric_values[1], self.metric_values_range[1])] return metric_values except: import logging logging.exception(__name__) return self.metric_values def to_dict(self, fields2show=None): base_dict = super(InsightsAnalysis, self).to_dict() base_dict.pop('_results') base_dict.pop('_timerange_results') base_dict['results'] = self.results base_dict['timerange_results'] = self.timerange_results base_dict['status'] = self.status_progress base_dict['metric_values'] = self.compute_class_names() base_dict['metric_values_range'] = self.metric_values_range base_dict['level'] = self.get_timerange_level() return base_dict def get_timerange_level(self): try: return guess_timeslot_level(parse_datetime(self.filters['from']), parse_datetime(self.filters['to'])) except: LOGGER.warn('Unknown period to determine the timerange level') def get_user(self): return User.objects.get(self.user) def initialize_timeslot_counts(self): time_results = {} self.time_increment = (self._cached_to_date - self._cached_from_date).days * 24 / float(self.NUM_TIMERANGE_SLOTS) for class_idx in range(-1, self.get_num_classes()): time_results[class_idx] = dict() for slot_idx in xrange(self.NUM_TIMERANGE_SLOTS): timeslot = datetime_to_timestamp_ms(self._cached_from_date + timedelta(hours=self.time_increment * slot_idx)) time_results[class_idx][timeslot] = 0 return time_results def get_num_classes(self): if self.metric_type == self.NUMERIC_METRIC: return len(self.metric_values) + 1 else: return len(self.metric_values) + 2 def get_timeslot_index(self, item): for idx in xrange(self.NUM_TIMERANGE_SLOTS): if hasattr(item, 'created_at') and utc(item.created_at) > self._cached_from_date + timedelta(hours=self.time_increment * idx): continue else: break return datetime_to_timestamp_ms(self._cached_from_date + timedelta(hours=self.time_increment * idx)) def process(self): if self.application is None: self.application = self.get_user().account.selected_app if self.application == "Journey Analytics": # process_journeys_analysis.ignore(self) process_journeys_analysis(self) elif self.application == "Predictive Matching": # process_predictive_analysis.ignore(self) process_predictive_analysis(self) def save(self, **kw): if 'upsert' not in kw: kw['upsert'] = False # import json # analysis_file = open('analysis_' + str(self.id) + '.json', 'w') # json_data = {} # from bson import ObjectId # for key, val in self.data.iteritems(): # if not isinstance(val, ObjectId): # json_data[key] = val # json.dump(json_data, analysis_file) # analysis_file.close() if self.id: self.objects.update(self.data, **kw) else: self.id = self.objects.insert(self.data, **kw) def start(self): datetime.strptime('2011-01-01', '%Y-%m-%d') # dummy call (https://bugs.launchpad.net/openobject-server/+bug/947231/comments/8) self.process() def stop(self): self.progress = PROGRESS_STOPPED self.save() def restart(self): self.progress = 0 self.save() self.start() def terminate(self): self.progress = PROGRESS_ERROR self.status_message = 'Process had been terminated.' self.save() @property def timerange_results(self): if self._timerange_results: return json.loads(self._timerange_results) return {} @property def results(self): # Just in case we need some post-processing done if self._results: return json.loads(self._results) return {}
class ServiceChannelStats(ChannelAuthDocument): "Store stats for month, day and hour" manager = ServiceChannelStatsManager channel = fields.ObjectIdField(required=True, unique_with='time_slot', db_field='cl') # The time slot is a numeric encoding of elapsed time # see utils.timeslot for details time_slot = fields.NumField(required=True, db_field="ts") agent = fields.NumField( db_field='a', default=0, # 0 - all agents required=True) volume = fields.NumField(default=0, db_field='v') #number of outbound posts latency = fields.NumField(default=0, db_field='l') #reply delay indexes = [('channel', 'time_slot', 'agent')] @property def average_latency(self): """Returns average reply delay in seconds.""" if self.volume > 0: return float(self.latency) / float(self.volume) return 0.0 @property def level(self): return decode_timeslot(self.time_slot)[1] def to_dict(self, fields2show=None): result = ChannelAuthDocument.to_dict(self, fields2show) result['level'] = self.level return result def __str__(self): return "%s" % self.time_slot @property def _query(self): query = self.__class__.get_query(channel=str(self.channel), time_slot=self.time_slot, agent=self.agent) return query def update(self, **kwargs): document = {} for arg in kwargs: (operation, field) = arg.split('__', 1) operation = '$' + operation if operation not in document: document[operation] = {} db_field = self.fields[field].db_field document[operation][db_field] = kwargs[arg] self.objects.coll.update(self._query, document, multi=False, upsert=True) def reload(self): source = ServiceChannelStats.objects.find_one(time_slot=self.time_slot, channel=self.channel, agent=self.agent) self.data = source.data
class JobStatus(ArchivingAuthDocument): STATUSES = PENDING, RUNNING, ABANDONED, SUCCESSFUL, FAILED, \ RESUBMITTED, SLEEPING, TERMINATED = \ 'Pending', 'Running', 'Abandoned', 'Successful', 'Failed', \ 'Resubmitted', 'Sleeping', 'Terminated' RUNNABLE_STATUSES = PENDING, SLEEPING collection = 'jobs' account = fields.ObjectIdField(null=True) topic = fields.StringField() name = fields.StringField() args = fields.PickledField() kwargs = fields.PickledField() metadata = fields.DictField(null=True) created_at = fields.DateTimeField() started_date = fields.DateTimeField() completion_date = fields.DateTimeField() status = fields.StringField(choices=STATUSES) state = fields.DictField() last_activity = fields.DateTimeField() awake_at = fields.DateTimeField(null=True) @property def git_commit(self): return (self.metadata or {}).get('git_commit') @property def resubmission_info(self): return (self.metadata or {}).get('resubmitted') def abandon(self): if self.status == self.PENDING: self.status = self.ABANDONED res = self.objects.coll.update( { self.F.id: self.id, self.F.status: self.PENDING }, {"$set": { self.F.status: self.ABANDONED }}) if isinstance(res, dict) and res.get('nModified') == 1: return True def resume(self): if self.status != self.FAILED: raise RuntimeError("Job can not be resumed in '{}' state.".format( self.status)) from solariat_bottle.jobs.manager import manager job = manager.registry.get(self.name) res = job.submit(self.topic, self.name, self.args, self.kwargs, self.metadata) # updating old job meta = self.metadata or {} meta.update(resubmitted={ 'new_id': res.job_instance.id, 'result': str(res.submission_result) }) self.update(status=JobStatus.RESUBMITTED, metadata=meta) # updating new job meta = res.job_instance.metadata or {} meta.update(resubmitted={'old_id': self.id}) res.job_instance.update(metadata=meta) return [self, res.job_instance] def can_edit(self, user_or_group, admin_roles=None): if admin_roles is None: admin_roles = self.admin_roles account_check = user_or_group.is_staff or (user_or_group.is_admin and user_or_group.account.id == self.account) edit_check = (bool( set(admin_roles).intersection(set(user_or_group.user_roles))) or (hasattr(user_or_group, 'is_superuser') and user_or_group.is_superuser)) return account_check and edit_check @property def wait_time(self): if self.started_date and self.created_at: return (utc(self.started_date or now()) - utc(self.created_at)).total_seconds() @property def execution_time(self): if self.completion_date and self.started_date: now_ = now() return (utc(self.completion_date or now_) - utc(self.started_date or now_)).total_seconds()
class ResponseTermStats(ChannelHotTopics): post = fields.ObjectIdField(db_field="pt") response_types = fields.ListField(fields.StringField(), db_field="rt") @classmethod def increment(cls, channel_id=None, topic=None, intention_id=None, timeslot=None, is_leaf=True, response=None, n=1, **kw): #TODO: FIX return hashed_parents = map(hash, get_largest_subtopics(topic)) # --- ALL intentions stats --- stats = cls.objects.find_one( time_slot=timeslot, channel=channel_id, intention_id=ALL_INTENTIONS.oid, topic=topic, ) if not stats: stats = cls.objects.create( time_slot=timeslot, channel=channel_id, intention_id=ALL_INTENTIONS.oid, topic=topic, hashed_parents=hashed_parents, post=response.post.id, response_types=response.get_filter_types(), ) stats.inc_term_count(n=n) if is_leaf: stats.inc_topic_count(n=n) stats.upsert() # --- intention stats --- stats = cls.objects.find_one( time_slot=timeslot, channel=channel_id, intention_id=intention_id, topic=topic, ) if not stats: stats = cls.objects.create( time_slot=timeslot, channel=channel_id, intention_id=intention_id, topic=topic, hashed_parents=hashed_parents, post=response.post.id, response_types=response.get_filter_types(), ) stats.inc_term_count(n=n) if is_leaf: stats.inc_topic_count(n=n) stats.upsert() return stats
class CustomerJourney(AuthDocument, EventSequenceStatsMixin): FEAT_TYPE = 'type' FEAT_LABEL = 'label' FEAT_EXPR = 'field_expr' FEAT_NAME = 'name' collection = "CustomerJourney" manager = CustomerJourneyManager stage_name = fields.StringField( db_field='fs') # stage_name of current_stage # Dict in the form: # <strategy_type> : <list of index__stage_name>. strategy_type can be for now (default, platform, event_type) stage_sequences = fields.DictField(db_field='sseq') # Dict in the form # index__stage_name: {actual attributes computed for this specific stage} stage_information = fields.DictField(db_field='si') customer_id = fields.BaseField( db_field='ci') # dynamic profiles may use custom id type customer_name = fields.StringField( db_field='cn') # Just for quick access w/o extra db call agent_ids = fields.ListField(fields.ObjectIdField(), db_field='ag') agent_names = fields.ListField( fields.StringField(), db_field='ans') # Just for quick access w/o extra db calls journey_tags = fields.ListField(fields.ObjectIdField(), db_field='jts') channels = fields.ListField(fields.ObjectIdField(), db_field='chls') last_updated = fields.DateTimeField(db_field='lu') # time spent by events in each stage-eventtype status node_sequence = fields.ListField(fields.DictField(), db_field='nds') node_sequence_agr = fields.ListField(fields.StringField(), db_field='ndsn') # time spent by events in each stage-eventtype status journey_attributes_schema = fields.ListField(fields.DictField(), db_field='jas') first_event_date = fields.DateTimeField(db_field='fed') last_event_date = fields.DateTimeField(db_field='led') indexes = [('journey_type_id', 'journey_tags'), ('journey_attributes', ), ('journey_type_id', 'channels'), ('customer_id', ), ('agent_ids', )] parsers_cache = dict() @classmethod def to_mongo(cls, data, fill_defaults=True): """ Same as super method, except parser.evaluate is skipped (would be called in process_event) """ return super(CustomerJourney, cls).to_mongo(data, fill_defaults=fill_defaults, evaluate=False) @classmethod def metric_label(cls, metric, param, value): # from solariat_bottle.db.predictors.customer_segment import CustomerSegment if param == 'status': value = JourneyStageType.STATUS_TEXT_MAP[value] if param == 'journey_type_id': value = JourneyType.objects.get(value).display_name #value = value[0] if type(value) in [list, tuple] and value else value if value is not None else 'N/A' if value is None: value = 'N/A' return str(value) def ui_repr(self): base_repr = "Status: %s; Start date: %s; End date: %s;" % ( self.status, self.first_event_date, self.last_event_date) if self.customer_name: base_repr += " Customer: %s;" % self.customer_name if self.agent_names: base_repr += " Agents: %s;" % self.agent_names return base_repr def to_dict(self, *args, **kwargs): # from solariat_bottle.db.predictors.customer_segment import CustomerSegment base_dict = super(CustomerJourney, self).to_dict() base_dict['agents'] = map(str, self.agents) base_dict['channels'] = map(str, self.channels) base_dict['smart_tags'] = map(str, self.smart_tags) base_dict['journey_tags'] = map(str, self.journey_tags) base_dict['status'] = JourneyStageType.STATUS_TEXT_MAP[self.status] base_dict['string_repr'] = self.ui_repr() base_dict['journey_attributes'] = self.journey_attributes return base_dict def handle_add_tag(self, tag_id): tag_id = ObjectId(tag_id) self.update(addToSet__smart_tags=tag_id) def handle_remove_tag(self, tag_id): tag_id = ObjectId(tag_id) self.update(pull__smart_tags=tag_id) def apply_schema(self, expression, context): hash_key = str(expression) + '__'.join(context) if hash_key in CustomerJourney.parsers_cache: parser = CustomerJourney.parsers_cache[hash_key] else: parser = BaseParser(expression, context.keys()) CustomerJourney.parsers_cache[hash_key] = parser try: value = parser.evaluate(context) except TypeError: value = None return value def process_event(self, event, customer, agent, journey_stage_type): self._current_event = event received_event_from_past = False created_at = utc(event.created_at) last_updated = utc(self.last_updated) if self.last_updated else None if last_updated and created_at < last_updated: # log.error("=========RECEIVED EVENT FROM THE PAST %s %s < last updated %s" % ( # event, event.created_at, self.last_updated)) received_event_from_past = True # IMPORTANT: No mongo calls should be done here at all! if agent: if agent.id not in self.agent_ids: self.agent_ids.append(agent.id) # TODO: This needs to be enforced on profile dynamic classes as a separate specific # column (can be optional) self.agent_names.append(str(agent)) # TODO: Same as for agent profile, this needs to be set on dynamic class level self.customer_name = str(customer) if event.channels[0] not in self.channels: self.channels.append(event.channels[0]) if not received_event_from_past: if journey_stage_type: self.status = journey_stage_type.status self.last_event_date = event.created_at self.last_updated = event.created_at # TODO: This whole strategy switch will need to be changed to be defined somehow on journey level # TODO: ISSSUE for the last stage the information is not copied. Will need to do this on journey closure. for strategy in [ STRATEGY_DEFAULT, STRATEGY_PLATFORM, STRATEGY_EVENT_TYPE ]: self.check_for_stage_transition(strategy, event, journey_stage_type) schema_computed_attributes = dict() # All of these need to be returned directly from customer data (no extra mongo calls!) expression_context = dict(agents=self.agents, customer_profile=self.customer_profile, current_event=event, event_sequence=self.event_sequence, current_stage=self.current_stage, previous_stage=self.previous_stage, stage_sequence=self.stage_sequence) # for k in self.field_names: # expression_context[k] = getattr(self, k) # adding func with @property decorator to context for key in CustomerJourney.get_properties(): expression_context[key] = getattr(self, key) for schema_entry in self.journey_attributes_schema: expression = schema_entry[self.FEAT_EXPR] f_name = schema_entry[self.FEAT_NAME] schema_computed_attributes[f_name] = self.apply_schema( expression, expression_context) expression_context[f_name] = schema_computed_attributes[f_name] self.journey_attributes = schema_computed_attributes if self.status in [ JourneyStageType.COMPLETED, JourneyStageType.TERMINATED ]: self.node_sequence_agr = [] for i, item in enumerate(self.node_sequence): key, value = item.items()[0] self.node_sequence_agr.append(key) @classmethod def get_properties(cls): """ returns all list of member funcs decorated with @property """ from copy import deepcopy base = deepcopy(cls.field_names) base = [ field for field in base if field not in ('is_archived', '_t', 'acl', 'match_expression', 'journey_type_id', 'display_name', 'account_id', 'id', 'available_stages') ] base.extend([ name for name, value in vars(cls).items() if isinstance(value, property) ]) return base @property def CONSTANT_DATE_NOW(self): # For being picked up by context from datetime import datetime return datetime.now() @property def CONSTANT_ONE_DAYS(self): from datetime import timedelta return timedelta(hours=24) @property def CONSTANT_ONE_HOUR(self): from datetime import timedelta return timedelta(hours=1) def check_for_stage_transition(self, strategy, event, journey_stage_type): current_stage = self.get_current_stage(strategy) if strategy == STRATEGY_DEFAULT: new_stage = journey_stage_type.display_name if journey_stage_type else current_stage elif strategy == STRATEGY_EVENT_TYPE: new_stage = journey_stage_type.display_name + ':' + str( event.event_type) if journey_stage_type else current_stage elif strategy == STRATEGY_PLATFORM: new_stage = journey_stage_type.display_name + ':' + event.platform if journey_stage_type else current_stage if new_stage != current_stage: stage_index = self.get_current_index(strategy) new_stage_value = STAGE_INDEX_SEPARATOR.join( [new_stage, str(stage_index + 1)]) if current_stage is not None: full_stage_name = STAGE_INDEX_SEPARATOR.join( [current_stage, str(stage_index)]) self.stage_information[ full_stage_name] = self.compute_stage_information(strategy) self.stage_sequences[strategy] = self.stage_sequences.get( strategy, []) + [new_stage_value] if strategy == STRATEGY_DEFAULT: self.stage_name = journey_stage_type.display_name self.stage_sequence_names.append(new_stage) if strategy == STRATEGY_EVENT_TYPE: if current_stage is None and new_stage is None: return # TODO: This is still kind of hard coded for MPC if new_stage != current_stage: self.node_sequence.append({new_stage: 1}) else: self.node_sequence[-1][new_stage] += 1 def compute_stage_information(self, strategy): info = dict() for key, val in self.journey_attributes.iteritems(): info[key] = val info['end_date'] = self.last_event_date if len(self.stage_sequences.get(strategy, [])) <= 1: info['start_date'] = self.first_event_date else: info['start_date'] = self.stage_information[ self.stage_sequences[strategy][-2]]['end_date'] return info def close_journey(self): for strategy in [ STRATEGY_DEFAULT, STRATEGY_PLATFORM, STRATEGY_EVENT_TYPE ]: current_stage = self.get_current_stage(strategy) stage_index = self.get_current_index(strategy) if current_stage is not None: full_stage_name = STAGE_INDEX_SEPARATOR.join( [current_stage, str(stage_index)]) self.stage_information[ full_stage_name] = self.compute_stage_information(strategy) self.save() def get_current_stage(self, strategy_type): if not self.stage_sequences.get(strategy_type): return None else: return self.stage_sequences.get(strategy_type)[-1].split( STAGE_INDEX_SEPARATOR)[0] def get_current_index(self, strategy_type): if not self.stage_sequences.get(strategy_type): return -1 else: return int( self.stage_sequences.get(strategy_type)[-1].split( STAGE_INDEX_SEPARATOR)[1]) def stage_sequence_by_strategy(self, strategy): return [ val.split(STAGE_INDEX_SEPARATOR)[0] for val in self.stage_sequences[strategy] ] def __get_agents(self): if hasattr(self, '_agents'): return self._agents else: self._agents = self.account.get_agent_profile_class().objects.find( id__in=self.agent_ids)[:] return self._agents def __set_agents(self, agents): self._agents = agents agents = property(__get_agents, __set_agents) def __get_customer_profile(self): if hasattr(self, '_customer_profile'): return self._customer_profile else: self._customer_profile = self.account.get_customer_profile_class( ).objects.get(self.customer_id) return self._customer_profile def __set_customer_profile(self, customer_profile): self._customer_profile = customer_profile customer_profile = property(__get_customer_profile, __set_customer_profile) def __get_current_event(self): if hasattr(self, '_current_event'): return self._current_event else: self._current_event = self.event_sequence[ -1] if self.event_sequence else None return self._current_event def __set_current_event(self, event): self._current_event = event current_event = property(__get_current_event, __set_current_event) def __get_event_sequence(self): if hasattr(self, '_event_sequence'): return self._event_sequence else: from solariat_bottle.db.account import Account account = Account.objects.get(self.account_id) CustomerProfile = account.get_customer_profile_class() try: customer = CustomerProfile.objects.get(self.customer_id) except CustomerProfile.DoesNotExist: self._event_sequence = [] return self._event_sequence if self.first_event_date and self.last_event_date: events = Event.objects.events_for_actor( self.first_event_date, self.last_event_date, customer.actor_num)[:] self._event_sequence = events return self._event_sequence # event_type_ids = [x.event_type for x in events] # event_types = EventType.objects(id__in=event_type_ids)[:] # event_type_map = {str(x.id): x.name for x in event_types} # return [event_type_map[x.event_type] for x in events] self._event_sequence = [] return self._event_sequence def __set_event_sequence(self, event_sequence): self._event_sequence = event_sequence event_sequence = property(__get_event_sequence, __set_event_sequence) @property def current_stage(self): if len(self.stage_sequences.get(STRATEGY_DEFAULT, [])) == 0: return None else: last_stage = self.stage_sequences[STRATEGY_DEFAULT][-1].split( STAGE_INDEX_SEPARATOR)[0] return last_stage @property def nps(self): nps1 = self.journey_attributes.get('nps') event = self.current_event from solariat_bottle.db.post.nps import NPSOutcome if isinstance(event, NPSOutcome): nps2 = self.current_event.score else: nps2 = None return max(nps1, nps2) @staticmethod def nps_value_to_label(value): if value is None: return 'n/a' elif 0 <= value <= 6: return 'detractor' elif value in (7, 8): return 'passive' elif value in (9, 10): return 'promoter' else: raise Exception("invalid nps value (%r given)" % value) @property def nps_category(self): # from solariat_bottle.views.facets import nps_value_to_label if self.nps == 'N/A': return 'N/A' else: return self.nps_value_to_label(self.nps) @property def previous_stage(self): if self.current_stage is None: return None if len(self.stage_sequences.get(STRATEGY_DEFAULT, [])) <= 1: return None else: last_stage = self.stage_sequences[STRATEGY_DEFAULT][-2].split( STAGE_INDEX_SEPARATOR)[0] return last_stage @property def stage_sequence(self): if len(self.stage_sequences.get(STRATEGY_DEFAULT, [])) == 0: return [] else: return [ val.split(STAGE_INDEX_SEPARATOR)[0] for val in self.stage_sequences[STRATEGY_DEFAULT] ] @property def first_event(self): event_sequence = self.event_sequence if event_sequence: return event_sequence[0] else: return None @property def is_abandoned(self): if self.status == JourneyStageType.TERMINATED: return 1 else: return 0 @property def days(self): if self.first_event_date and self.last_event_date: return (utc(self.last_event_date) - utc(self.first_event_date)).days else: return None