class RelatedDocExpressionSpec(JsonObject): type = TypeProperty('related_doc') related_doc_type = StringProperty() doc_id_expression = DictProperty(required=True) value_expression = DictProperty(required=True) db_lookup = lambda self, type: get_db() def configure(self, related_doc_type, doc_id_expression, value_expression): self._related_doc_type = related_doc_type self._doc_id_expression = doc_id_expression self._value_expression = value_expression # used in caching self._vary_on = json.dumps(self.value_expression, sort_keys=True) def __call__(self, item, context=None): doc_id = self._doc_id_expression(item, context) if doc_id: return self.get_value(doc_id, context) @quickcache(['self._vary_on', 'doc_id']) def get_value(self, doc_id, context): try: doc = self.db_lookup(self.related_doc_type).get(doc_id) # ensure no cross-domain lookups of different documents assert context.root_doc['domain'] if context.root_doc['domain'] != doc.get('domain'): return None # explicitly use a new evaluation context since this is a new document return self._value_expression(doc, EvaluationContext(doc, 0)) except ResourceNotFound: return None
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 ResumableIteratorState(JsonObject): doc_type = "ResumableIteratorState" _id = StringProperty() name = StringProperty() timestamp = DateTimeProperty() args = ListProperty() kwargs = DictProperty() retry = DictProperty() progress = DictProperty() complete = BooleanProperty(default=False)
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 NestedExpressionSpec(JsonObject): type = TypeProperty('nested') argument_expression = DictProperty(required=True) value_expression = DictProperty(required=True) def configure(self, argument_expression, value_expression): self._argument_expression = argument_expression self._value_expression = value_expression def __call__(self, item, context=None): argument = self._argument_expression(item, context) return self._value_expression(argument, context)
class DynamicChoiceListFilterSpec(FilterSpec): type = TypeProperty('dynamic_choice_list') show_all = BooleanProperty(default=True) datatype = DataTypeProperty(default='string') choice_provider = DictProperty() ancestor_expression = DictProperty(default={}, required=False) def get_choice_provider_spec(self): return self.choice_provider or {'type': DATA_SOURCE_COLUMN} @property def choices(self): return []
class RelatedDocExpressionSpec(JsonObject): type = TypeProperty('related_doc') related_doc_type = StringProperty() doc_id_expression = DictProperty(required=True) value_expression = DictProperty(required=True) def configure(self, doc_id_expression, value_expression): non_couch_doc_types = (LOCATION_DOC_TYPE, ) if (self.related_doc_type not in non_couch_doc_types and get_db_by_doc_type(self.related_doc_type) is None): raise BadSpecError( 'Cannot determine database for document type {}!'.format( self.related_doc_type)) self._doc_id_expression = doc_id_expression self._value_expression = value_expression def __call__(self, item, context=None): doc_id = self._doc_id_expression(item, context) if doc_id: return self.get_value(doc_id, context) @staticmethod @ucr_context_cache(vary_on=( 'related_doc_type', 'doc_id', )) def _get_document(related_doc_type, doc_id, context): document_store = get_document_store_for_doc_type( context.root_doc['domain'], related_doc_type, load_source="related_doc_expression") try: doc = document_store.get_document(doc_id) except DocumentNotFoundError: return None if context.root_doc['domain'] != doc.get('domain'): return None return doc def get_value(self, doc_id, context): assert context.root_doc['domain'] doc = self._get_document(self.related_doc_type, doc_id, context) # explicitly use a new evaluation context since this is a new document return self._value_expression(doc, EvaluationContext(doc, 0)) def __str__(self): return "{}[{}]/{}".format(self.related_doc_type, str(self._doc_id_expression), str(self._value_expression))
class IntegerBucketsColumn(_CaseExpressionColumn): """Used for grouping by SQL conditionals""" type = TypeProperty('integer_buckets') _agg_column_type = ConditionalAggregation field = StringProperty(required=True) ranges = DictProperty() def get_whens(self): whens = [] for value, bounds in self.ranges.items(): if len(bounds) != 2: raise BadSpecError( 'Range must contain 2 items, contains {}'.format( len(bounds))) try: bounds = [int(b) for b in bounds] except ValueError: raise BadSpecError('Invalid range: [{}, {}]'.format( bounds[0], bounds[1])) whens.append( [self._base_expression(bounds), bindparam(None, value)]) return whens def _base_expression(self, bounds): return "{} between {} and {}".format(self.field, bounds[0], bounds[1])
class ConditionalExpressionSpec(JsonObject): type = TypeProperty('conditional') test = DictProperty(required=True) expression_if_true = DictProperty(required=True) expression_if_false = DictProperty(required=True) def configure(self, test_function, true_expression, false_expression): self._test_function = test_function self._true_expression = true_expression self._false_expression = false_expression def __call__(self, item, context=None): if self._test_function(item, context): return self._true_expression(item, context) else: return self._false_expression(item, context)
class DictExpressionSpec(JsonObject): type = TypeProperty('dict') properties = DictProperty(required=True) def configure(self, compiled_properties): for key in compiled_properties: if not isinstance(key, (six.text_type, bytes)): raise BadSpecError( "Properties in a dict expression must be strings!") if six.PY3: soft_assert_type_text(key) self._compiled_properties = compiled_properties def __call__(self, item, context=None): ret = {} for property_name, expression in self._compiled_properties.items(): ret[property_name] = expression(item, context) return ret def __str__(self): dict_text = ", ".join([ "{}:{}".format(name, str(exp)) for name, exp in self._compiled_properties.items() ]) return "({})".format(dict_text)
class IteratorExpressionSpec(NoPropertyTypeCoercionMixIn, JsonObject): type = TypeProperty('iterator') expressions = ListProperty(required=True) # an optional filter to test the values on - if they don't match they won't be included in the iteration test = DictProperty() def configure(self, expressions, test): self._expression_fns = expressions if test: self._test = test else: # if not defined then all values should be returned self._test = lambda *args, **kwargs: True def __call__(self, item, context=None): values = [] for expression in self._expression_fns: value = expression(item, context) if self._test(value): values.append(value) return values def __str__(self): expressions_text = ", ".join(str(e) for e in self._expression_fns) return "iterate on [{}] if {}".format(expressions_text, str(self._test))
class ArrayIndexExpressionSpec(NoPropertyTypeCoercionMixIn, JsonObject): type = TypeProperty('array_index') array_expression = DictProperty(required=True) index_expression = DefaultProperty(required=True) def configure(self, array_expression, index_expression): self._array_expression = array_expression self._index_expression = index_expression def __call__(self, item, context=None): array_value = self._array_expression(item, context) if not isinstance(array_value, list): return None index_value = self._index_expression(item, context) if not isinstance(index_value, int): return None try: return array_value[index_value] except IndexError: return None def __str__(self): return "{}[{}]".format(str(self._array_expression), str(self._index_expression))
class CoalesceExpressionSpec(JsonObject): type = TypeProperty('coalesce') expression = DictProperty(required=True) default_expression = DictProperty(required=True) def configure(self, expression, default_expression): self._expression = expression self._default_expression = default_expression def __call__(self, item, context=None): expression_value = self._expression(item, context) default_value = self._default_expression(item, context) if expression_value is None or expression_value == '': return default_value else: return expression_value
class _GroupsExpressionSpec(JsonObject): user_id_expression = DictProperty(required=True) def configure(self, user_id_expression): self._user_id_expression = user_id_expression def __call__(self, item, context=None): user_id = self._user_id_expression(item, context) if not user_id: return [] assert context.root_doc['domain'] return self._get_groups(user_id, context) def _get_groups(self, user_id, context): domain = context.root_doc['domain'] cache_key = (self.__class__.__name__, domain, user_id) if context.get_cache_value(cache_key) is not None: return context.get_cache_value(cache_key) user = CommCareUser.get_by_user_id(user_id, domain) if not user: return [] groups = self._get_groups_from_user(user) groups = [g.to_json() for g in groups] context.set_cache_value(cache_key, groups) return groups def _get_groups_from_user(self, user): raise NotImplementedError
class LocationTypeSpec(JsonObject): type = TypeProperty('location_type_name') location_id_expression = DictProperty(required=True) def configure(self, location_id_expression): self._location_id_expression = location_id_expression def __call__(self, item, context=None): doc_id = self._location_id_expression(item, context) if not doc_id: return None assert context.root_doc['domain'] return self._get_location_type(doc_id, context.root_doc['domain']) @staticmethod @quickcache(['location_id', 'domain'], timeout=600) def _get_location_type(location_id, domain): sql_location = SQLLocation.objects.filter( domain=domain, location_id=location_id ) if sql_location: return sql_location[0].location_type.name else: return None
class ReportColumn(BaseReportColumn): transform = DictProperty() calculate_total = BooleanProperty(default=False) def format_data(self, data): """ Subclasses can apply formatting to the entire dataset. """ pass 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)))
class EvalExpressionSpec(JsonObject): type = TypeProperty('evaluator') statement = StringProperty(required=True) context_variables = DictProperty() datatype = DataTypeProperty(required=False) def configure(self, context_variables): self._context_variables = context_variables def __call__(self, item, context=None): var_dict = self.get_variables(item, context) try: untransformed_value = eval_statements(self.statement, var_dict) return transform_from_datatype(self.datatype)(untransformed_value) except (InvalidExpression, SyntaxError, TypeError, ZeroDivisionError): return None def get_variables(self, item, context): var_dict = { slug: variable_expression(item, context) for slug, variable_expression in self._context_variables.items() } return var_dict def __str__(self): value = self.statement for name, exp in self._context_variables.items(): value.replace(name, str(exp)) if self.datatype: value = "({}){}".format(self.datatype, value) return value
class SplitStringExpressionSpec(JsonObject): type = TypeProperty('split_string') string_expression = DictProperty(required=True) index_expression = DefaultProperty(required=False) delimiter = StringProperty(required=False) def configure(self, string_expression, index_expression): self._string_expression = string_expression self._index_expression = index_expression def __call__(self, item, context=None): string_value = self._string_expression(item, context) if not isinstance(string_value, six.string_types): return None soft_assert_type_text(string_value) index_value = None if self.index_expression is not None: index_value = self._index_expression(item, context) if not isinstance(index_value, int): return None try: split = string_value.split(self.delimiter) return split[index_value] if index_value is not None else split except IndexError: return None def __str__(self): split_text = "split {}".format(str(self._string_expression)) if self.delimiter: split_text += " on '{}'".format(self.delimiter) return "(split {})[{}]".format(str(split_text), str(self._index_expression))
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)))
class ExpressionIndicatorSpec(IndicatorSpecBase): type = TypeProperty('expression') datatype = DataTypeProperty(required=True) is_nullable = BooleanProperty(default=True) is_primary_key = BooleanProperty(default=False) create_index = BooleanProperty(default=False) expression = DefaultProperty(required=True) transform = DictProperty(required=False) def parsed_expression(self, context): from corehq.apps.userreports.expressions.factory import ExpressionFactory expression = ExpressionFactory.from_spec(self.expression, context) datatype_transform = transform_for_datatype(self.datatype) if self.transform: generic_transform = TransformFactory.get_transform( self.transform).get_transform_function() inner_getter = TransformedGetter(expression, generic_transform) else: inner_getter = expression return TransformedGetter(inner_getter, datatype_transform) def readable_output(self, context): from corehq.apps.userreports.expressions.factory import ExpressionFactory expression_object = ExpressionFactory.from_spec( self.expression, context) return str(expression_object)
class _GroupsExpressionSpec(JsonObject): user_id_expression = DictProperty(required=True) def configure(self, user_id_expression): self._user_id_expression = user_id_expression def __call__(self, item, context=None): user_id = self._user_id_expression(item, context) if not user_id: return [] assert context.root_doc['domain'] return self._get_groups(user_id, context) @ucr_context_cache(vary_on=('user_id', )) def _get_groups(self, user_id, context): domain = context.root_doc['domain'] user = CommCareUser.get_by_user_id(user_id, domain) if not user: return [] groups = self._get_groups_from_user(user) return [g.to_json() for g in groups] def _get_groups_from_user(self, user): raise NotImplementedError
class ConditionalExpressionSpec(JsonObject): """ This expression returns ``"legal" if doc["age"] > 21 else "underage"``: .. code:: json { "type": "conditional", "test": { "operator": "gt", "expression": { "type": "property_name", "property_name": "age", "datatype": "integer" }, "type": "boolean_expression", "property_value": 21 }, "expression_if_true": { "type": "constant", "constant": "legal" }, "expression_if_false": { "type": "constant", "constant": "underage" } } Note that this expression contains other expressions inside it! This is why expressions are powerful. (It also contains a filter, but we haven't covered those yet - if you find the ``"test"`` section confusing, keep reading...) Note also that it's important to make sure that you are comparing values of the same type. In this example, the expression that retrieves the age property from the document also casts the value to an integer. If this datatype is not specified, the expression will compare a string to the ``21`` value, which will not produce the expected results! """ type = TypeProperty('conditional') test = DictProperty(required=True) expression_if_true = DefaultProperty(required=True) expression_if_false = DefaultProperty(required=True) def configure(self, test_function, true_expression, false_expression): self._test_function = test_function self._true_expression = true_expression self._false_expression = false_expression def __call__(self, item, context=None): if self._test_function(item, context): return self._true_expression(item, context) else: return self._false_expression(item, context) def __str__(self): return "if {test}:\n{true}\nelse:\n{false}".format( test=str(self._test_function), true=add_tabbed_text(str(self._true_expression)), false=add_tabbed_text(str(self._false_expression)))
class IndexedCaseExpressionSpec(JsonObject): type = TypeProperty('indexed_case') case_expression = DictProperty(required=True) index = StringProperty(required=False) def configure(self, case_expression, context): self._case_expression = case_expression index = self.index or 'parent' spec = { 'type': 'related_doc', 'related_doc_type': 'CommCareCase', 'doc_id_expression': { 'type': 'nested', 'argument_expression': self.case_expression, 'value_expression': { 'type': 'nested', 'argument_expression': { 'type': 'array_index', 'array_expression': { 'type': 'filter_items', 'items_expression': { 'datatype': 'array', 'type': 'property_name', 'property_name': 'indices' }, 'filter_expression': { 'type': 'boolean_expression', 'operator': 'eq', 'property_value': index, 'expression': { 'type': 'property_name', 'property_name': 'identifier' } } }, 'index_expression': { 'type': 'constant', 'constant': 0 } }, 'value_expression': { 'type': 'property_name', 'property_name': 'referenced_id' } } }, 'value_expression': { 'type': 'identity' } } self._expression = ExpressionFactory.from_spec(spec, context) def __call__(self, item, context=None): return self._expression(item, context) def __str__(self): return "{case}/{index}".format(case=str(self._case_expression), index=self.index or "parent")
class BooleanIndicatorSpec(IndicatorSpecBase): type = TypeProperty('boolean') filter = DictProperty(required=True) def readable_output(self, context): from corehq.apps.userreports.filters.factory import FilterFactory filter_object = FilterFactory.from_spec(self.filter, context) return str(filter_object)
class FilterItemsExpressionSpec(NoPropertyTypeCoercionMixIn, JsonObject): """ ``filter_items`` performs filtering on given list and returns a new list. If the boolean expression specified by ``filter_expression`` evaluates to a ``True`` value, the item is included in the new list and if not, is not included in the new list. ``items_expression`` can be any valid expression that returns a list. If this doesn't evaluate to a list an empty list is returned. It may be necessary to specify a ``datatype`` of ``array`` if the expression could return a single element. ``filter_expression`` can be any valid boolean expression relative to the items in above list. .. code:: json { "type": "filter_items", "items_expression": { "datatype": "array", "type": "property_name", "property_name": "family_repeat" }, "filter_expression": { "type": "boolean_expression", "expression": { "type": "property_name", "property_name": "gender" }, "operator": "eq", "property_value": "female" } } """ type = TypeProperty('filter_items') items_expression = DefaultProperty(required=True) filter_expression = DictProperty(required=True) def configure(self, items_expression, filter_expression): self._items_expression = items_expression self._filter_expression = filter_expression def __call__(self, doc, context=None): items = _evaluate_items_expression(self._items_expression, doc, context) values = [] for item in items: if self._filter_expression(item, context): values.append(item) return values def __str__(self): return "filter:\n{items}\non:\n{filter}\n".format( items=add_tabbed_text(str(self._items_expression)), filter=add_tabbed_text(str(self._filter_expression)))
class LedgerBalancesIndicatorSpec(IndicatorSpecBase): type = TypeProperty('ledger_balances') product_codes = ListProperty(required=True) ledger_section = StringProperty(required=True) case_id_expression = DictProperty(required=True) def get_case_id_expression(self): from corehq.apps.userreports.expressions.factory import ExpressionFactory return ExpressionFactory.from_spec(self.case_id_expression)
class SplitStringExpressionSpec(JsonObject): """ This expression returns ``(doc["foo bar"]).split(' ')[0]``: .. code:: json { "type": "split_string", "string_expression": { "type": "property_name", "property_name": "multiple_value_string" }, "index_expression": { "type": "constant", "constant": 0 }, "delimiter": "," } The delimiter is optional and is defaulted to a space. It will return nothing if the string_expression is not a string, or if the index isn't a number or the indexed item doesn't exist. The index_expression is also optional. Without it, the expression will return the list of elements. """ type = TypeProperty('split_string') string_expression = DictProperty(required=True) index_expression = DefaultProperty(required=False) delimiter = StringProperty(required=False) def configure(self, string_expression, index_expression): self._string_expression = string_expression self._index_expression = index_expression def __call__(self, item, context=None): string_value = self._string_expression(item, context) if not isinstance(string_value, str): return None index_value = None if self.index_expression is not None: index_value = self._index_expression(item, context) if not isinstance(index_value, int): return None try: split = string_value.split(self.delimiter) return split[index_value] if index_value is not None else split except IndexError: return None def __str__(self): split_text = "split {}".format(str(self._string_expression)) if self.delimiter: split_text += " on '{}'".format(self.delimiter) return "(split {})[{}]".format(str(split_text), str(self._index_expression))
class BooleanExpressionFilterSpec(BaseFilterSpec): type = TypeProperty('boolean_expression') operator = StringProperty(choices=OPERATORS.keys(), required=True) property_value = DefaultProperty() expression = DictProperty(required=True) @classmethod def wrap(cls, obj): _assert_prop_in_obj('property_value', obj) return super(BooleanExpressionFilterSpec, cls).wrap(obj)
class RelatedDocExpressionSpec(JsonObject): type = TypeProperty('related_doc') related_doc_type = StringProperty() doc_id_expression = DictProperty(required=True) value_expression = DictProperty(required=True) def configure(self, doc_id_expression, value_expression): non_couch_doc_types = (LOCATION_DOC_TYPE, ) if (self.related_doc_type not in non_couch_doc_types and get_db_by_doc_type(self.related_doc_type) is None): raise BadSpecError( u'Cannot determine database for document type {}!'.format( self.related_doc_type)) self._doc_id_expression = doc_id_expression self._value_expression = value_expression def __call__(self, item, context=None): doc_id = self._doc_id_expression(item, context) if doc_id: return self.get_value(doc_id, context) def _vary_on(self, doc_id, context): # For caching. Gets called with the same params as get_value. return [ context.root_doc['domain'], doc_id, json.dumps(self.value_expression, sort_keys=True) ] @quickcache(_vary_on) def get_value(self, doc_id, context): try: assert context.root_doc['domain'] document_store = get_document_store(context.root_doc['domain'], self.related_doc_type) doc = document_store.get_document(doc_id) # ensure no cross-domain lookups of different documents if context.root_doc['domain'] != doc.get('domain'): return None # explicitly use a new evaluation context since this is a new document return self._value_expression(doc, EvaluationContext(doc, 0)) except DocumentNotFoundError: return None
class FormQuestion(JsonObject): label = StringProperty() translations = DictProperty(exclude_if_none=True) tag = StringProperty() type = StringProperty(choices=list(VELLUM_TYPES)) value = StringProperty() repeat = StringProperty() group = StringProperty() options = ListProperty(FormQuestionOption) calculate = StringProperty() relevant = StringProperty() required = BooleanProperty() comment = StringProperty() setvalue = StringProperty() data_source = DictProperty(exclude_if_none=True) @property def icon(self): try: return VELLUM_TYPES[self.type]['icon'] except KeyError: return 'fa fa-question-circle' @property def relative_value(self): if self.group: prefix = self.group + '/' if self.value.startswith(prefix): return self.value[len(prefix):] return '/'.join(self.value.split('/')[2:]) @property def option_values(self): return [o.value for o in self.options] @property def editable(self): if not self.type: return True vtype = VELLUM_TYPES[self.type] if 'editable' not in vtype: return False return vtype['editable']