class VoucherUpdate(jsonobject.JsonObject): voucher_id = jsonobject.StringProperty(required=True) payment_status = jsonobject.StringProperty(required=True, choices=['success', 'failure']) payment_amount = jsonobject.DecimalProperty(required=False) failure_description = jsonobject.StringProperty(required=False) case_type = CASE_TYPE_VOUCHER @property def case_id(self): return self.voucher_id @property def properties(self): if self.payment_status == 'success': return { 'state': 'paid', 'amount_fulfilled': self.payment_amount, 'date_fulfilled': datetime.datetime.utcnow().date().isoformat(), } else: return { 'state': 'rejected', 'reason_rejected': self.failure_description, 'date_rejected': datetime.datetime.utcnow().date().isoformat(), }
class AggregationSpec(jsonobject.JsonObject): domain = jsonobject.StringProperty(required=True) table_id = jsonobject.StringProperty(required=True) display_name = jsonobject.StringProperty() primary_table = jsonobject.ObjectProperty(PrimaryTableSpec) time_aggregation = jsonobject.ObjectProperty(TimeAggregationConfigSpec) secondary_tables = jsonobject.ListProperty(SecondaryTableSpec)
class IncentiveUpdate(PaymentUpdate): eventType = jsonobject.StringProperty(required=True, choices=['Incentive']) eventID = jsonobject.StringProperty( required=False, choices=BETS_EVENT_IDS) case_type = CASE_TYPE_EPISODE @property def properties(self): status_key = 'tb_incentive_{}_status'.format(self.eventID) comments_key = 'tb_incentive_{}_comments'.format(self.eventID) if self.status == SUCCESS: amount_key = 'tb_incentive_{}_amount'.format(self.eventID) date_key = 'tb_incentive_{}_payment_date'.format(self.eventID) payment_mode_key = 'tb_incentive_{}_payment_mode'.format(self.eventID) check_number_key = 'tb_incentive_{}_check_number'.format(self.eventID) bank_name_key = 'tb_incentive_{}_bank_name'.format(self.eventID) return { status_key: 'paid', amount_key: self.amount, date_key: self.paymentDate.isoformat(), comments_key: self.comments or "", payment_mode_key: self.paymentMode or "", check_number_key: self.checkNumber or "", bank_name_key: self.bankName or "", } else: date_key = 'tb_incentive_{}_rejection_date'.format(self.eventID) reason_key = 'tb_incentive_{}_rejection_reason'.format(self.eventID) return { status_key: 'rejected', date_key: self.paymentDate.isoformat(), reason_key: self.failureDescription or "", comments_key: self.comments or "", }
class ChangeMeta(jsonobject.JsonObject): """ Metadata about a change. If available, this will be set on Change.metadata. This is only used in kafka-based pillows. """ _allow_dynamic_properties = False document_id = DefaultProperty(required=True) # Only relevant for Couch documents document_rev = jsonobject.StringProperty() # 'couch' or 'sql' data_source_type = jsonobject.StringProperty(required=True) # couch database name or one of data sources listed in corehq.apps.change_feed.data_sources data_source_name = jsonobject.StringProperty(required=True) # doc_type property of doc or else the topic name document_type = DefaultProperty() document_subtype = jsonobject.StringProperty() domain = jsonobject.StringProperty() is_deletion = jsonobject.BooleanProperty() publish_timestamp = jsonobject.DateTimeProperty(default=datetime.utcnow) # track of retry attempts attempts = jsonobject.IntegerProperty(default=0)
class ElasticsearchIndexInfo(jsonobject.JsonObject): index = jsonobject.StringProperty(required=True) alias = jsonobject.StringProperty() type = jsonobject.StringProperty() mapping = jsonobject.DictProperty() hq_index_name = jsonobject.StringProperty() def __str__(self): return '{} ({})'.format(self.alias, self.index) @property def meta(self): meta_settings = deepcopy(ES_INDEX_SETTINGS['default']) meta_settings.update(ES_INDEX_SETTINGS.get(self.hq_index_name, {})) meta_settings.update( ES_INDEX_SETTINGS.get(settings.SERVER_ENVIRONMENT, {}).get(self.hq_index_name, {})) if settings.ES_SETTINGS is not None: for hq_index_name in ['default', self.hq_index_name]: for key, value in settings.ES_SETTINGS.get(hq_index_name, {}).items(): if value is REMOVE_SETTING: del meta_settings['settings'][key] else: meta_settings['settings'][key] = value return meta_settings def to_json(self): json = super(ElasticsearchIndexInfo, self).to_json() json['meta'] = self.meta return json
class XFormPhoneMetadata(jsonobject.JsonObject): """ Metadata of an xform, from a meta block structured like: <Meta> <timeStart /> <timeEnd /> <instanceID /> <userID /> <deviceID /> <username /> <!-- CommCare extension --> <appVersion /> <location /> </Meta> See spec: https://bitbucket.org/javarosa/javarosa/wiki/OpenRosaMetaDataSchema username is not part of the spec but included for convenience """ timeStart = jsonobject.DateTimeProperty() timeEnd = jsonobject.DateTimeProperty() instanceID = jsonobject.StringProperty() userID = jsonobject.StringProperty() deviceID = jsonobject.StringProperty() username = jsonobject.StringProperty() appVersion = jsonobject.StringProperty() location = GeoPointProperty()
class FieldSpec(jsonobject.StrictJsonObject): field = jsonobject.StringProperty() description = jsonobject.StringProperty() show_in_menu = jsonobject.BooleanProperty(default=False) discoverable = jsonobject.BooleanProperty(default=True) values_hints = jsonobject.ListProperty() deprecated = jsonobject.BooleanProperty(default=False)
class TaskStatusResultError(jsonobject.StrictJsonObject): title = jsonobject.StringProperty() description = jsonobject.StringProperty() column = jsonobject.StringProperty() # usually an int, but field has been hijacked to include other debug info # search 'row_number=' in tasks.py # longer-term solution would be to have another field for debug info rows = jsonobject.ListProperty()
class ElasticsearchIndexInfo(jsonobject.JsonObject): index = jsonobject.StringProperty(required=True) alias = jsonobject.StringProperty() type = jsonobject.StringProperty() meta = jsonobject.DictProperty() mapping = jsonobject.DictProperty() def __unicode__(self): return u'{} ({})'.format(self.alias, self.index)
class ChangeMeta(jsonobject.JsonObject): """ Metadata about a change. If available, this will be set on Change.metadata. This is only used in kafka-based pillows. """ document_id = DefaultProperty(required=True) data_source_type = jsonobject.StringProperty(required=True) data_source_name = jsonobject.StringProperty(required=True) document_type = DefaultProperty() document_subtype = jsonobject.StringProperty() domain = jsonobject.StringProperty() is_deletion = jsonobject.BooleanProperty() _allow_dynamic_properties = False
class FormDetails(jsonobject.JsonObject): xmlns = jsonobject.StringProperty() app = jsonobject.ObjectProperty(AppInfo) module = jsonobject.ObjectProperty(AppPart) form = jsonobject.ObjectProperty(AppPart) is_deleted = jsonobject.BooleanProperty() is_user_registration = jsonobject.BooleanProperty(default=False)
class ChangeMeta(jsonobject.JsonObject): """ Metadata about a change. If available, this will be set on Change.metadata. This is only used in kafka-based pillows. """ document_id = DefaultProperty(required=True) document_rev = jsonobject.StringProperty() # Only relevant for Couch documents data_source_type = jsonobject.StringProperty(required=True) data_source_name = jsonobject.StringProperty(required=True) document_type = DefaultProperty() document_subtype = jsonobject.StringProperty() domain = jsonobject.StringProperty() is_deletion = jsonobject.BooleanProperty() publish_timestamp = jsonobject.DateTimeProperty(default=datetime.utcnow) _allow_dynamic_properties = False
class PaymentUpdate(jsonobject.JsonObject): id = jsonobject.StringProperty(required=True) status = jsonobject.StringProperty(required=True, choices=[SUCCESS, FAILURE]) amount = jsonobject.FloatProperty(required=False) paymentDate = FlexibleDateTimeProperty(required=True) comments = jsonobject.StringProperty(required=False) failureDescription = jsonobject.StringProperty(required=False) paymentMode = jsonobject.StringProperty(required=False) checkNumber = jsonobject.StringProperty(required=False) bankName = jsonobject.StringProperty(required=False) @classmethod def wrap(cls, data): amount = data.get('amount', None) if amount: try: float_amount = float(amount) data['amount'] = float_amount except (ValueError, TypeError): raise BadValueError( "amount '{}' is not a number".format(amount)) return super(PaymentUpdate, cls).wrap(data) @property def case_id(self): return self.id
class IncentiveUpdate(jsonobject.JsonObject): beneficiary_id = jsonobject.StringProperty(required=True) episode_id = jsonobject.StringProperty(required=True) payment_status = jsonobject.StringProperty(required=True, choices=['success', 'failure']) payment_amount = jsonobject.DecimalProperty(required=False) failure_description = jsonobject.StringProperty(required=False) bets_parent_event_id = jsonobject.StringProperty(required=False, choices=BETS_EVENT_IDS) case_type = CASE_TYPE_EPISODE @property def case_id(self): return self.episode_id @property def properties(self): status_key = 'tb_incentive_{}_status'.format(self.bets_parent_event_id) if self.payment_status == 'success': amount_key = 'tb_incentive_{}_amount'.format( self.bets_parent_event_id) date_key = 'tb_incentive_{}_payment_date'.format( self.bets_parent_event_id) return { status_key: 'paid', amount_key: self.payment_amount, date_key: datetime.datetime.utcnow().date().isoformat(), } else: date_key = 'tb_incentive_{}_rejection_date'.format( self.bets_parent_event_id) reason_key = 'tb_incentive_{}_rejection_reason'.format( self.bets_parent_event_id) return { status_key: 'rejected', date_key: datetime.datetime.utcnow().date().isoformat(), reason_key: self.failure_description, }
class StockReportHelper(jsonobject.JsonObject): """ Intermediate class for dealing with stock XML """ domain = jsonobject.StringProperty() form_id = jsonobject.StringProperty() timestamp = jsonobject.DateTimeProperty() tag = jsonobject.StringProperty() transactions = jsonobject.ListProperty(lambda: StockTransactionHelper) server_date = jsonobject.DateTimeProperty() deprecated = jsonobject.BooleanProperty() @property def report_type(self): # this is for callers to be able to use a less confusing name return self.tag @classmethod def make_from_form(cls, form, timestamp, tag, transactions): deprecated = form.is_deprecated return cls( domain=form.domain, form_id=form.form_id if not deprecated else form.orig_id, timestamp=timestamp, tag=tag, transactions=transactions, server_date=form.received_on, deprecated=deprecated, ) def validate(self): """ Validates this object as best we can and raises Exceptions if we find anything invalid . """ if any(transaction_helper.product_id in ('', None) for transaction_helper in self.transactions): raise MissingProductId( _('Product IDs must be set for all ledger updates!'))
class ElasticsearchIndexInfo(jsonobject.JsonObject): index = jsonobject.StringProperty(required=True) alias = jsonobject.StringProperty() type = jsonobject.StringProperty() mapping = jsonobject.DictProperty() def __unicode__(self): return u'{} ({})'.format(self.alias, self.index) @property def meta(self): meta_settings = deepcopy(settings.ES_META['default']) meta_settings.update(settings.ES_META.get(self.alias, {})) meta_settings.update( settings.ES_META.get(settings.SERVER_ENVIRONMENT, {}).get(self.alias, {})) return meta_settings def to_json(self): json = super(ElasticsearchIndexInfo, self).to_json() json['meta'] = self.meta return json
class CaseUploadJSON(jsonobject.StrictJsonObject): domain = jsonobject.StringProperty(required=True) # In user display format, e.g. Dec 08, 2016 19:19 EST created_display = jsonobject.StringProperty(required=True) created = jsonobject.StringProperty(required=True) upload_id = jsonobject.StringProperty(required=True) task_status = jsonobject.ObjectProperty(lambda: TaskStatus) user_name = jsonobject.StringProperty(required=True) case_type = jsonobject.StringProperty(required=True) comment = jsonobject.StringProperty() upload_file_name = jsonobject.StringProperty() upload_file_length = jsonobject.IntegerProperty() upload_file_download_allowed = jsonobject.BooleanProperty(required=True) upload_comment_edit_allowed = jsonobject.BooleanProperty(required=True)
class TestFormMetadata(jsonobject.JsonObject): domain = jsonobject.StringProperty(required=False) xmlns = jsonobject.StringProperty(default='http://openrosa.org/formdesigner/form-processor') app_id = jsonobject.StringProperty(default='123') form_name = jsonobject.StringProperty(default='New Form') device_id = jsonobject.StringProperty(default='DEV IL') user_id = jsonobject.StringProperty(default='cruella_deville') username = jsonobject.StringProperty(default='eve') time_end = jsonobject.DateTimeProperty(default=datetime(2013, 4, 19, 16, 52, 2)) time_start = jsonobject.DateTimeProperty(default=datetime(2013, 4, 19, 16, 53, 2)) # Set this property to fake the submission time received_on = jsonobject.DateTimeProperty(default=datetime.utcnow)
class XFormPhoneMetadata(jsonobject.JsonObject): """ Metadata of an xform, from a meta block structured like: <Meta> <timeStart /> <timeEnd /> <instanceID /> <userID /> <deviceID /> <username /> <!-- CommCare extension --> <appVersion /> <location /> </Meta> See spec: https://bitbucket.org/javarosa/javarosa/wiki/OpenRosaMetaDataSchema username is not part of the spec but included for convenience """ timeStart = jsonobject.DateTimeProperty() timeEnd = jsonobject.DateTimeProperty() instanceID = jsonobject.StringProperty() userID = jsonobject.StringProperty() deviceID = jsonobject.StringProperty() username = jsonobject.StringProperty() appVersion = jsonobject.StringProperty() location = GeoPointProperty() @property def commcare_version(self): from corehq.apps.receiverwrapper.util import get_commcare_version_from_appversion_text from distutils.version import LooseVersion version_text = get_commcare_version_from_appversion_text(self.appVersion) if version_text: return LooseVersion(version_text)
class StockLedgerValueWrapper(jsonobject.JsonObject): """ Wrapper class to abstract StockState and the equivalent model coming out of Elasticsearch """ domain = jsonobject.StringProperty() case_id = jsonobject.StringProperty() section_id = jsonobject.StringProperty() entry_id = jsonobject.StringProperty() balance = jsonobject.DecimalProperty() # todo: should this be an int? last_modified = jsonobject.DateTimeProperty() last_modified_form_id = jsonobject.StringProperty() daily_consumption = jsonobject.FloatProperty() location_id = jsonobject.StringProperty() _sql_product = None _sql_location = None def __init__(self, _obj=None, sql_product=None, sql_location=None, *args, **kwargs): self._sql_product = sql_product self._sql_location = sql_location super(StockLedgerValueWrapper, self).__init__(_obj, *args, **kwargs) @property def sql_product(self): if self.entry_id and not self._sql_product: try: self._sql_product = SQLProduct.objects.get(domain=self.domain, product_id=self.entry_id) except ObjectDoesNotExist: # todo: cache this result so multiple failing calls don't keep hitting the DB return None return self._sql_product @property def sql_location(self): if self.location_id and not self._sql_location: try: self._sql_location = SQLLocation.objects.get(domain=self.domain, location_id=self.location_id) except ObjectDoesNotExist: # todo: cache this result so multiple failing calls don't keep hitting the DB return None return self._sql_location @classmethod def from_stock_state(cls, stock_state): return cls( case_id=stock_state.case_id, section_id=stock_state.section_id, entry_id=stock_state.product_id, balance=stock_state.stock_on_hand, last_modified=stock_state.last_modified_date, last_modified_form_id=stock_state.last_modified_form_id, daily_consumption=stock_state.daily_consumption, location_id=stock_state.location_id, sql_location=stock_state.sql_location, sql_product=stock_state.sql_product, )
class PaymentUpdate(jsonobject.JsonObject): id = jsonobject.StringProperty(required=True) status = jsonobject.StringProperty(required=True, choices=[SUCCESS, FAILURE]) amount = jsonobject.IntegerProperty(required=False) paymentDate = FlexibleDateTimeProperty(required=True) comments = jsonobject.StringProperty(required=False) failureDescription = jsonobject.StringProperty(required=False) paymentMode = jsonobject.StringProperty(required=False) checkNumber = jsonobject.StringProperty(required=False) bankName = jsonobject.StringProperty(required=False) @property def case_id(self): return self.id
class StockLedgerValueWrapper(jsonobject.JsonObject): """ Wrapper class to abstract StockState and the equivalent model coming out of Elasticsearch """ domain = jsonobject.StringProperty() case_id = jsonobject.StringProperty() section_id = jsonobject.StringProperty() entry_id = jsonobject.StringProperty() balance = jsonobject.DecimalProperty() # todo: should this be an int? last_modified = jsonobject.DateTimeProperty() last_modified_form_id = jsonobject.StringProperty() daily_consumption = jsonobject.FloatProperty() location_id = jsonobject.StringProperty() @property @quickcache(['self.domain', 'self.entry_id']) def sql_product(self): try: return SQLProduct.objects.get(domain=self.domain, product_id=self.entry_id) except ObjectDoesNotExist: return None @property @quickcache(['self.domain', 'self.location_id']) def sql_location(self): try: return ( SQLLocation.objects .prefetch_related('location_type') .get(domain=self.domain, location_id=self.location_id) ) except ObjectDoesNotExist: return None @classmethod def from_stock_state(cls, stock_state): return cls( case_id=stock_state.case_id, section_id=stock_state.section_id, entry_id=stock_state.product_id, balance=stock_state.stock_on_hand, last_modified=stock_state.last_modified_date, last_modified_form_id=stock_state.last_modified_form_id, daily_consumption=stock_state.daily_consumption, location_id=stock_state.location_id, sql_location=stock_state.sql_location, sql_product=stock_state.sql_product, )
class VoucherUpdate(PaymentUpdate): eventType = jsonobject.StringProperty(required=True, choices=['Voucher']) case_type = CASE_TYPE_VOUCHER @property def properties(self): if self.status == SUCCESS: return { 'state': 'paid', 'amount_fulfilled': self.amount, 'date_fulfilled': self.paymentDate.isoformat(), 'comments': self.comments or "", 'payment_mode': self.paymentMode or "", 'check_number': self.checkNumber or "", 'bank_name': self.bankName or "", } else: return { 'state': 'rejected', 'comments': self.comments or "", 'reason_rejected': self.failureDescription or "", 'date_rejected': self.paymentDate.isoformat(), }
class StockTransactionHelper(jsonobject.JsonObject): """ Helper class for transactions """ product_id = jsonobject.StringProperty() action = jsonobject.StringProperty() subaction = jsonobject.StringProperty() domain = jsonobject.StringProperty() quantity = jsonobject.DecimalProperty() # todo: this field is never populated during normal form submissions, only on SMS submissions location_id = jsonobject.StringProperty() timestamp = jsonobject.DateTimeProperty() case_id = jsonobject.StringProperty() section_id = jsonobject.StringProperty() @property def ledger_reference(self): return UniqueLedgerReference( case_id=self.case_id, section_id=self.section_id, entry_id=self.product_id ) @property def relative_quantity(self): """ Gets the quantity of this transaction as a positive or negative number depending on the action/context """ if self.action == const.StockActions.CONSUMPTION: return -self.quantity else: return self.quantity def action_config(self, commtrack_config): action = CommtrackActionConfig(action=self.action, subaction=self.subaction) for a in commtrack_config.all_actions: if a.name == action.name: return a return None @property def date(self): if self.timestamp: return dateparse.json_format_datetime(self.timestamp) def to_xml(self, E=None, **kwargs): if not E: E = XML() return E.entry( id=self.product_id, quantity=str(self.quantity if self.action != StockActions.STOCKOUT else 0), ) @property def category(self): return 'stock' def fragment(self): """ A short string representation of this to be used in sms correspondence """ if self.quantity is not None: quant = self.quantity else: quant = '' # FIXME product fetch here is inefficient return '%s%s' % (Product.get(self.product_id).code.lower(), quant) def __repr__(self): return '{action} ({subaction}): {quantity} (loc: {location_id}, product: {product_id})'.format( action=self.action, subaction=self.subaction, quantity=self.quantity, location_id=self.location_id, product_id=self.product_id, )
class SingleFieldColumnProperties(jsonobject.JsonObject): referenced_column = jsonobject.StringProperty(required=True)
class SqlColumnProperties(jsonobject.JsonObject): datatype = DataTypeProperty(required=True) statement = jsonobject.StringProperty(required=True) statement_params = jsonobject.DictProperty()
class MonthlyPerformanceSummary(jsonobject.JsonObject): month = jsonobject.DateProperty() domain = jsonobject.StringProperty() performance_threshold = jsonobject.IntegerProperty() active = jsonobject.IntegerProperty() performing = jsonobject.IntegerProperty() def __init__(self, domain, month, selected_users, active_not_deleted_users, performance_threshold, previous_summary=None, delta_high_performers=0, delta_low_performers=0): self._previous_summary = previous_summary self._next_summary = None self._is_final = None base_queryset = MALTRow.objects.filter( domain_name=domain, month=month, user_type__in=['CommCareUser', 'CommCareUser-Deleted'], user_id__in=active_not_deleted_users, ) if selected_users: base_queryset = base_queryset.filter(user_id__in=selected_users, ) self._user_stat_from_malt = (base_queryset.values( 'user_id', 'username').annotate(total_num_forms=Sum('num_of_forms'))) num_performing_users = (self._user_stat_from_malt.filter( total_num_forms__gte=performance_threshold).count()) num_active_users = self._user_stat_from_malt.count() num_low_performing_user = num_active_users - num_performing_users if self._previous_summary: delta_high_performers = num_performing_users - self._previous_summary.number_of_performing_users delta_low_performers = num_low_performing_user - self._previous_summary.number_of_low_performing_users super(MonthlyPerformanceSummary, self).__init__( month=month, domain=domain, performance_threshold=performance_threshold, active=num_active_users, total_users_by_month=0, percent_active=0, performing=num_performing_users, delta_high_performers=delta_high_performers, delta_low_performers=delta_low_performers, ) def set_next_month_summary(self, next_month_summary): self._next_summary = next_month_summary def set_percent_active(self): self.total_users_by_month = self.inactive + self.number_of_active_users if self.total_users_by_month: self.percent_active = float(self.number_of_active_users) / float( self.total_users_by_month) else: self.percent_active = 0 @property def number_of_performing_users(self): return self.performing @property def number_of_low_performing_users(self): return self.active - self.performing @property def number_of_active_users(self): return self.active @property @memoized def inactive(self): dropouts = self.get_dropouts() return len(dropouts) if dropouts else 0 @property def previous_month(self): prev_year, prev_month = add_months(self.month.year, self.month.month, -1) return datetime.datetime(prev_year, prev_month, 1) @property def delta_high_performing(self): if self._previous_summary: return self.number_of_performing_users - self._previous_summary.number_of_performing_users else: return self.number_of_performing_users @property def delta_high_performing_pct(self): if (self.delta_high_performing and self._previous_summary and self._previous_summary.number_of_performing_users): return float(self.delta_high_performing / float( self._previous_summary.number_of_performing_users)) * 100. @property def delta_low_performing(self): if self._previous_summary: return self.number_of_low_performing_users - self._previous_summary.number_of_low_performing_users else: return self.number_of_low_performing_users @property def delta_low_performing_pct(self): if self.delta_low_performing and self._previous_summary \ and self._previous_summary.number_of_low_performing_users: return float(self.delta_low_performing / float( self._previous_summary.number_of_low_performing_users)) * 100. @property def delta_active(self): return self.active - self._previous_summary.active if self._previous_summary else self.active @property def delta_active_pct(self): if self.delta_active and self._previous_summary and self._previous_summary.active: return float(self.delta_active / float(self._previous_summary.active)) * 100. @property def delta_inactive(self): return self.inactive - self._previous_summary.inactive if self._previous_summary else self.inactive @property def delta_inactive_pct(self): if self.delta_inactive and self._previous_summary: if self._previous_summary.inactive == 0: return self.delta_inactive * 100. return float(self.delta_inactive / float(self._previous_summary.inactive)) * 100. def _get_all_user_stubs(self): return { row['user_id']: UserActivityStub( user_id=row['user_id'], username=raw_username(row['username']), num_forms_submitted=row['total_num_forms'], is_performing=row['total_num_forms'] >= self.performance_threshold, previous_stub=None, next_stub=None, ) for row in self._user_stat_from_malt } def finalize(self): """ Before a summary is "finalized" certain fields can't be accessed. """ self._is_final = True @memoized def _get_all_user_stubs_with_extra_data(self): if not self._is_final: # intentionally fail-hard with developer-facing error raise Exception( "User stubs accessed before finalized. " "Please call finalize() before calling this method.") if self._previous_summary: previous_stubs = self._previous_summary._get_all_user_stubs() next_stubs = self._next_summary._get_all_user_stubs( ) if self._next_summary else {} user_stubs = self._get_all_user_stubs() ret = [] for user_stub in user_stubs.values(): ret.append( UserActivityStub( user_id=user_stub.user_id, username=user_stub.username, num_forms_submitted=user_stub.num_forms_submitted, is_performing=user_stub.is_performing, previous_stub=previous_stubs.get(user_stub.user_id), next_stub=next_stubs.get(user_stub.user_id), )) for missing_user_id in set(previous_stubs.keys()) - set( user_stubs.keys()): previous_stub = previous_stubs[missing_user_id] ret.append( UserActivityStub( user_id=previous_stub.user_id, username=previous_stub.username, num_forms_submitted=0, is_performing=False, previous_stub=previous_stub, next_stub=next_stubs.get(missing_user_id), )) return ret def get_unhealthy_users(self): """ Get a list of unhealthy users - defined as those who were "performing" last month but are not this month (though are still active). """ if self._previous_summary: unhealthy_users = filter( lambda stub: stub.is_active and not stub.is_performing, self._get_all_user_stubs_with_extra_data()) return sorted(unhealthy_users, key=lambda stub: stub.delta_forms) def get_dropouts(self): """ Get a list of dropout users - defined as those who were active last month but are not active this month """ if self._previous_summary: dropouts = filter(lambda stub: not stub.is_active, self._get_all_user_stubs_with_extra_data()) return sorted(dropouts, key=lambda stub: stub.delta_forms) def get_newly_performing(self): """ Get a list of "newly performing" users - defined as those who are "performing" this month after not performing last month. """ if self._previous_summary: dropouts = filter(lambda stub: stub.is_newly_performing, self._get_all_user_stubs_with_extra_data()) return sorted(dropouts, key=lambda stub: -stub.delta_forms)
class FuzzyProperties(jsonobject.JsonObject): case_type = jsonobject.StringProperty() properties = jsonobject.ListProperty(unicode)
class SecondaryTableSpec(jsonobject.JsonObject): data_source_id = jsonobject.StringProperty(required=True) join_column_primary = jsonobject.StringProperty(required=True) join_column_secondary = jsonobject.StringProperty(required=True) time_window_column = jsonobject.StringProperty() columns = jsonobject.ListProperty(SecondaryColumnSpec)
class AppInfo(jsonobject.JsonObject): id = jsonobject.StringProperty() names = jsonobject.StringProperty() langs = jsonobject.ListProperty(unicode)