Exemple #1
0
class DefaultConsumption(SyncCouchToSQLMixin, CachedCouchDocumentMixin,
                         Document):
    """
    Model for setting the default consumption value of an entity
    """
    type = StringProperty(
    )  # 'domain', 'product', 'supply-point-type', 'supply-point'
    domain = StringProperty()
    product_id = StringProperty()
    supply_point_type = StringProperty()
    supply_point_id = StringProperty()
    default_consumption = DecimalProperty()

    @classmethod
    def _migration_get_fields(cls):
        return [
            "type",
            "domain",
            "product_id",
            "supply_point_type",
            "supply_point_id",
            "default_consumption",
        ]

    @classmethod
    def _migration_get_sql_model_class(cls):
        return SQLDefaultConsumption
Exemple #2
0
class PropertyWeight(DocumentSchema):
    case_property = StringProperty()
    weight = DecimalProperty()
    match_type = StringProperty(required=False,
                                choices=MATCH_TYPES,
                                default=MATCH_TYPE_DEFAULT)
    match_params = ListProperty(required=False)
Exemple #3
0
class DefaultConsumption(CachedCouchDocumentMixin, Document):
    """
    Model for setting the default consumption value of an entity
    """
    type = StringProperty()  # 'domain', 'product', 'supply-point-type', 'supply-point'
    domain = StringProperty()
    product_id = StringProperty()
    supply_point_type = StringProperty()
    supply_point_id = StringProperty()
    default_consumption = DecimalProperty()

    @classmethod
    def get_domain_default(cls, domain):
        return cls._by_index_key([domain, None, None, None])

    @classmethod
    def get_product_default(cls, domain, product_id):
        return cls._by_index_key([domain, product_id, None, None])

    @classmethod
    def get_supply_point_default(cls, domain, product_id, supply_point_id):
        return cls._by_index_key([domain, product_id, {}, supply_point_id])

    @classmethod
    def _by_index_key(cls, key):
        return cls.view('consumption/consumption_index',
            key=key,
            reduce=False,
            include_docs=True,
        ).one()
Exemple #4
0
class InternalProperties(DocumentSchema, UpdatableSchema):
    """
    Project properties that should only be visible/editable by superusers
    """
    sf_contract_id = StringProperty()
    sf_account_id = StringProperty()
    commcare_edition = StringProperty(
        choices=['', "plus", "community", "standard", "pro", "advanced", "enterprise"],
        default="community"
    )
    initiative = StringListProperty()
    workshop_region = StringProperty()
    project_state = StringProperty(choices=["", "POC", "transition", "at-scale"], default="")
    self_started = BooleanProperty(default=True)
    area = StringProperty()
    sub_area = StringProperty()
    using_adm = BooleanProperty()
    using_call_center = BooleanProperty()
    custom_eula = BooleanProperty()
    can_use_data = BooleanProperty(default=True)
    notes = StringProperty()
    organization_name = StringProperty()
    platform = StringListProperty()
    project_manager = StringProperty()
    phone_model = StringProperty()
    goal_time_period = IntegerProperty()
    goal_followup_rate = DecimalProperty()
    # intentionally different from and commtrack_enabled so that FMs can change
    commtrack_domain = BooleanProperty()
    performance_threshold = IntegerProperty()
    experienced_threshold = IntegerProperty()
    amplifies_workers = StringProperty(
        choices=[AMPLIFIES_YES, AMPLIFIES_NO, AMPLIFIES_NOT_SET],
        default=AMPLIFIES_NOT_SET
    )
    amplifies_project = StringProperty(
        choices=[AMPLIFIES_YES, AMPLIFIES_NO, AMPLIFIES_NOT_SET],
        default=AMPLIFIES_NOT_SET
    )
    business_unit = StringProperty(choices=BUSINESS_UNITS + [""], default="")
    data_access_threshold = IntegerProperty()
    partner_technical_competency = IntegerProperty()
    support_prioritization = IntegerProperty()
    gs_continued_involvement = StringProperty()
    technical_complexity = StringProperty()
    app_design_comments = StringProperty()
    training_materials = StringProperty()
    partner_comments = StringProperty()
    partner_contact = StringProperty()
    dimagi_contact = StringProperty()
