示例#1
0
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
示例#3
0
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)
示例#4
0
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', )]
示例#5
0
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))
示例#6
0
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
示例#7
0
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)
示例#8
0
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)
示例#9
0
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
示例#10
0
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
示例#11
0
文件: base.py 项目: princez1214/flask
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))
示例#12
0
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)
示例#13
0
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
示例#14
0
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()
示例#15
0
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)]
示例#16
0
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)
示例#17
0
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])
示例#18
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'), ]
示例#19
0
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
示例#20
0
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', )]
示例#21
0
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}
示例#22
0
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)
示例#23
0
文件: base.py 项目: princez1214/flask
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
示例#24
0
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
示例#25
0
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
示例#26
0
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 {}
示例#27
0
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
示例#28
0
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()
示例#29
0
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
示例#30
0
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