class CouchConfig(JsonObject): user = StringProperty() password = StringProperty() host = StringProperty(required=True) headers = DefaultProperty() def __init__(self, obj=None, **kwargs): PREFIX = "CI" # Default to settings variables if 'user' not in obj: obj['user'] = getattr( settings, "{prefix}_{host}_USER".format( prefix=PREFIX, host=obj['host'].replace('.', '_').upper(), )) if 'password' not in obj: obj['password'] = getattr( settings, "{prefix}_{host}_PASSWORD".format( prefix=PREFIX, host=obj['host'].replace('.', '_').upper(), )) super(CouchConfig, self).__init__(obj, **kwargs) @property def uri(self): return u"https://{user}:{password}@{host}".format( user=self.user, password=self.password, host=self.host)
class CaseProperty(JsonObject): name = StringProperty() forms = ListProperty(CaseFormMeta) short_details = ListProperty(CaseDetailMeta) long_details = ListProperty(CaseDetailMeta) has_errors = BooleanProperty() description = StringProperty() def get_form(self, form_id): try: form = next(form for form in self.forms if form.form_id == form_id) except StopIteration: form = CaseFormMeta(form_id=form_id) self.forms.append(form) return form def add_load(self, form_id, question): form = self.get_form(form_id) form.load_questions.append(ConditionalFormQuestionResponse( question=question, condition=None )) def add_save(self, form_id, question, condition=None): form = self.get_form(form_id) form.save_questions.append(ConditionalFormQuestionResponse( question=question, condition=(condition if condition and condition.type == 'if' else None) )) def add_detail(self, type_, module_id, header, format): { "short": self.short_details, "long": self.long_details, }[type_].append(CaseDetailMeta(module_id=module_id, header=header, format=format))
class ReportFilter(JsonObject): """ This is a spec class that is just used for validation on a ReportConfiguration object. These get converted to FilterSpecs (below) by the FilterFactory. """ # todo: this class is silly and can likely be removed. type = StringProperty(required=True) slug = StringProperty(required=True) field = StringProperty(required=True) display = DefaultProperty() compare_as_string = BooleanProperty(default=False) _class_map = { 'quarter': QuarterFilterValue, 'date': DateFilterValue, 'numeric': NumericFilterValue, 'pre': PreFilterValue, 'choice_list': ChoiceListFilterValue, 'dynamic_choice_list': ChoiceListFilterValue, 'multi_field_dynamic_choice_list': MultiFieldChoiceListFilterValue, 'location_drilldown': LocationDrilldownFilterValue, } def create_filter_value(self, value): return self._class_map[self.type](self, value)
class EQAExpressionSpec(JsonObject): type = TypeProperty('eqa_expression') question_id = StringProperty() display_text = StringProperty() xmlns = StringProperty() def __call__(self, item, context=None): xforms_ids = CaseAccessors(item['domain']).get_case_xform_ids( item['_id']) forms = FormAccessors(item['domain']).get_forms(xforms_ids) f_forms = [f for f in forms if f.xmlns == self.xmlns] s_forms = sorted(f_forms, key=lambda x: x.received_on) if len(s_forms) >= 2: curr_form = s_forms[-1] prev_form = s_forms[-2] elif len(s_forms) == 1: curr_form = s_forms[-1] prev_form = None else: curr_form = None prev_form = None path_question = 'form/%s' % self.question_id curr_ques = get_val(curr_form, path_question, 99) prev_ques = get_val(prev_form, path_question, 99) return { 'question_id': self.question_id, 'display_text': self.display_text, 'current_submission': get_yes_no(curr_ques), 'previous_submission': get_yes_no(prev_ques), 'status': STATUSES.get((curr_ques, prev_ques)) }
class Product(JsonObject): name = StringProperty() units = StringProperty() sms_code = StringProperty() description = StringProperty() is_active = BooleanProperty() program = ObjectProperty(item_type=Program)
class EQAPercentExpression(JsonObject): type = TypeProperty('eqa_percent_expression') question_id = StringProperty() display_text = StringProperty() xmlns = StringProperty() def __call__(self, item, context=None): curr_form, prev_form = get_two_last_forms(item, self.xmlns) path_question = 'form/%s' % self.question_id curr_ques = get_val(curr_form, path_question, -1, float) prev_ques = get_val(prev_form, path_question, -1, float) if curr_ques == -1 or prev_ques == -1: status = "N/A" elif curr_ques > prev_ques: status = "Improved" elif curr_ques < prev_ques: status = "Declined" else: status = "Satisfactory" return { 'question_id': self.question_id, 'display_text': self.display_text, 'current_submission': "%.2f%%" % curr_ques if curr_ques != -1 else "N/A", 'previous_submission': "%.2f%%" % prev_ques if prev_ques != -1 else "N/A", 'status': status }
class ArrayAggLastValueReportColumn(ReportColumn): type = TypeProperty('array_agg_last_value') field = StringProperty(required=True) order_by_col = StringProperty(required=False) _agg_column_type = ArrayAggColumn def get_column_config(self, data_source_config, lang): def _last_value(array): return array[-1] if array else None return ColumnConfig(columns=[ DatabaseColumn( header=self.get_header(lang), agg_column=self._agg_column_type( key=self.field, order_by_col=self.order_by_col, alias=self.column_id, ), format_fn=_last_value, data_slug=self.column_id, help_text=self.description, visible=self.visible, sortable=False, ) ])
class CaseTypeMeta(JsonObject): name = StringProperty(required=True) relationships = DictProperty() # relationship name -> case type properties = ListProperty(CaseProperty) # property -> CaseProperty opened_by = DictProperty(ConditionList) # form_ids -> [FormActionCondition, ...] closed_by = DictProperty(ConditionList) # form_ids -> [FormActionCondition, ...] error = StringProperty() has_errors = BooleanProperty() def get_property(self, name, allow_parent=False): if not allow_parent: assert '/' not in name, "Add parent properties to the correct case type" try: prop = next(prop for prop in self.properties if prop.name == name) except StopIteration: prop = CaseProperty(name=name) self.properties.append(prop) return prop def add_opener(self, form_id, condition): openers = self.opened_by.get(form_id, ConditionList()) if condition.type == 'if': # only add optional conditions openers.conditions.append(condition) self.opened_by[form_id] = openers def add_closer(self, form_id, condition): closers = self.closed_by.get(form_id, ConditionList()) if condition.type == 'if': # only add optional conditions closers.conditions.append(condition) self.closed_by[form_id] = closers
class SumWhenTemplateSpec(JsonObject): type = StringProperty(required=True) expression = StringProperty(required=True) binds = ListProperty() then = IntegerProperty() def bind_count(self): return len(re.sub(r'[^?]', '', self.expression))
class StockTransaction(JsonObject): beginning_balance = DecimalProperty() date = StringProperty() ending_balance = DecimalProperty() product = StringProperty() quantity = DecimalProperty() report_type = StringProperty() supply_point = IntegerProperty()
class ChartSpec(JsonObject): type = StringProperty(required=True) title = StringProperty() chart_id = StringProperty() @classmethod def wrap(cls, obj): if obj.get('chart_id') is None: obj['chart_id'] = (obj.get('title') or '') + str(hash(json.dumps(sorted(obj.items())))) return super(ChartSpec, cls).wrap(obj)
class GraphDisplayColumn(JsonObject): column_id = StringProperty(required=True) display = StringProperty(required=True) @classmethod def wrap(cls, obj): # automap column_id to display if display isn't set if isinstance(obj, dict) and 'column_id' in obj and 'display' not in obj: obj['display'] = obj['column_id'] return super(GraphDisplayColumn, cls).wrap(obj)
class ReportsForm(JsonObject): time = DateTimeProperty() completion_time = DateTimeProperty() start_time = DateTimeProperty() duration = IntegerProperty() submission_time = DateTimeProperty() xmlns = StringProperty() app_id = StringProperty() user_id = StringProperty() username = StringProperty()
class ResumableIteratorState(JsonObject): doc_type = "ResumableIteratorState" _id = StringProperty() name = StringProperty() timestamp = DateTimeProperty() args = ListProperty() kwargs = DictProperty() retry = DictProperty() progress = DictProperty() complete = BooleanProperty(default=False)
class Location(JsonObject): id = IntegerProperty() name = StringProperty() type = StringProperty() parent_id = IntegerProperty() latitude = StringProperty() longitude = StringProperty() code = StringProperty() groups = ListProperty() historical_groups = DictProperty()
class EQAActionItemSpec(JsonObject): type = TypeProperty('cqi_action_item') xmlns = StringProperty() section = StringProperty() question_id = StringProperty() def __call__(self, item, context=None): xforms_ids = CommCareCase.objects.get_case_xform_ids(item['_id']) forms = XFormInstance.objects.get_forms(xforms_ids, item['domain']) f_forms = [f for f in forms if f.xmlns == self.xmlns] s_forms = sorted(f_forms, key=lambda x: x.received_on) if len(s_forms) > 0: latest_form = s_forms[-1] else: latest_form = None path_to_action_plan = 'form/action_plan/%s/action_plan' % self.section if latest_form: action_plans = latest_form.get_data(path_to_action_plan) if action_plans: action_plan_for_question = None for action_plan in action_plans: if action_plan.get('incorrect_questions', '') == self.question_id: action_plan_for_question = action_plan break if action_plan_for_question: incorrect_question = action_plan_for_question.get('incorrect_questions', '') responsible = ', '.join( [ item.get(x.strip(), '---') for x in action_plan_for_question.get('action_plan_input', {}).get('responsible', '').split(',') ] ) support = ', '.join( [ item.get(x.strip(), '---') for x in action_plan_for_question.get('action_plan_input', {}).get('support', '').split(',') ] ) application = Application.get(latest_form.app_id) form = application.get_forms_by_xmlns(self.xmlns)[0] question_list = application.get_questions(self.xmlns) questions = {x['value']: x for x in question_list} return { 'form_name': form.name['en'], 'section': self.section, 'timeEnd': latest_form.metadata.timeEnd, 'gap': questions.get('data/code_to_text/%s' % incorrect_question, {}).get('label', '---'), 'intervention_action': action_plan_for_question.get('intervention_action', '---'), 'responsible': responsible, 'support': support, 'deadline': action_plan_for_question.get('DEADLINE', '---'), 'notes': action_plan_for_question.get('notes', '---'), }
class ResumableIteratorState(JsonObject): doc_type = "ResumableIteratorState" _id = StringProperty() name = StringProperty() timestamp = DateTimeProperty() args = ListProperty() kwargs = DictProperty() progress = DictProperty() def is_resume(self): return bool(getattr(self, '_rev', None))
class Location(JsonObject): id = IntegerProperty() name = StringProperty() type = StringProperty() parent_id = IntegerProperty() latitude = StringProperty() longitude = StringProperty() code = StringProperty() groups = ListProperty() supervised_by = IntegerProperty() supply_points = ListProperty(item_type=SupplyPoint) is_active = BooleanProperty()
class MigrationConfig(JsonObject): from_db_postfix = StringProperty() to_db_postfix = StringProperty() doc_types = ListProperty(required=True) couch_views = ListProperty() @property def source_db(self): return get_db(self.from_db_postfix) @property def dest_db(self): return get_db(self.to_db_postfix)
class FilterSpec(JsonObject): """ This is the spec for a report filter - a thing that should show up as a UI filter element in a report (like a date picker or a select list). """ type = StringProperty(required=True, choices=['date', 'numeric', 'pre', 'choice_list', 'dynamic_choice_list']) # this shows up as the ID in the filter HTML. slug = StringProperty(required=True) field = StringProperty(required=True) # this is the actual column that is queried display = DefaultProperty() datatype = DataTypeProperty(default='string') def get_display(self): return self.display or self.slug
class SupplyPoint(JsonObject): id = IntegerProperty() active = BooleanProperty() code = StringProperty() groups = ListProperty() last_reported = StringProperty() name = StringProperty() primary_reporter = IntegerProperty() supervised_by = IntegerProperty() supplied_by = IntegerProperty() type = StringProperty() location_id = IntegerProperty() products = ListProperty() incharges = ListProperty()
class ReportFilter(JsonObject): type = StringProperty(required=True) slug = StringProperty(required=True) field = StringProperty(required=True) display = DefaultProperty() compare_as_string = BooleanProperty(default=False) def create_filter_value(self, value): return { 'date': DateFilterValue, 'numeric': NumericFilterValue, 'choice_list': ChoiceListFilterValue, 'dynamic_choice_list': ChoiceListFilterValue, }[self.type](self, value)
class FieldColumn(ReportColumn): type = TypeProperty('field') field = StringProperty(required=True) aggregation = StringProperty( choices=SQLAGG_COLUMN_MAP.keys(), required=True, ) format = StringProperty(default='default', choices=[ 'default', 'percent_of_total', ]) sortable = BooleanProperty(default=False) @classmethod def wrap(cls, obj): # lazy migrations for legacy data. # todo: remove once all reports are on new format # 1. set column_id to alias, or field if no alias found _add_column_id_if_missing(obj) # 2. if aggregation='expand' convert to ExpandedColumn if obj.get('aggregation') == 'expand': del obj['aggregation'] obj['type'] = 'expanded' return ExpandedColumn.wrap(obj) return super(FieldColumn, cls).wrap(obj) def format_data(self, data): if self.format == 'percent_of_total': column_name = self.column_id total = sum(row[column_name] for row in data) for row in data: row[column_name] = '{:.0%}'.format( float(row[column_name]) / total ) def get_sql_column_config(self, data_source_config, lang): return SqlColumnConfig(columns=[ DatabaseColumn( header=self.get_header(lang), agg_column=SQLAGG_COLUMN_MAP[self.aggregation](self.field, alias=self.column_id), sortable=self.sortable, data_slug=self.column_id, format_fn=self.get_format_fn(), help_text=self.description ) ]) def get_query_column_ids(self): return [self.column_id]
class ReportColumn(JsonObject): type = StringProperty(required=True) column_id = StringProperty(required=True) display = DefaultProperty() description = StringProperty() transform = DictProperty() calculate_total = BooleanProperty(default=False) @classmethod def wrap(cls, obj): if 'display' not in obj and 'column_id' in obj: obj['display'] = obj['column_id'] return super(ReportColumn, cls).wrap(obj) def format_data(self, data): """ Subclasses can apply formatting to the entire dataset. """ pass def get_sql_column_config(self, data_source_config, lang): raise NotImplementedError('subclasses must override this') def get_format_fn(self): """ A function that gets applied to the data just in time before the report is rendered. """ if self.transform: return TransformFactory.get_transform(self.transform).get_transform_function() return None def get_query_column_ids(self): """ Gets column IDs associated with a query. These could be different from the normal column_ids if the same column ends up in multiple columns in the query (e.g. an aggregate date splitting into year and month) """ raise InvalidQueryColumn(_("You can't query on columns of type {}".format(self.type))) def get_header(self, lang): return localize(self.display, lang) def get_column_ids(self): """ Used as an abstraction layer for columns that can contain more than one data column (for example, PercentageColumns). """ return [self.column_id]
class BaseReportColumn(JsonObject): type = StringProperty(required=True) column_id = StringProperty(required=True) display = DefaultProperty() description = StringProperty() visible = BooleanProperty(default=True) @classmethod def wrap(cls, obj): if 'display' not in obj and 'column_id' in obj: obj['display'] = obj['column_id'] return super(BaseReportColumn, cls).wrap(obj) def get_header(self, lang): return localize(self.display, lang) def get_column_ids(self): """ Used as an abstraction layer for columns that can contain more than one data column (for example, PercentageColumns). """ return [self.column_id] def get_column_config(self, data_source_config, lang): raise NotImplementedError('subclasses must override this') def aggregations(self, data_source_config, lang): """ Returns a list of aggregations to be used in an ES query """ raise NotImplementedError('subclasses must override this') def get_es_data(self, row, data_source_config, lang, from_aggregation=True): """ Returns a dictionary of the data of this column from an ES query """ raise NotImplementedError('subclasses must override this') def get_fields(self, data_source_config=None, lang=None): """ Get database fields associated with this column. Could be one, or more if a column is a function of two values in the DB (e.g. PercentageColumn) """ raise NotImplementedError('subclasses must override this')
def TypeProperty(value): """ Shortcut for making a required property and restricting it to a single specified value. This adds additional validation that the objects are being wrapped as expected according to the type. """ return StringProperty(required=True, choices=[value])
class LocationColumn(ReportColumn): type = TypeProperty('location') field = StringProperty(required=True) sortable = BooleanProperty(default=False) def format_data(self, data): column_name = self.column_id for row in data: try: row[column_name] = '{g.latitude} {g.longitude} {g.altitude} {g.accuracy}'.format( g=GeoPointProperty().wrap(row[column_name])) except BadValueError: row[column_name] = u'{} ({})'.format(row[column_name], _('Invalid Location')) def get_column_config(self, data_source_config, lang): return ColumnConfig(columns=[ DatabaseColumn(header=self.get_header(lang), agg_column=SimpleColumn(self.field, alias=self.column_id), sortable=self.sortable, data_slug=self.column_id, format_fn=self.get_format_fn(), help_text=self.description) ])
class GetLastCasePropertyUpdateSpec(JsonObject): type = TypeProperty('icds_get_last_case_property_update') case_property = StringProperty(required=True) case_id_expression = DefaultProperty(required=False) start_date = DefaultProperty(required=False) end_date = DefaultProperty(required=False) filter = DefaultProperty(required=False)
class PillowResetConfig(JsonObject): seq = StringProperty(required=True) pillow_names = ListProperty(required=True) @property def pillows(self): return [get_pillow_by_name(name) for name in self.pillow_names]
class SortItemsExpressionSpec(NoPropertyTypeCoercionMixIn, JsonObject): ASC, DESC = "ASC", "DESC" type = TypeProperty('sort_items') items_expression = DefaultProperty(required=True) sort_expression = DictProperty(required=True) order = StringProperty(choices=[ASC, DESC], default=ASC) def configure(self, items_expression, sort_expression): self._items_expression = items_expression self._sort_expression = sort_expression def __call__(self, doc, context=None): items = _evaluate_items_expression(self._items_expression, doc, context) try: return sorted(items, key=lambda i: self._sort_expression(i, context), reverse=True if self.order == self.DESC else False) except TypeError: return [] def __str__(self): return "sort:\n{items}\n{order} on:\n{sort}".format( items=add_tabbed_text(str(self._items_expression)), order=self.order, sort=add_tabbed_text(str(self._sort_expression)))