Exemple #5
0
class WeightedPropertyPatientFinder(PatientFinder):
    """
    Finds patients that match cases by assigning weights to matching
    property values, and adding those weights to calculate a confidence
    score.
    """

    # Identifiers that are searchable in OpenMRS. e.g.
    #     [ 'bahmni_id', 'household_id', 'last_name']
    searchable_properties = ListProperty()

    # The weight assigned to a matching property.
    # [
    #     {"case_property": "bahmni_id", "weight": 0.9},
    #     {"case_property": "household_id", "weight": 0.9},
    #     {"case_property": "dob", "weight": 0.75},
    #     {"case_property": "first_name", "weight": 0.025},
    #     {"case_property": "last_name", "weight": 0.025},
    #     {"case_property": "municipality", "weight": 0.2},
    # ]
    property_weights = ListProperty(PropertyWeight)

    # The threshold that the sum of weights must pass for a CommCare case to
    # be considered a match to an OpenMRS patient
    threshold = DecimalProperty(default=1.0)

    # If more than one patient passes `threshold`, the margin by which the
    # weight of the best match must exceed the weight of the second-best match
    # to be considered correct.
    confidence_margin = DecimalProperty(default=0.667)  # Default: Matches two thirds better than second-best

    def __init__(self, *args, **kwargs):
        super(WeightedPropertyPatientFinder, self).__init__(*args, **kwargs)
        self._property_map = {}

    def set_property_map(self, case_config):
        """
        Set self._property_map to map OpenMRS properties and attributes
        to case properties.
        """
        # Example value of case_config::
        #
        #     {
        #         "person_properties": {
        #             "birthdate": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "dob"
        #             }
        #         },
        #         "person_preferred_name": {
        #             "givenName": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "given_name"
        #             },
        #             "familyName": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "family_name"
        #             }
        #         },
        #         "person_preferred_address": {
        #             "address1": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "address_1"
        #             },
        #             "address2": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "address_2"
        #             }
        #         },
        #         "person_attributes": {
        #             "c1f4239f-3f10-11e4-adec-0800271c1b75": {
        #                 "doc_type": "CaseProperty",
        #                 "case_property": "caste"
        #             },
        #             "c1f455e7-3f10-11e4-adec-0800271c1b75": {
        #                 "doc_type": "CasePropertyMap",
        #                 "case_property": "class",
        #                 "value_map": {
        #                     "sc": "c1fcd1c6-3f10-11e4-adec-0800271c1b75",
        #                     "general": "c1fc20ab-3f10-11e4-adec-0800271c1b75",
        #                     "obc": "c1fb51cc-3f10-11e4-adec-0800271c1b75",
        #                     "other_caste": "c207073d-3f10-11e4-adec-0800271c1b75",
        #                     "st": "c20478b6-3f10-11e4-adec-0800271c1b75"
        #                 }
        #             }
        #         }
        #         // ...
        #     }
        #
        for person_prop, value_source in case_config['person_properties'].items():
            jsonpath = parse('person.' + person_prop)
            self._property_map.update(get_caseproperty_jsonpathvaluemap(jsonpath, value_source))

        for attr_uuid, value_source in case_config['person_attributes'].items():
            # jsonpath_rw offers programmatic JSONPath expressions. For details on how to create JSONPath
            # expressions programmatically see the
            # `jsonpath_rw documentation <https://github.com/kennknowles/python-jsonpath-rw#programmatic-jsonpath>`__
            #
            # The `Where` JSONPath expression "*jsonpath1* `where` *jsonpath2*" returns nodes matching *jsonpath1*
            # where a child matches *jsonpath2*. `Cmp` does a comparison in *jsonpath2*. It accepts a
            # comparison operator and a value. The JSONPath expression below is the equivalent of::
            #
            #     (person.attributes[*] where attributeType.uuid eq attr_uuid).value
            #
            # This `for` loop will let us extract the person attribute values where their attribute type UUIDs
            # match those configured in case_config['person_attributes']
            jsonpath = Child(
                Where(
                    Child(Child(Fields('person'), Fields('attributes')), Slice()),
                    Cmp(Child(Fields('attributeType'), Fields('uuid')), eq, attr_uuid)
                ),
                Fields('value')
            )
            self._property_map.update(get_caseproperty_jsonpathvaluemap(jsonpath, value_source))

        for name_prop, value_source in case_config['person_preferred_name'].items():
            jsonpath = parse('person.preferredName.' + name_prop)
            self._property_map.update(get_caseproperty_jsonpathvaluemap(jsonpath, value_source))

        for addr_prop, value_source in case_config['person_preferred_address'].items():
            jsonpath = parse('person.preferredAddress.' + addr_prop)
            self._property_map.update(get_caseproperty_jsonpathvaluemap(jsonpath, value_source))

        for id_type_uuid, value_source in case_config['patient_identifiers'].items():
            if id_type_uuid == 'uuid':
                jsonpath = parse('uuid')
            else:
                # The JSONPath expression below is the equivalent of::
                #
                #     (identifiers[*] where identifierType.uuid eq id_type_uuid).identifier
                #
                # Similar to `person_attributes` above, this will extract the person identifier values where
                # their identifier type UUIDs match those configured in case_config['patient_identifiers']
                jsonpath = Child(
                    Where(
                        Child(Fields('identifiers'), Slice()),
                        Cmp(Child(Fields('identifierType'), Fields('uuid')), eq, id_type_uuid)
                    ),
                    Fields('identifier')
                )
            self._property_map.update(get_caseproperty_jsonpathvaluemap(jsonpath, value_source))

    def get_score(self, patient, case):
        """
        Return the sum of weighted properties to give an OpenMRS
        patient a score of how well they match a CommCare case.
        """
        def weights():
            for property_weight in self.property_weights:
                prop = property_weight['case_property']
                weight = property_weight['weight']

                matches = self._property_map[prop].jsonpath.find(patient)
                for match in matches:
                    patient_value = match.value
                    value_map = self._property_map[prop].value_map
                    case_value = case.get_case_property(prop)
                    is_equal = value_map.get(patient_value, patient_value) == case_value
                    yield weight if is_equal else 0

        return sum(weights())

    def find_patients(self, requests, case, case_config):
        """
        Matches cases to patients. Returns a list of patients, each
        with a confidence score >= self.threshold
        """
        from corehq.motech.openmrs.logger import logger
        from corehq.motech.openmrs.repeater_helpers import search_patients

        self.set_property_map(case_config)

        candidates = {}  # key on OpenMRS UUID to filter duplicates
        for prop in self.searchable_properties:
            value = case.get_case_property(prop)
            if value:
                response_json = search_patients(requests, value)
                for patient in response_json['results']:
                    score = self.get_score(patient, case)
                    if score >= self.threshold:
                        candidates[patient['uuid']] = PatientScore(patient, score)
        if not candidates:
            logger.info(
                'Unable to match case "%s" (%s): No candidate patients found.',
                case.name, case.get_id,
            )
            return []
        if len(candidates) == 1:
            patient = list(candidates.values())[0].patient
            logger.info(
                'Matched case "%s" (%s) to ONLY patient candidate: \n%s',
                case.name, case.get_id, pformat(patient, indent=2),
            )
            return [patient]
        patients_scores = sorted(six.itervalues(candidates), key=lambda candidate: candidate.score, reverse=True)
        if patients_scores[0].score / patients_scores[1].score > 1 + self.confidence_margin:
            # There is more than a `confidence_margin` difference
            # (defaults to 10%) in score between the best-ranked
            # patient and the second-best-ranked patient. Let's go with
            # Patient One.
            patient = patients_scores[0].patient
            logger.info(
                'Matched case "%s" (%s) to BEST patient candidate: \n%s',
                case.name, case.get_id, pformat(patients_scores, indent=2),
            )
            return [patient]
        # We can't be sure. Just send them all.
        logger.info(
            'Unable to match case "%s" (%s) to patient candidates: \n%s',
            case.name, case.get_id, pformat(patients_scores, indent=2),
        )
        return [ps.patient for ps in patients_scores]
