class CaseDisplaySettings(DocumentSchema): case_details = DictProperty( verbose_name="Mapping of case type to definitions of properties " "to display above the fold on case details") form_details = DictProperty( verbose_name="Mapping of form xmlns to definitions of properties " "to display for individual forms")
class Dhis2CaseConfig(DocumentSchema): """ A Dhis2CaseConfig maps a case type to a tracked entity type. """ case_type = StringProperty() # The ID of the Tracked Entity type. e.g. the ID of "Person" te_type_id = StringProperty() # CommCare case indices to export as DHIS2 relationships relationships_to_export = SchemaListProperty(RelationshipConfig) # The case property to store the ID of the corresponding Tracked # Entity instance. If this is not set, MOTECH will search for a # matching Tracked Entity on every payload. tei_id = DictProperty() # The corresponding Org Unit of the case's location org_unit_id = DictProperty() # Attribute Type ID to case property / constant attributes = DictProperty() # Events for this Tracked Entity: form_configs = ListProperty(Dhis2FormConfig) finder_config = SchemaProperty(FinderConfig)
class BaseExpressionRepeater(Repeater): """Uses a UCR dict expression to send a generic json response """ class Meta: app_label = 'repeaters' configured_filter = DictProperty() configured_expression = DictProperty() url_template = StringProperty() payload_generator_classes = (ExpressionPayloadGenerator, ) @property @memoized def parsed_filter(self): return FilterFactory.from_spec(self.configured_filter, FactoryContext.empty()) @property @memoized def parsed_expression(self): return ExpressionFactory.from_spec(self.configured_expression, FactoryContext.empty()) @classmethod def available_for_domain(cls, domain): return EXPRESSION_REPEATER.enabled(domain) def allowed_to_forward(self, payload): payload_json = payload.to_json() return self.parsed_filter(payload_json, EvaluationContext(payload_json)) @memoized def get_payload(self, repeat_record): return self.generator.get_payload( repeat_record, self.payload_doc(repeat_record), self.parsed_expression, ) def get_url(self, repeat_record): base_url = super().get_url(repeat_record) if self.url_template: return base_url + self.generator.get_url( repeat_record, self.url_template, self.payload_doc(repeat_record), ) return base_url @classmethod def _migration_get_sql_model_class(cls): return SQLBaseExpressionRepeater @classmethod def _migration_get_fields(cls): return super()._migration_get_fields() + [ "configured_filter", "configured_expression" ]
class NavigationEventAudit(AuditEvent): """ Audit event to track happenings within the system, ie, view access """ request_path = StringProperty() ip_address = StringProperty() user_agent = StringProperty() view = StringProperty() # the fully qualifid view name view_kwargs = DictProperty() headers = DictProperty() # the request.META? # in the future possibly save some disk space by storing user agent and IP stuff in a separte session document? session_key = StringProperty() status_code = IntegerProperty() extra = DictProperty() @property def summary(self): return "%s from %s" % (self.request_path, self.ip_address) class Meta(object): app_label = 'auditcare' @cached_property def domain(self): from corehq.apps.domain.utils import get_domain_from_url return get_domain_from_url(self.request_path) @classmethod def audit_view(cls, request, user, view_func, view_kwargs, extra={}): """Creates an instance of a Access log.""" try: audit = cls.create_audit(cls, user) audit.description += "View" if len(list(request.GET)) > 0: params = "&".join(f"{x}={request.GET[x]}" for x in request.GET.keys()) audit.request_path = f"{request.path}?{params}" else: audit.request_path = request.path audit.ip_address = get_ip(request) audit.user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>') audit.view = "%s.%s" % (view_func.__module__, view_func.__name__) for k in STANDARD_HEADER_KEYS: header_item = request.META.get(k, None) if header_item is not None: audit.headers[k] = header_item # it's a bit verbose to go to that extreme, TODO: need to have # targeted fields in the META, but due to server differences, it's # hard to make it universal. #audit.headers = request.META audit.session_key = request.session.session_key audit.extra = extra audit.view_kwargs = view_kwargs return audit except Exception: log.exception("NavigationEventAudit.audit_view error")
class Dhis2FormConfig(DocumentSchema): xmlns = StringProperty(required=True) program_id = StringProperty(required=True) enrollment_date = DictProperty(required=False) incident_date = DictProperty(required=False) program_stage_id = DictProperty(required=False) program_status = DictProperty( required=False, default={"value": DHIS2_PROGRAM_STATUS_ACTIVE}) org_unit_id = DictProperty( required=False, default={"form_user_ancestor_location_field": LOCATION_DHIS_ID}) event_date = DictProperty(required=True, default={ "form_question": "/metadata/received_on", "external_data_type": DHIS2_DATA_TYPE_DATE, }) event_status = DictProperty( required=False, default={"value": DHIS2_EVENT_STATUS_COMPLETED}) completed_date = DictProperty(required=False) datavalue_maps = SchemaListProperty(FormDataValueMap) event_location = DictProperty(required=False, default={}) @classmethod def wrap(cls, data): if isinstance(data.get('org_unit_id'), str): # Convert org_unit_id from a string to a ConstantValue data['org_unit_id'] = {'value': data['org_unit_id']} if isinstance(data.get('event_status'), str): data['event_status'] = {'value': data['event_status']} if isinstance(data.get('program_status'), str): data['program_status'] = {'value': data['program_status']} return super(Dhis2FormConfig, cls).wrap(data)
class NavigationEventAudit(AuditEvent): """ Audit event to track happenings within the system, ie, view access """ request_path = StringProperty() ip_address = StringProperty() user_agent = StringProperty() view = StringProperty() #the fully qualifid view name view_kwargs = DictProperty() headers = DictProperty() #the request.META? session_key = StringProperty( ) #in the future possibly save some disk space by storing user agent and IP stuff in a separte session document? status_code = IntegerProperty() extra = DictProperty() @property def summary(self): return "%s from %s" % (self.request_path, self.ip_address) class Meta: app_label = 'auditcare' @classmethod def audit_view(cls, request, user, view_func, view_kwargs, extra={}): """Creates an instance of a Access log.""" try: audit = cls.create_audit(cls, user) audit.description += "View" if len(request.GET.keys()) > 0: audit.request_path = "%s?%s" % (request.path, '&'.join([ "%s=%s" % (x, request.GET[x]) for x in request.GET.keys() ])) else: audit.request_path = request.path audit.ip_address = utils.get_ip(request) audit.user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>') audit.view = "%s.%s" % (view_func.__module__, view_func.func_name) for k in STANDARD_HEADER_KEYS: header_item = request.META.get(k, None) if header_item is not None: audit.headers[k] = header_item #audit.headers = request.META #it's a bit verbose to go to that extreme, TODO: need to have targeted fields in the META, but due to server differences, it's hard to make it universal. audit.session_key = request.session.session_key audit.extra = extra audit.view_kwargs = view_kwargs audit.save() return audit except Exception, ex: log.error("NavigationEventAudit.audit_view error: %s", ex)
class PatientFinder(DocumentSchema): """ The ``PatientFinder`` base class was developed as a way to handle situations where patient cases are created in CommCare instead of being imported from OpenMRS. When patients are imported from OpenMRS, they will come with at least one identifier that MOTECH can use to match the case in CommCare with the corresponding patient in OpenMRS. But if the case is registered in CommCare then we may not have an ID, or the ID could be wrong. We need to search for a corresponding OpenMRS patient. Different projects may focus on different kinds of case properties, so it was felt that a base class would allow some flexibility. The ``PatientFinder.wrap()`` method allows you to wrap documents of subclasses. The ``PatientFinder.find_patients()`` method must be implemented by subclasses. It returns a list of zero, one, or many patients. If it returns one patient, the OpenmrsRepeater.find_or_create_patient() will accept that patient as a true match. .. NOTE:: The consequences of a false positive (a Type II error) are severe: A real patient will have their valid values overwritten by those of someone else. So ``PatientFinder`` subclasses should be written and configured to skew towards false negatives (Type I errors). In other words, it is much better not to choose a patient than to choose the wrong patient. """ # Whether to create a new patient if no patients are found create_missing = DictProperty(default=constant_false) @classmethod def wrap(cls, data): if 'create_missing' in data and isinstance(data['create_missing'], bool): data['create_missing'] = { 'external_data_type': OPENMRS_DATA_TYPE_BOOLEAN, 'value': str(data['create_missing']) } if cls is PatientFinder: subclass = { sub._doc_type: sub for sub in recurse_subclasses(cls) }.get(data['doc_type']) return subclass.wrap(data) if subclass else None else: return super(PatientFinder, cls).wrap(data) def find_patients(self, requests, case, case_config): """ Given a case, search OpenMRS for possible matches. Return the best results. Subclasses must define "best". If just one result is returned, it will be chosen. """ raise NotImplementedError
class CommCareCaseAttachment(LooselyEqualDocumentSchema, CaseAttachmentMixin, UnicodeMixIn): identifier = StringProperty() attachment_src = StringProperty() attachment_from = StringProperty() attachment_name = StringProperty() server_mime = StringProperty() # Server detected MIME server_md5 = StringProperty() # Couch detected hash attachment_size = IntegerProperty() # file size attachment_properties = DictProperty( ) # width, height, other relevant metadata @property def content_type(self): return self.server_mime @classmethod def from_case_index_update(cls, attachment): if attachment.attachment_src: guessed = mimetypes.guess_type(attachment.attachment_src) if len(guessed) > 0 and guessed[0] is not None: mime_type = guessed[0] else: mime_type = None ret = cls(identifier=attachment.identifier, attachment_src=attachment.attachment_src, attachment_from=attachment.attachment_from, attachment_name=attachment.attachment_name, server_mime=mime_type) else: ret = cls(identifier=attachment.identifier) return ret
class FixtureItemField(DocumentSchema): """ "field_value": "Delhi_IN_HIN", "properties": {"lang": "hin"} """ field_value = StringProperty() properties = DictProperty()
class CasePropertyMap(CaseProperty): """ Maps case property values to OpenMRS values or concept UUIDs """ # Example "person_attribute" value:: # # { # "00000000-771d-0000-0000-000000000000": { # "doc_type": "CasePropertyMap", # "case_property": "pill" # "value_map": { # "red": "00ff0000-771d-0000-0000-000000000000", # "blue": "000000ff-771d-0000-0000-000000000000", # } # } # } # value_map = DictProperty() def serialize(self, value): # Don't bother serializing. self.value_map does that already. # # Using `.get()` because it's OK if some CommCare answers are # not mapped to OpenMRS concepts, e.g. when only the "yes" value # of a yes-no question in CommCare is mapped to a concept in # OpenMRS. return self.value_map.get(value) def deserialize(self, external_value): reverse_map = {v: k for k, v in self.value_map.items()} return reverse_map.get(external_value)
class CasePropertyMap(CaseProperty): """ Maps case property values to OpenMRS values or concept UUIDs """ # Example "person_attribute" value:: # # { # "00000000-771d-0000-0000-000000000000": { # "doc_type": "CasePropertyMap", # "case_property": "pill" # "value_map": { # "red": "00ff0000-771d-0000-0000-000000000000", # "blue": "000000ff-771d-0000-0000-000000000000", # } # } # } # value_map = DictProperty() def get_value(self, case_trigger_info): value = super(CasePropertyMap, self).get_value(case_trigger_info) try: return self.value_map[value] except KeyError: # We don't care if some CommCare answers are not mapped to OpenMRS concepts, e.g. when only the "yes" # value of a yes-no question in CommCare is mapped to a concept in OpenMRS. return None
class FixtureReportResult(Document, QueryMixin): domain = StringProperty() location_id = StringProperty() start_date = DateProperty() end_date = DateProperty() report_slug = StringProperty() rows = DictProperty() name = StringProperty() class Meta(object): app_label = "m4change" @classmethod def by_composite_key(cls, domain, location_id, start_date, end_date, report_slug): try: return cls.view("m4change/fixture_by_composite_key", key=[domain, location_id, start_date, end_date, report_slug], include_docs=True).one(except_all=True) except (NoResultFound, ResourceNotFound, MultipleResultsFound): return None @classmethod def all_by_composite_key(cls, domain, location_id, start_date, end_date, report_slug): return cls.view("m4change/fixture_by_composite_key", startkey=[domain, location_id, start_date, end_date, report_slug], endkey=[domain, location_id, start_date, end_date, report_slug], include_docs=True).all() @classmethod def by_domain(cls, domain): return cls.view("m4change/fixture_by_composite_key", startkey=[domain], endkey=[domain, {}], include_docs=True).all() @classmethod def get_report_results_by_key(cls, domain, location_id, start_date, end_date): return cls.view("m4change/fixture_by_composite_key", startkey=[domain, location_id, start_date, end_date], endkey=[domain, location_id, start_date, end_date, {}], include_docs=True).all() @classmethod def _validate_params(cls, params): for param in params: if param is None or len(param) == 0: return False return True @classmethod def save_result(cls, domain, location_id, start_date, end_date, report_slug, rows, name): if not cls._validate_params([domain, location_id, report_slug]) \ or not isinstance(rows, dict) or len(rows) == 0 \ or not isinstance(start_date, date) or not isinstance(end_date, date): return FixtureReportResult(domain=domain, location_id=location_id, start_date=start_date, end_date=end_date, report_slug=report_slug, rows=rows, name=name).save()
class FormQuestionMap(FormQuestion): """ Maps form question values to OpenMRS values or concept UUIDs """ value_map = DictProperty() def get_value(self, case_trigger_info): value = super(FormQuestionMap, self).get_value(case_trigger_info) try: return self.value_map[value] except KeyError: return None
class AuxMedia(DocumentSchema): """ Additional metadata companion for couch models that you want to add arbitrary attachments to """ uploaded_date = DateTimeProperty() uploaded_by = StringProperty() uploaded_filename = StringProperty() # the uploaded filename info checksum = StringProperty() attachment_id = StringProperty() # the actual attachment id in blobs media_meta = DictProperty() notes = StringProperty()
class ModelAuditEvent(models.Model): object_type = StringProperty() # String of ContentType/Model, verbose_name='Case linking content type', blank=True, null=True) object_uuid = StringProperty() #('object_uuid', max_length=32, db_index=True, blank=True, null=True) properties = StringListProperty() #models.ManyToManyField(FieldAccess, blank=True, null=True) property_data = DictProperty() #models.TextField() #json of the actual fields accessed user = StringProperty() # The User's username accessing this accessed = DateTimeProperty(default=getdate) class Meta: app_label = 'auditcare'
class FormQuestionConcept(FormQuestion): """ Maps form question values to OpenMRS concepts """ value_concepts = DictProperty() def get_value(self, case_trigger_info): value = super(FormQuestionConcept, self).get_value(case_trigger_info) try: return self.value_concepts[value] except KeyError: return None
class FormQuestionMap(FormQuestion): """ Maps form question values to OpenMRS values or concept UUIDs """ value_map = DictProperty() def serialize(self, value): return self.value_map.get(value) def deserialize(self, external_value): reverse_map = {v: k for k, v in self.value_map.items()} return reverse_map.get(external_value)
class TranslationMixin(Document): translations = DictProperty() def set_translation(self, lang, key, value): if lang not in self.translations: self.translations[lang] = {} if value is not None: self.translations[lang][key] = value else: del self.translations[lang][key] def set_translations(self, lang, translations): self.translations[lang] = translations
class CasePropertyConcept(CaseProperty): """ Maps case property values to OpenMRS concepts """ value_concepts = DictProperty() def get_value(self, case_trigger_info): value = super(CasePropertyConcept, self).get_value(case_trigger_info) try: return self.value_concepts[value] except KeyError: # We don't care if some CommCare answers are not mapped to OpenMRS concepts, e.g. when only the "yes" # value of a yes-no question in CommCare is mapped to a concept in OpenMRS. return None
class CommCareCaseAttachment(LooselyEqualDocumentSchema, UnicodeMixIn): identifier = StringProperty() attachment_src = StringProperty() attachment_from = StringProperty() attachment_name = StringProperty() server_mime = StringProperty() # Server detected MIME server_md5 = StringProperty() # Couch detected hash attachment_size = IntegerProperty() # file size attachment_properties = DictProperty( ) # width, height, other relevant metadata @property def is_image(self): if self.server_mime is None: return None return True if self.server_mime.startswith('image/') else False @property def is_present(self): """ Helper method to see if this is a delete vs. update """ if self.identifier and (self.attachment_src == self.attachment_from is None): return False else: return True @property def attachment_key(self): return self.identifier @classmethod def from_case_index_update(cls, attachment): if attachment.attachment_src: guessed = mimetypes.guess_type(attachment.attachment_src) if len(guessed) > 0 and guessed[0] is not None: mime_type = guessed[0] else: mime_type = None ret = cls(identifier=attachment.identifier, attachment_src=attachment.attachment_src, attachment_from=attachment.attachment_from, attachment_name=attachment.attachment_name, server_mime=mime_type) else: ret = cls(identifier=attachment.identifier) return ret
class OpenmrsFormConfig(DocumentSchema): xmlns = StringProperty() # Used to determine the start of a visit and an encounter. The end # of a visit is set to one day (specifically 23:59:59) later. If not # given, the value defaults to when the form was completed according # to the device, /meta/timeEnd. openmrs_start_datetime = DictProperty(required=False) openmrs_visit_type = StringProperty() openmrs_encounter_type = StringProperty() openmrs_form = StringProperty() openmrs_observations = ListProperty(ObservationMapping) bahmni_diagnoses = ListProperty(ObservationMapping)
class OpenmrsImporter(Document): """ Import cases from an OpenMRS instance using a report """ domain = StringProperty() server_url = StringProperty() # e.g. "http://www.example.com/openmrs" username = StringProperty() password = StringProperty() # If a domain has multiple OpenmrsImporter instances, for which CommCare location is this one authoritative? location_id = StringProperty() # How often should cases be imported import_frequency = StringProperty(choices=IMPORT_FREQUENCY_CHOICES, default=IMPORT_FREQUENCY_MONTHLY) log_level = IntegerProperty() # OpenMRS UUID of the report of patients to be imported report_uuid = StringProperty() # Can include template params, e.g. {"endDate": "{{ today }}"} # Available template params: "today", "location" report_params = DictProperty() # The case type of imported cases case_type = StringProperty() # The ID of the owner of imported cases, if all imported cases are to have the same owner. To assign imported # cases to different owners, see `location_type` below. owner_id = StringProperty() # If report_params includes "{{ location }}" then location_type_name is used to determine which locations to # pull the report for. Those locations will need an "openmrs_uuid" param set. Imported cases will be owned by # the first mobile worker assigned to that location. If this OpenmrsImporter.location_id is set, only # sub-locations will be returned location_type_name = StringProperty() # external_id should always be the OpenMRS UUID of the patient (and not, for example, a national ID number) # because it is immutable. external_id_column is the column that contains the UUID external_id_column = StringProperty() # Space-separated column(s) to be concatenated to create the case name (e.g. "givenName familyName") name_columns = StringProperty() column_map = ListProperty(ColumnMapping) def __str__(self): return self.server_url
class ObservationMapping(DocumentSchema): """ Maps OpenMRS Observations to value sources. e.g.:: { "concept": "123456": "value": { "form_question": "/data/trimester" "value_map": { "first": "123456", "second": "123456", "third": "123456" }, "direction": "out" } } """ # If no concept is specified, this ObservationMapping is used for # setting a case property or creating an extension case for any # concept concept = StringProperty(required=True, default=ALL_CONCEPTS) value = DictProperty() # Import Observations as case updates from Atom feed. (Case type is # OpenmrsRepeater.white_listed_case_types[0]; Atom feed integration # requires len(OpenmrsRepeater.white_listed_case_types) == 1.) case_property = StringProperty(required=False) # Use indexed_case_mapping to create an extension case or a child # case instead of setting a case property. Used for referrals. indexed_case_mapping = SchemaProperty( IndexedCaseMapping, required=False, default=None, exclude_if_none=True ) def __eq__(self, other): return ( isinstance(other, self.__class__) and other.concept == self.concept and other.value == self.value and other.case_property == self.case_property )
class CommCareCaseAttachment(LooselyEqualDocumentSchema, IsImageMixin): identifier = StringProperty() attachment_src = StringProperty() attachment_from = StringProperty() attachment_name = StringProperty() server_mime = StringProperty() # Server detected MIME server_md5 = StringProperty() # Couch detected hash attachment_size = IntegerProperty() # file size attachment_properties = DictProperty( ) # width, height, other relevant metadata @property def content_type(self): return self.server_mime @property def is_present(self): """ Helper method to see if this is a delete vs. update NOTE this is related to but reversed logic from `casexml.apps.case.xml.parser.CaseAttachment.is_delete`. """ return self.attachment_src or self.attachment_from @classmethod def from_case_index_update(cls, attachment): if attachment.attachment_src or attachment.attachment_from: guessed = mimetypes.guess_type(attachment.attachment_src) if len(guessed) > 0 and guessed[0] is not None: mime_type = guessed[0] else: mime_type = None ret = cls(identifier=attachment.identifier, attachment_src=attachment.attachment_src, attachment_from=attachment.attachment_from, attachment_name=attachment.attachment_name, server_mime=mime_type) else: ret = cls(identifier=attachment.identifier) return ret
class HQMediaMapItem(DocumentSchema): multimedia_id = StringProperty() media_type = StringProperty() output_size = DictProperty() version = IntegerProperty() unique_id = StringProperty() @property def url(self): return reverse("hqmedia_download", args=[self.media_type, self.multimedia_id ]) if self.multimedia_id else "" @classmethod def gen_unique_id(cls, m_id, path): return hashlib.md5( b"%s: %s" % (path.encode('utf-8'), m_id.encode('utf-8'))).hexdigest()
class CustomReportConfiguration(JsonObject): """ For statically defined reports based off of custom data sources """ domains = ListProperty() report_id = StringProperty() data_source_table = StringProperty() config = DictProperty() @classmethod def get_doc_id(cls, domain, report_id): return '{}{}-{}'.format(CUSTOM_PREFIX, domain, report_id) @classmethod def all(cls): for path in settings.CUSTOM_UCR_REPORTS: with open(path) as f: wrapped = cls.wrap(json.load(f)) for domain in wrapped.domains: doc = copy(wrapped.config) doc['domain'] = domain doc['_id'] = cls.get_doc_id(domain, wrapped.report_id) doc['config_id'] = CustomDataSourceConfiguration.get_doc_id(domain, wrapped.data_source_table) yield ReportConfiguration.wrap(doc) @classmethod def by_domain(cls, domain): """ Returns a list of ReportConfiguration objects, NOT CustomReportConfigurations. """ return [ds for ds in cls.all() if ds.domain == domain] @classmethod def by_id(cls, config_id): """ Returns a ReportConfiguration object, NOT CustomReportConfigurations. """ for ds in cls.all(): if ds.get_id == config_id: return ds raise BadSpecError(_('The report configuration referenced by this report could ' 'not be found.'))
class CustomDataSourceConfiguration(JsonObject): """ For custom data sources maintained in the repository """ _datasource_id_prefix = CUSTOM_PREFIX domains = ListProperty() config = DictProperty() @classmethod def get_doc_id(cls, domain, table_id): return '{}{}-{}'.format(cls._datasource_id_prefix, domain, table_id) @classmethod def all(cls): for path in settings.CUSTOM_DATA_SOURCES: with open(path) as f: wrapped = cls.wrap(json.load(f)) for domain in wrapped.domains: doc = copy(wrapped.config) doc['domain'] = domain doc['_id'] = cls.get_doc_id(domain, doc['table_id']) yield DataSourceConfiguration.wrap(doc) @classmethod def by_domain(cls, domain): """ Returns a list of DataSourceConfiguration objects, NOT CustomDataSourceConfigurations. """ return [ds for ds in cls.all() if ds.domain == domain] @classmethod def by_id(cls, config_id): """ Returns a DataSourceConfiguration object, NOT a CustomDataSourceConfiguration. """ for ds in cls.all(): if ds.get_id == config_id: return ds raise BadSpecError(_('The data source referenced by this report could ' 'not be found.'))
class ComputedDocumentMixin(DocumentSchema): """ Use this mixin for things like CommCareCase or XFormInstance documents that take advantage of indicator definitions. computed_ is namespaced and may look like the following for indicators: computed_: { mvp_indicators: { indicator_slug: { version: 1, value: "foo" } } } """ computed_ = DictProperty() computed_modified_on_ = DateTimeProperty() # a flag for the indicator pillows so that there aren't any Document Update Conflicts initial_processing_complete = BooleanProperty()
class OpenmrsCaseConfig(DocumentSchema): # "patient_identifiers": { # "e2b966d0-1d5f-11e0-b929-000c29ad1d07": { # "case_property": "nid" # }, # "uuid": { # "case_property": "openmrs_uuid", # } # } patient_identifiers = DictProperty() # The patient_identifiers that are considered reliable # "match_on_ids": ["uuid", "e2b966d0-1d5f-11e0-b929-000c29ad1d07", match_on_ids = ListProperty() # "person_properties": { # "gender": { # "case_property": "gender" # }, # "birthdate": { # "case_property": "dob" # } # } person_properties = DictProperty() # "patient_finder": { # "doc_type": "WeightedPropertyPatientFinder", # "searchable_properties": ["nid", "family_name"], # "property_weights": [ # {"case_property": "nid", "weight": 0.9}, # // if "match_type" is not given it defaults to "exact" # {"case_property": "family_name", "weight": 0.4}, # { # "case_property": "given_name", # "weight": 0.3, # "match_type": "levenshtein", # // levenshtein function takes edit_distance / len # "match_params": [0.2] # // i.e. 0.2 (20%) is one edit for every 5 characters # // e.g. "Riyaz" matches "Riaz" but not "Riazz" # }, # {"case_property": "city", "weight": 0.2}, # { # "case_property": "dob", # "weight": 0.3, # "match_type": "days_diff", # // days_diff matches based on days difference from given date # "match_params": [364] # } # ] # } patient_finder = PatientFinder(required=False) # "person_preferred_name": { # "givenName": { # "case_property": "given_name" # }, # "middleName": { # "case_property": "middle_name" # }, # "familyName": { # "case_property": "family_name" # } # } person_preferred_name = DictProperty() # "person_preferred_address": { # "address1": { # "case_property": "address_1" # }, # "address2": { # "case_property": "address_2" # }, # "cityVillage": { # "case_property": "city" # } # } person_preferred_address = DictProperty() # "person_attributes": { # "c1f4239f-3f10-11e4-adec-0800271c1b75": { # "case_property": "caste" # }, # "c1f455e7-3f10-11e4-adec-0800271c1b75": { # "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" # } # } # } person_attributes = DictProperty() # Create cases when importing via the Atom feed import_creates_cases = BooleanProperty(default=True) # If we ever need to disable updating cases, ``import_updates_cases`` # could be added here. Similarly, we could replace # ``patient_finder.create_missing`` with ``export_creates_patients`` # and ``export_updates_patients`` @classmethod def wrap(cls, data): if 'id_matchers' in data: # Convert legacy id_matchers to patient_identifiers. e.g. # [{'doc_type': 'IdMatcher' # 'identifier_type_id': 'e2b966d0-1d5f-11e0-b929-000c29ad1d07', # 'case_property': 'nid'}] # to # {'e2b966d0-1d5f-11e0-b929-000c29ad1d07': {'doc_type': 'CaseProperty', 'case_property': 'nid'}}, patient_identifiers = { m['identifier_type_id']: { 'doc_type': 'CaseProperty', 'case_property': m['case_property'] } for m in data['id_matchers'] } data['patient_identifiers'] = patient_identifiers data['match_on_ids'] = list(patient_identifiers) data.pop('id_matchers') # Set default data types for known properties for property_, value_source in chain( data.get('person_properties', {}).items(), data.get('person_preferred_name', {}).items(), data.get('person_preferred_address', {}).items(), ): data_type = OPENMRS_PROPERTIES[property_] value_source.setdefault('external_data_type', data_type) return super(OpenmrsCaseConfig, cls).wrap(data)
class BlobMixin(Document): class Meta(object): abstract = True # TODO evaluate all uses of `external_blobs` external_blobs = DictProperty(BlobMetaRef) # When true, fallback to couch on fetch and delete if blob is not # found in blobdb. Set this to True on subclasses that are in the # process of being migrated. When this is false (the default) the # methods on this mixin will not touch couchdb. _migrating_blobs_from_couch = False _atomic_blobs = None @classmethod def wrap(cls, data): if data.get("external_blobs"): doc_id = safe_id(data["_id"]) dbname = _get_couchdb_name(cls) normalize = BlobMetaRef._normalize_json blobs = {} normalized = False for key, value in data["external_blobs"].items(): if value["doc_type"] == "BlobMetaRef": blobs[key] = value else: blobs[key] = normalize(dbname, data['_id'], value) normalized = True if normalized: data = data.copy() data["external_blobs"] = blobs return super(BlobMixin, cls).wrap(data) @classproperty def _blobdb_type_code(cls): """Blob DB type code This is an abstract attribute that must be set on non-abstract subclasses of `BlobMixin`. Its value should be one of the codes in `corehq.blobs.CODES`. """ raise NotImplementedError( "abstract class attribute %s._blobdb_type_code is missing" % cls.__name__ ) @property def blobs(self): """Get a dictionary of BlobMetaRef objects keyed by attachment name Includes CouchDB attachments if `_migrating_blobs_from_couch` is true. The returned value should not be mutated. """ if not self._migrating_blobs_from_couch or not self._attachments: return self.external_blobs value = {name: BlobMetaRef._from_attachment(info) for name, info in self._attachments.items()} value.update(self.external_blobs) return value @document_method def put_attachment(self, content, name=None, content_type=None, content_length=None, domain=None, type_code=None): """Put attachment in blob database See `get_short_identifier()` for restrictions on the upper bound for number of attachments per object. :param content: String or file object. """ db = get_blob_db() if name is None: name = getattr(content, "name", None) if name is None: raise InvalidAttachment("cannot save attachment without name") if self._id is None: raise ResourceNotFound("cannot put attachment on unidentified document") if hasattr(self, "domain"): if domain is not None and self.domain != domain: raise ValueError("domain mismatch: %s != %s" % (self.domain, domain)) domain = self.domain elif domain is None: raise ValueError("domain attribute or argument is required") old_meta = self.blobs.get(name) if isinstance(content, str): content = BytesIO(content.encode("utf-8")) elif isinstance(content, bytes): content = BytesIO(content) # do we need to worry about BlobDB reading beyond content_length? meta = db.put( content, domain=domain or self.domain, parent_id=self._id, name=name, type_code=(self._blobdb_type_code if type_code is None else type_code), content_type=content_type, ) self.external_blobs[name] = BlobMetaRef( key=meta.key, blobmeta_id=meta.id, content_type=content_type, content_length=meta.content_length, ) if self._migrating_blobs_from_couch and self._attachments: self._attachments.pop(name, None) if self._atomic_blobs is None: self.save() if old_meta and old_meta.key: db.delete(key=old_meta.key) elif old_meta and old_meta.key: self._atomic_blobs[name].append(old_meta.key) return True @document_method def fetch_attachment(self, name, stream=False): """Get named attachment :param stream: When true, return a file-like object that can be read at least once (streamers should not expect to seek within or read the contents of the returned file more than once). """ db = get_blob_db() try: try: key = self.external_blobs[name].key except KeyError: if self._migrating_blobs_from_couch: return super(BlobMixin, self) \ .fetch_attachment(name, stream=stream) raise NotFound(name) blob = db.get(key=key) except NotFound: raise ResourceNotFound( "{model} {model_id} attachment: {name!r}".format( model=type(self).__name__, model_id=self._id, name=name, )) if stream: return blob with blob: return blob.read() def has_attachment(self, name): return name in self.blobs def delete_attachment(self, name): if self._migrating_blobs_from_couch and self._attachments: deleted = bool(self._attachments.pop(name, None)) else: deleted = False meta = self.external_blobs.pop(name, None) if meta is not None: if self._atomic_blobs is None: deleted = get_blob_db().delete(key=meta.key) or deleted else: self._atomic_blobs[name].append(meta.key) deleted = True if self._atomic_blobs is None: self.save() return deleted @document_method def atomic_blobs(self, save=None): """Return a context manager to atomically save doc + blobs Usage:: with doc.atomic_blobs(): doc.put_attachment(...) # doc and blob are now saved Blobs saved inside the context manager will be deleted if an exception is raised inside the context body. :param save: A function to be called instead of `self.save()` """ @contextmanager def atomic_blobs_context(): if self._id is None: self._id = uuid.uuid4().hex old_external_blobs = dict(self.external_blobs) if self._migrating_blobs_from_couch: if self._attachments: old_attachments = dict(self._attachments) else: old_attachments = None atomicity = self._atomic_blobs self._atomic_blobs = new_deleted = defaultdict(list) db = get_blob_db() success = False try: yield (self.save if save is None else save)() success = True except: typ, exc, tb = sys.exc_info() # delete new blobs that were not saved for name, meta in self.external_blobs.items(): old_meta = old_external_blobs.get(name) if old_meta is None or meta.key != old_meta.key: db.delete(key=meta.key) self.external_blobs = old_external_blobs if self._migrating_blobs_from_couch: self._attachments = old_attachments six.reraise(typ, exc, tb) finally: self._atomic_blobs = atomicity if success: # delete replaced blobs deleted = set() blobs = self.blobs for name, meta in list(old_external_blobs.items()): if name not in blobs or meta.key != blobs[name].key: db.delete(key=meta.key) deleted.add(meta.key) # delete newly created blobs that were overwritten or deleted for key in chain.from_iterable(new_deleted.values()): if key not in deleted: db.delete(key=key) return atomic_blobs_context()