Exemple #6
0
class PropertyWeight(DocumentSchema):
    case_property = StringProperty()
    weight = DecimalProperty()
Exemple #7
0
class ReportConfiguration(UnicodeMixIn, QuickCachedDocumentMixin, Document):
    """
    A report configuration. These map 1:1 with reports that show up in the UI.
    """
    domain = StringProperty(required=True)
    visible = BooleanProperty(default=True)
    # config_id of the datasource
    config_id = StringProperty(required=True)
    data_source_type = StringProperty(
        default=DATA_SOURCE_TYPE_STANDARD,
        choices=[DATA_SOURCE_TYPE_STANDARD, DATA_SOURCE_TYPE_AGGREGATE])
    title = StringProperty()
    description = StringProperty()
    aggregation_columns = StringListProperty()
    filters = ListProperty()
    columns = ListProperty()
    configured_charts = ListProperty()
    sort_expression = ListProperty()
    soft_rollout = DecimalProperty(default=0)  # no longer used
    report_meta = SchemaProperty(ReportMeta)
    custom_query_provider = StringProperty(required=False)

    def __unicode__(self):
        return '{} - {}'.format(self.domain, self.title)

    def save(self, *args, **kwargs):
        self.report_meta.last_modified = datetime.utcnow()
        super(ReportConfiguration, self).save(*args, **kwargs)

    @property
    @memoized
    def filters_without_prefilters(self):
        return [f for f in self.filters if f['type'] != 'pre']

    @property
    @memoized
    def prefilters(self):
        return [f for f in self.filters if f['type'] == 'pre']

    @property
    @memoized
    def config(self):
        return get_datasource_config(self.config_id, self.domain,
                                     self.data_source_type)[0]

    @property
    @memoized
    def report_columns(self):
        return [
            ReportColumnFactory.from_spec(c, self.is_static)
            for c in self.columns
        ]

    @property
    @memoized
    def ui_filters(self):
        return [ReportFilterFactory.from_spec(f, self) for f in self.filters]

    @property
    @memoized
    def charts(self):
        return [ChartFactory.from_spec(g._obj) for g in self.configured_charts]

    @property
    @memoized
    def location_column_id(self):
        cols = [col for col in self.report_columns if col.type == 'location']
        if cols:
            return cols[0].column_id

    @property
    def map_config(self):
        def map_col(column):
            if column['column_id'] != self.location_column_id:
                return {
                    'column_id': column['column_id'],
                    'label': column['display']
                }

        if self.location_column_id:
            return {
                'location_column_id': self.location_column_id,
                'layer_name': {
                    'XFormInstance': _('Forms'),
                    'CommCareCase': _('Cases')
                }.get(self.config.referenced_doc_type, "Layer"),
                'columns':
                [x for x in (map_col(col) for col in self.columns) if x]
            }

    @property
    @memoized
    def sort_order(self):
        return [
            ReportOrderByFactory.from_spec(e) for e in self.sort_expression
        ]

    @property
    def table_id(self):
        return self.config.table_id

    def get_ui_filter(self, filter_slug):
        for filter in self.ui_filters:
            if filter.name == filter_slug:
                return filter
        return None

    def get_languages(self):
        """
        Return the languages used in this report's column and filter display properties.
        Note that only explicitly identified languages are returned. So, if the
        display properties are all strings, "en" would not be returned.
        """
        langs = set()
        for item in self.columns + self.filters:
            if isinstance(item['display'], dict):
                langs |= set(item['display'].keys())
        return langs

    def validate(self, required=True):
        from corehq.apps.userreports.reports.data_source import ConfigurableReportDataSource

        def _check_for_duplicates(supposedly_unique_list, error_msg):
            # http://stackoverflow.com/questions/9835762/find-and-list-duplicates-in-python-list
            duplicate_items = set([
                item for item in supposedly_unique_list
                if supposedly_unique_list.count(item) > 1
            ])
            if len(duplicate_items) > 0:
                raise BadSpecError(
                    _(error_msg).format(', '.join(sorted(duplicate_items))))

        super(ReportConfiguration, self).validate(required)

        # check duplicates before passing to factory since it chokes on them
        _check_for_duplicates(
            [FilterSpec.wrap(f).slug for f in self.filters],
            'Filters cannot contain duplicate slugs: {}',
        )
        _check_for_duplicates(
            [
                column_id for c in self.report_columns
                for column_id in c.get_column_ids()
            ],
            'Columns cannot contain duplicate column_ids: {}',
        )

        # these calls all implicitly do validation
        ConfigurableReportDataSource.from_spec(self)
        self.ui_filters
        self.charts
        self.sort_order

    @classmethod
    @quickcache(['cls.__name__', 'domain'])
    def by_domain(cls, domain):
        return get_report_configs_for_domain(domain)

    @classmethod
    @quickcache(['cls.__name__', 'domain', 'data_source_id'])
    def count_by_data_source(cls, domain, data_source_id):
        return get_number_of_report_configs_by_data_source(
            domain, data_source_id)

    def clear_caches(self):
        super(ReportConfiguration, self).clear_caches()
        self.by_domain.clear(self.__class__, self.domain)
        self.count_by_data_source.clear(self.__class__, self.domain,
                                        self.config_id)

    @property
    def is_static(self):
        return report_config_id_is_static(self._id)
Exemple #8
0
class Product(Document):
    """
    A product, e.g. "coartem" or "tylenol"
    """
    domain = StringProperty()
    name = StringProperty()
    unit = StringProperty()
    code_ = StringProperty()  # todo: why the hell is this code_ and not code
    description = StringProperty()
    category = StringProperty()
    program_id = StringProperty()
    cost = DecimalProperty()
    product_data = DictProperty()
    is_archived = BooleanProperty(default=False)
    last_modified = DateTimeProperty()

    @classmethod
    def wrap(cls, data):
        from corehq.apps.groups.models import dt_no_Z_re
        # If "Z" is missing because of the Aug 2014 migration, then add it.
        # cf. Group class
        last_modified = data.get('last_modified')
        if last_modified and dt_no_Z_re.match(last_modified):
            data['last_modified'] += 'Z'
        return super(Product, cls).wrap(data)

    @classmethod
    def save_docs(cls, docs, use_uuids=True, codes_by_domain=None):
        from corehq.apps.commtrack.util import generate_code

        codes_by_domain = codes_by_domain or {}

        def get_codes(domain):
            if domain not in codes_by_domain:
                codes_by_domain[domain] = SQLProduct.objects.filter(domain=domain)\
                    .values_list('code', flat=True).distinct()
            return codes_by_domain[domain]

        for doc in docs:
            doc.last_modified = datetime.utcnow()
            if not doc['code_']:
                doc['code_'] = generate_code(
                    doc['name'],
                    get_codes(doc['domain'])
                )

        super(Product, cls).save_docs(docs, use_uuids)

        domains = {doc['domain'] for doc in docs}
        for domain in domains:
            cls.clear_caches(domain)

    bulk_save = save_docs

    def sync_to_sql(self):
        properties_to_sync = [
            ('product_id', '_id'),
            'domain',
            'name',
            'is_archived',
            ('code', 'code_'),
            'description',
            'category',
            'program_id',
            'cost',
            ('units', 'unit'),
            'product_data',
        ]

        # sync properties to SQL version
        sql_product, _ = SQLProduct.objects.get_or_create(
            product_id=self._id
        )

        for prop in properties_to_sync:
            if isinstance(prop, tuple):
                sql_prop, couch_prop = prop
            else:
                sql_prop = couch_prop = prop

            if hasattr(self, couch_prop):
                setattr(sql_product, sql_prop, getattr(self, couch_prop))

        sql_product.save()

    def save(self, *args, **kwargs):
        """
        Saving a couch version of Product will trigger
        one way syncing to the SQLProduct version of this
        product.
        """
        # mark modified time stamp for selective syncing
        self.last_modified = datetime.utcnow()

        # generate code if user didn't specify one
        if not self.code:
            from corehq.apps.commtrack.util import generate_code
            self.code = generate_code(
                self.name,
                SQLProduct.objects
                    .filter(domain=self.domain)
                    .values_list('code', flat=True)
                    .distinct()
            )

        result = super(Product, self).save(*args, **kwargs)

        self.clear_caches(self.domain)
        self.sync_to_sql()

        return result

    @property
    def code(self):
        return self.code_

    @code.setter
    def code(self, val):
        self.code_ = val.lower() if val else None

    @classmethod
    def clear_caches(cls, domain):
        from casexml.apps.phone.utils import clear_fixture_cache
        from corehq.apps.products.fixtures import ALL_CACHE_PREFIXES
        for prefix in ALL_CACHE_PREFIXES:
            clear_fixture_cache(domain, prefix)

    @classmethod
    def by_domain(cls, domain, wrap=True, include_archived=False):
        queryset = SQLProduct.objects.filter(domain=domain)
        if not include_archived:
            queryset = queryset.filter(is_archived=False)
        return list(queryset.couch_products(wrapped=wrap))

    @classmethod
    def _export_attrs(cls):
        return [
            ('name', str),
            ('unit', str),
            'description',
            'category',
            ('program_id', str),
            ('cost', lambda a: Decimal(a) if a else None),
        ]

    def to_dict(self):
        from corehq.apps.commtrack.util import encode_if_needed
        product_dict = {}

        product_dict['id'] = self._id
        product_dict['product_id'] = self.code_

        for attr in self._export_attrs():
            real_attr = attr[0] if isinstance(attr, tuple) else attr
            product_dict[real_attr] = encode_if_needed(
                getattr(self, real_attr)
            )

        return product_dict

    def custom_property_dict(self):
        from corehq.apps.commtrack.util import encode_if_needed
        property_dict = {}

        for prop, val in self.product_data.items():
            property_dict['data: ' + prop] = encode_if_needed(val)

        return property_dict

    def archive(self):
        """
        Mark a product as archived. This will cause it (and its data)
        to not show up in default Couch and SQL views.
        """
        self.is_archived = True
        self.save()

    def unarchive(self):
        """
        Unarchive a product, causing it (and its data) to show
        up in Couch and SQL views again.
        """
        if self.code:
            if SQLProduct.objects.filter(domain=self.domain, code=self.code, is_archived=False).exists():
                raise DuplicateProductCodeException()
        self.is_archived = False
        self.save()

    @classmethod
    def from_excel(cls, row, custom_data_validator):
        if not row:
            return None

        id = row.get('id')
        if id:
            try:
                p = cls.get(id)
            except ResourceNotFound:
                raise InvalidProductException(
                    _("Product with ID '{product_id}' could not be found!").format(product_id=id)
                )
        else:
            p = cls()

        p.code = str(row.get('product_id') or '')

        for attr in cls._export_attrs():
            key = attr[0] if isinstance(attr, tuple) else attr
            if key in row:
                val = row[key]
                if val is None:
                    val = ''
                if isinstance(attr, tuple):
                    val = attr[1](val)
                setattr(p, key, val)
            else:
                break

        if not p.code:
            raise InvalidProductException(_('Product ID is a required field and cannot be blank!'))
        if not p.name:
            raise InvalidProductException(_('Product name is a required field and cannot be blank!'))

        custom_data = row.get('data', {})
        error = custom_data_validator(custom_data)
        if error:
            raise InvalidProductException(error)

        p.product_data = custom_data
        p.product_data.update(row.get('uncategorized_data', {}))

        return p
Exemple #9
0
class WeightedPropertyPatientFinder(PatientFinder):
    """
    Finds patients that match cases by assigning weights to matching
    property values, and adding those weights to calculate a confidence
    score.
    """

    # Identifiers that are searchable in OpenMRS. e.g.
    #     [ 'bahmni_id', 'household_id', 'last_name']
    searchable_properties = ListProperty()

    # The weight assigned to a matching property.
    # [
    #     {"case_property": "bahmni_id", "weight": 0.9},
    #     {"case_property": "household_id", "weight": 0.9},
    #     {
    #         "case_property": "dob",
    #         "weight": 0.75,
    #         "match_type": "days_diff",
    #         // days_diff matches based on days difference from given date
    #         "match_params": [364]
    #     },
    #     {
    #         "case_property": "first_name",
    #         "weight": 0.025,
    #         "match_type": "levenshtein",
    #         // levenshtein function takes edit_distance / len
    #         "match_params": [0.2]
    #         // i.e. 20% is one edit for every 5 characters
    #         // e.g. "Riyaz" matches "Riaz" but not "Riazz"
    #     },
    #     {"case_property": "last_name", "weight": 0.025},
    #     {"case_property": "municipality", "weight": 0.2},
    # ]
    property_weights = ListProperty(PropertyWeight)

    # The threshold that the sum of weights must pass for a CommCare case to
    # be considered a match to an OpenMRS patient
    threshold = DecimalProperty(default=1.0)

    # If more than one patient passes `threshold`, the margin by which the
    # weight of the best match must exceed the weight of the second-best match
    # to be considered correct.
    confidence_margin = DecimalProperty(
        default=0.667)  # Default: Matches two thirds better than second-best

    def __init__(self, *args, **kwargs):
        super(WeightedPropertyPatientFinder, self).__init__(*args, **kwargs)
        self._property_map = {}

    def get_score(self, patient, case):
        """
        Return the sum of weighted properties to give an OpenMRS
        patient a score of how well they match a CommCare case.
        """
        def weights():
            for property_weight in self.property_weights:
                prop = property_weight['case_property']
                jsonpath, value_source = self._property_map[prop]
                weight = property_weight['weight']

                matches = jsonpath.find(patient)
                for match in matches:
                    patient_value = match.value
                    case_value = case.get_case_property(prop)
                    match_type = property_weight['match_type']
                    match_params = property_weight['match_params']
                    match_function = partial(MATCH_FUNCTIONS[match_type],
                                             *match_params)
                    is_equivalent = match_function(
                        value_source.deserialize(patient_value), case_value)
                    yield weight if is_equivalent else 0

        return sum(weights())

    def find_patients(self, requests, case, case_config):
        """
        Matches cases to patients. Returns a list of patients, each
        with a confidence score >= self.threshold
        """
        from corehq.motech.openmrs.logger import logger
        from corehq.motech.openmrs.openmrs_config import get_property_map
        from corehq.motech.openmrs.repeater_helpers import search_patients

        self._property_map = get_property_map(case_config)

        candidates = {}  # key on OpenMRS UUID to filter duplicates
        for prop in self.searchable_properties:
            value = case.get_case_property(prop)
            if value:
                response_json = search_patients(requests, value)
                for patient in response_json['results']:
                    score = self.get_score(patient, case)
                    if score >= self.threshold:
                        candidates[patient['uuid']] = PatientScore(
                            patient, score)
        if not candidates:
            logger.info(
                'Unable to match case "%s" (%s): No candidate patients found.',
                case.name,
                case.get_id,
            )
            return []
        if len(candidates) == 1:
            patient = list(candidates.values())[0].patient
            logger.info(
                'Matched case "%s" (%s) to ONLY patient candidate: \n%s',
                case.name,
                case.get_id,
                pformat(patient, indent=2),
            )
            return [patient]
        patients_scores = sorted(six.itervalues(candidates),
                                 key=lambda candidate: candidate.score,
                                 reverse=True)
        if patients_scores[0].score / patients_scores[
                1].score > 1 + self.confidence_margin:
            # There is more than a `confidence_margin` difference
            # (defaults to 10%) in score between the best-ranked
            # patient and the second-best-ranked patient. Let's go with
            # Patient One.
            patient = patients_scores[0].patient
            logger.info(
                'Matched case "%s" (%s) to BEST patient candidate: \n%s',
                case.name,
                case.get_id,
                pformat(patients_scores, indent=2),
            )
            return [patient]
        # We can't be sure. Just send them all.
        logger.info(
            'Unable to match case "%s" (%s) to patient candidates: \n%s',
            case.name,
            case.get_id,
            pformat(patients_scores, indent=2),
        )
        return [ps.patient for ps in patients_scores]
Exemple #10
0
class FinderConfig(DocumentSchema):
    property_weights = ListProperty(PropertyWeight)
    confidence_margin = DecimalProperty(default=0.5)
Exemple #11
0
class Product(Document):
    """
    A product, e.g. "coartem" or "tylenol"
    """
    domain = StringProperty()
    name = StringProperty()
    unit = StringProperty()
    code_ = StringProperty()  # todo: why the hell is this code_ and not code
    description = StringProperty()
    category = StringProperty()
    program_id = StringProperty()
    cost = DecimalProperty()
    product_data = DictProperty()
    is_archived = BooleanProperty(default=False)
    last_modified = DateTimeProperty()

    @classmethod
    def wrap(cls, data):
        from corehq.apps.groups.models import dt_no_Z_re
        # If "Z" is missing because of the Aug 2014 migration, then add it.
        # cf. Group class
        last_modified = data.get('last_modified')
        if last_modified and dt_no_Z_re.match(last_modified):
            data['last_modified'] += 'Z'
        return super(Product, cls).wrap(data)

    @classmethod
    def save_docs(cls,
                  docs,
                  use_uuids=True,
                  all_or_nothing=False,
                  codes_by_domain=None):
        from corehq.apps.commtrack.util import generate_code

        codes_by_domain = codes_by_domain or {}

        def get_codes(domain):
            if domain not in codes_by_domain:
                codes_by_domain[domain] = SQLProduct.objects.filter(domain=domain)\
                    .values_list('code', flat=True).distinct()
            return codes_by_domain[domain]

        for doc in docs:
            if not doc['code_']:
                doc['code_'] = generate_code(doc['name'],
                                             get_codes(doc['domain']))

        super(Product, cls).save_docs(docs, use_uuids, all_or_nothing)

    bulk_save = save_docs

    def sync_to_sql(self):
        properties_to_sync = [
            ('product_id', '_id'),
            'domain',
            'name',
            'is_archived',
            ('code', 'code_'),
            'description',
            'category',
            'program_id',
            'cost',
            ('units', 'unit'),
            'product_data',
        ]

        # sync properties to SQL version
        sql_product, _ = SQLProduct.objects.get_or_create(product_id=self._id)

        for prop in properties_to_sync:
            if isinstance(prop, tuple):
                sql_prop, couch_prop = prop
            else:
                sql_prop = couch_prop = prop

            if hasattr(self, couch_prop):
                setattr(sql_product, sql_prop, getattr(self, couch_prop))

        sql_product.save()

    def save(self, *args, **kwargs):
        """
        Saving a couch version of Product will trigger
        one way syncing to the SQLProduct version of this
        product.
        """
        # mark modified time stamp for selective syncing
        self.last_modified = datetime.utcnow()

        # generate code if user didn't specify one
        if not self.code:
            from corehq.apps.commtrack.util import generate_code
            self.code = generate_code(
                self.name,
                SQLProduct.objects.filter(domain=self.domain).values_list(
                    'code', flat=True).distinct())

        result = super(Product, self).save(*args, **kwargs)

        self.sync_to_sql()

        return result

    @property
    def code(self):
        return self.code_

    @code.setter
    def code(self, val):
        self.code_ = val.lower() if val else None

    @classmethod
    def get_by_code(cls, domain, code):
        if not code:
            return None
        result = cls.view("commtrack/product_by_code",
                          key=[domain, code.lower()],
                          include_docs=True).first()
        return result

    @classmethod
    def by_program_id(cls, domain, prog_id, wrap=True, **kwargs):
        kwargs.update(
            dict(view_name='commtrack/product_by_program_id',
                 key=[domain, prog_id],
                 include_docs=True))
        if wrap:
            return Product.view(**kwargs)
        else:
            return [
                row["doc"] for row in Product.view(wrap_doc=False, **kwargs)
            ]

    @classmethod
    def by_domain(cls, domain, wrap=True, include_archived=False, **kwargs):
        """
        Gets all products in a domain.

        By default this filters out any archived products.
        WARNING: this doesn't paginate correctly; it filters after the query
        If you need pagination, use SQLProduct instead
        """
        kwargs.update(
            dict(view_name='commtrack/products',
                 startkey=[domain],
                 endkey=[domain, {}],
                 include_docs=True))
        if wrap:
            products = Product.view(**kwargs)
            if not include_archived:
                return filter(lambda p: not p.is_archived, products)
            else:
                return products
        else:
            if not include_archived:
                return [
                    row["doc"]
                    for row in Product.view(wrap_doc=False, **kwargs)
                    if not row["doc"].get('is_archived', False)
                ]
            else:
                return [
                    row["doc"]
                    for row in Product.view(wrap_doc=False, **kwargs)
                ]

    @classmethod
    def archived_by_domain(cls, domain, wrap=True, **kwargs):
        products = cls.by_domain(domain, wrap, kwargs)
        if wrap:
            return filter(lambda p: p.is_archived, products)
        else:
            return [p for p in products if p.get('is_archived', False)]

    @classmethod
    def ids_by_domain(cls, domain):
        """
        Gets all product ids in a domain.
        """
        view_results = Product.get_db().view(
            'commtrack/products',
            startkey=[domain],
            endkey=[domain, {}],
            include_docs=False,
        )
        return [row['id'] for row in view_results]

    @classmethod
    def count_by_domain(cls, domain):
        """
        Gets count of products in a domain
        """
        # todo: we should add a reduce so we can get this out of couch
        return len(cls.ids_by_domain(domain))

    @classmethod
    def _export_attrs(cls):
        return [
            ('name', unicode),
            ('unit', unicode),
            'description',
            'category',
            ('program_id', str),
            ('cost', lambda a: Decimal(a) if a else None),
        ]

    def to_dict(self):
        from corehq.apps.commtrack.util import encode_if_needed
        product_dict = {}

        product_dict['id'] = self._id
        product_dict['product_id'] = self.code_

        for attr in self._export_attrs():
            real_attr = attr[0] if isinstance(attr, tuple) else attr
            product_dict[real_attr] = encode_if_needed(getattr(
                self, real_attr))

        return product_dict

    def custom_property_dict(self):
        from corehq.apps.commtrack.util import encode_if_needed
        property_dict = {}

        for prop, val in self.product_data.iteritems():
            property_dict['data: ' + prop] = encode_if_needed(val)

        return property_dict

    def archive(self):
        """
        Mark a product as archived. This will cause it (and its data)
        to not show up in default Couch and SQL views.
        """
        self.is_archived = True
        self.save()

    def unarchive(self):
        """
        Unarchive a product, causing it (and its data) to show
        up in Couch and SQL views again.
        """
        if self.code:
            if SQLProduct.objects.filter(code=self.code,
                                         is_archived=False).exists():
                raise DuplicateProductCodeException()
        self.is_archived = False
        self.save()

    @classmethod
    def from_excel(cls, row, custom_data_validator):
        if not row:
            return None

        id = row.get('id')
        if id:
            try:
                p = cls.get(id)
            except ResourceNotFound:
                raise InvalidProductException(
                    _("Product with ID '{product_id}' could not be found!").
                    format(product_id=id))
        else:
            p = cls()

        p.code = str(row.get('product_id') or '')

        for attr in cls._export_attrs():
            key = attr[0] if isinstance(attr, tuple) else attr
            if key in row:
                val = row[key]
                if val is None:
                    val = ''
                if isinstance(attr, tuple):
                    val = attr[1](val)
                setattr(p, key, val)
            else:
                break

        if not p.code:
            raise InvalidProductException(
                _('Product ID is a required field and cannot be blank!'))
        if not p.name:
            raise InvalidProductException(
                _('Product name is a required field and cannot be blank!'))

        custom_data = row.get('data', {})
        error = custom_data_validator(custom_data)
        if error:
            raise InvalidProductException(error)

        p.product_data = custom_data
        p.product_data.update(row.get('uncategorized_data', {}))

        return p