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 FixtureItemField(DocumentSchema): """ "field_value": "Delhi_IN_HIN", "properties": {"lang": "hin"} """ field_value = StringProperty() properties = DictProperty()
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(default=False) def update_indicator(self, indicator_def, save_on_update=True): existing_indicators = self.computed_.get(indicator_def.namespace, {}) updated_indicators, is_update = indicator_def.update_computed_namespace( existing_indicators, self) if is_update: self.computed_[indicator_def.namespace] = updated_indicators self.computed_modified_on_ = datetime.datetime.utcnow() if save_on_update: self.save() return is_update
class FixtureReportResult(Document, QueryMixin): domain = StringProperty() location_id = StringProperty() start_date = DateProperty() end_date = DateProperty() report_slug = StringProperty() rows = DictProperty() name = StringProperty() class Meta: 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 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 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 TranslationMixin(Document): translations = DictProperty() def init(self, lang): self.translations[lang] = Translation.get_translations(lang, one=True) 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 FixtureDataItem(Document): """ Example old Item: domain = "hq-domain" data_type_id = <id of state FixtureDataType> fields = { "country": "India", "state_name": "Delhi", "state_id": "DEL" } Example new Item with attributes: domain = "hq-domain" data_type_id = <id of state FixtureDataType> fields = { "country": {"field_list": [ {"field_value": "India", "properties": {}}, ]}, "state_name": {"field_list": [ {"field_value": "Delhi_IN_ENG", "properties": {"lang": "eng"}}, {"field_value": "Delhi_IN_HIN", "properties": {"lang": "hin"}}, ]}, "state_id": {"field_list": [ {"field_value": "DEL", "properties": {}} ]} } If one of field's 'properties' is an empty 'dict', the field has no attributes """ domain = StringProperty() data_type_id = StringProperty() fields = DictProperty(FieldList) item_attributes = DictProperty() sort_key = IntegerProperty() @classmethod def wrap(cls, obj): if not obj["doc_type"] == "FixtureDataItem": raise ResourceNotFound if not obj["fields"]: return super(FixtureDataItem, cls).wrap(obj) # Migrate old basic fields to fields with attributes is_of_new_type = False fields_dict = {} def _is_new_type(field_val): old_types = (basestring, int, float) return field_val is not None and not isinstance(field_val, old_types) for field in obj['fields']: field_val = obj['fields'][field] if _is_new_type(field_val): # assumes all-or-nothing conversion of old types to new is_of_new_type = True break fields_dict[field] = { "field_list": [{ 'field_value': str(field_val) if not isinstance(field_val, basestring) else field_val, 'properties': {} }] } if not is_of_new_type: obj['fields'] = fields_dict # Migrate fixture-items to have attributes if 'item_attributes' not in obj: obj['item_attributes'] = {} return super(FixtureDataItem, cls).wrap(obj) @property def fields_without_attributes(self): fields = {} for field in self.fields: # if the field has properties, a unique field_val can't be generated for FixtureItem if len(self.fields[field].field_list) > 1: raise FixtureVersionError("This method is not supported for fields with properties." " field '%s' has properties" % field) fields[field] = self.fields[field].field_list[0].field_value return fields @property def try_fields_without_attributes(self): """This is really just for the API""" try: return self.fields_without_attributes except FixtureVersionError: return {key: value.to_api_json() for key, value in self.fields.items()} @property def data_type(self): if not hasattr(self, '_data_type'): self._data_type = FixtureDataType.get(self.data_type_id) return self._data_type def add_owner(self, owner, owner_type, transaction=None): assert(owner.domain == self.domain) with transaction or CouchTransaction() as transaction: o = FixtureOwnership(domain=self.domain, owner_type=owner_type, owner_id=owner.get_id, data_item_id=self.get_id) transaction.save(o) return o def remove_owner(self, owner, owner_type): for ownership in FixtureOwnership.view('fixtures/ownership', key=[self.domain, 'by data_item and ' + owner_type, self.get_id, owner.get_id], reduce=False, include_docs=True ): try: ownership.delete() except ResourceNotFound: # looks like it was already deleted pass except ResourceConflict: raise FixtureException(( "couldn't remove ownership {owner_id} for item {fixture_id} of type " "{data_type_id} in domain {domain}. It was updated elsewhere" ).format( owner_id=ownership._id, fixture_id=self._id, data_type_id=self.data_type_id, domain=self.domain )) def add_user(self, user, transaction=None): return self.add_owner(user, 'user', transaction=transaction) def remove_user(self, user): return self.remove_owner(user, 'user') def add_group(self, group, transaction=None): return self.add_owner(group, 'group', transaction=transaction) def remove_group(self, group): return self.remove_owner(group, 'group') def type_check(self): fields = set(self.fields.keys()) for field in self.data_type.fields: if field.field_name in fields: fields.remove(field) else: raise FixtureTypeCheckError("field %s not in fixture data %s" % (field.field_name, self.get_id)) if fields: raise FixtureTypeCheckError("fields %s from fixture data %s not in fixture data type" % (', '.join(fields), self.get_id)) def to_xml(self): def _serialize(val): return unicode(val) if isinstance(val, (int, Decimal)) else val xData = ElementTree.Element(self.data_type.tag) for attribute in self.data_type.item_attributes: try: xData.attrib[attribute] = _serialize(self.item_attributes[attribute]) except KeyError as e: # This should never occur, buf if it does, the OTA restore on mobile will fail and # this error would have been raised and email-logged. raise FixtureTypeCheckError( "Table with tag %s has an item with id %s that doesn't have an attribute as defined in its types definition" % (self.data_type.tag, self.get_id) ) for field in self.data_type.fields: if not self.fields.has_key(field.field_name): xField = ElementTree.SubElement(xData, field.field_name) xField.text = "" else: for field_with_attr in self.fields[field.field_name].field_list: xField = ElementTree.SubElement(xData, field.field_name) xField.text = field_with_attr.field_value or "" for attribute in field_with_attr.properties: val = field_with_attr.properties[attribute] xField.attrib[attribute] = _serialize(val) return xData def get_groups(self, wrap=True): group_ids = set( get_db().view('fixtures/ownership', key=[self.domain, 'group by data_item', self.get_id], reduce=False, wrapper=lambda r: r['value'] ) ) if wrap: return set(Group.view('_all_docs', keys=list(group_ids), include_docs=True)) else: return group_ids @property @memoized def groups(self): return self.get_groups() def get_users(self, wrap=True, include_groups=False): user_ids = set( get_db().view('fixtures/ownership', key=[self.domain, 'user by data_item', self.get_id], reduce=False, wrapper=lambda r: r['value'] ) ) if include_groups: group_ids = self.get_groups(wrap=False) else: group_ids = set() users_in_groups = [group.get_users(only_commcare=True) for group in Group.view('_all_docs', keys=list(group_ids), include_docs=True )] if wrap: return set(CommCareUser.view('_all_docs', keys=list(user_ids), include_docs=True)).union(*users_in_groups) else: return user_ids | set([user.get_id for user in users_in_groups]) def get_all_users(self, wrap=True): return self.get_users(wrap=wrap, include_groups=True) @property @memoized def users(self): return self.get_users() @classmethod def by_user(cls, user, wrap=True, domain=None): group_ids = Group.by_user(user, wrap=False) if isinstance(user, dict): user_id = user.get('user_id') user_domain = domain else: user_id = user.user_id user_domain = user.domain fixture_ids = set( FixtureOwnership.get_db().view('fixtures/ownership', keys=[[user_domain, 'data_item by user', user_id]] + [[user_domain, 'data_item by group', group_id] for group_id in group_ids], reduce=False, wrapper=lambda r: r['value'], ) ) if wrap: results = cls.get_db().view('_all_docs', keys=list(fixture_ids), include_docs=True) # sort the results into those corresponding to real documents # and those corresponding to deleted or non-existent documents docs = [] deleted_fixture_ids = set() for result in results: if result.get('doc'): docs.append(cls.wrap(result['doc'])) elif result.get('error'): assert result['error'] == 'not_found' deleted_fixture_ids.add(result['key']) else: assert result['value']['deleted'] is True deleted_fixture_ids.add(result['id']) # fetch and delete ownership documents pointing # to deleted or non-existent fixture documents # this cleanup is necessary since we used to not do this bad_ownerships = FixtureOwnership.for_all_item_ids(deleted_fixture_ids, user_domain) FixtureOwnership.get_db().bulk_delete(bad_ownerships) return docs else: return fixture_ids @classmethod def by_group(cls, group, wrap=True): fixture_ids = get_db().view('fixtures/ownership', key=[group.domain, 'data_item by group', group.get_id], reduce=False, wrapper=lambda r: r['value'], descending=True ).all() return cls.view('_all_docs', keys=list(fixture_ids), include_docs=True) if wrap else fixture_ids @classmethod def by_data_type(cls, domain, data_type): data_type_id = _id_from_doc(data_type) return cls.view('fixtures/data_items_by_domain_type', key=[domain, data_type_id], reduce=False, include_docs=True, descending=True) @classmethod def by_domain(cls, domain): return cls.view('fixtures/data_items_by_domain_type', startkey=[domain, {}], endkey=[domain], reduce=False, include_docs=True, descending=True ) @classmethod def by_field_value(cls, domain, data_type, field_name, field_value): data_type_id = _id_from_doc(data_type) return cls.view('fixtures/data_items_by_field_value', key=[domain, data_type_id, field_name, field_value], reduce=False, include_docs=True) @classmethod def get_item_list(cls, domain, tag): data_type = FixtureDataType.by_domain_tag(domain, tag).one() return cls.by_data_type(domain, data_type).all() @classmethod def get_indexed_items(cls, domain, tag, index_field): """ Looks up an item list and converts to mapping from `index_field` to a dict of all fields for that item. fixtures = FixtureDataItem.get_indexed_items('my_domain', 'item_list_tag', 'index_field') result = fixtures['index_val']['result_field'] """ fixtures = cls.get_item_list(domain, tag) return dict((f.fields_without_attributes[index_field], f.fields_without_attributes) for f in fixtures) def delete_ownerships(self, transaction): ownerships = FixtureOwnership.by_item_id(self.get_id, self.domain) transaction.delete_all(ownerships) def recursive_delete(self, transaction): self.delete_ownerships(transaction) transaction.delete(self)
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(default=False) def update_indicator(self, indicator_def, save_on_update=True, logger=None): existing_indicators = self.computed_.get(indicator_def.namespace, {}) updated_indicators, is_update = indicator_def.update_computed_namespace( existing_indicators, self) if is_update: self.computed_[indicator_def.namespace] = updated_indicators self.computed_modified_on_ = datetime.datetime.utcnow() if logger: logger.info( "[INDICATOR %(namespace)s %(domain)s] Updating %(indicator_type)s:%(indicator_slug)s " "in %(document_type)s [%(document_id)s]." % { 'namespace': indicator_def.namespace, 'domain': indicator_def.domain, 'indicator_type': indicator_def.__class__.__name__, 'indicator_slug': indicator_def.slug, 'document_type': self.__class__.__name__, 'document_id': self._id, }) if save_on_update: self.save(**get_safe_write_kwargs()) if logger: logger.debug("Saved %s." % self._id) return is_update def update_indicators_in_bulk(self, indicators, save_on_update=True, logger=None): is_update = False for indicator in indicators: try: if self.update_indicator(indicator, save_on_update=False, logger=logger): is_update = True except Exception: logger.exception( "[INDICATOR %(namespace)s %(domain)s] Failed to update %(indicator_type)s: " "%(indicator_slug)s in %(document_type)s [%(document_id)s]." % { 'namespace': indicator.namespace, 'domain': indicator.domain, 'indicator_type': indicator.__class__.__name__, 'indicator_slug': indicator.slug, 'document_type': self.__class__.__name__, 'document_id': self._id, }) if is_update and save_on_update: try: self.save(**get_safe_write_kwargs()) if logger: logger.info("Saved %s." % self._id) except ResourceConflict: logger.error( "[INDICATOR %(domain)s] Resource conflict failed to save document indicators for " "%(document_type)s [%(document_id)s]." % { 'domain': self.domain, 'document_type': self.__class__.__name__, 'document_id': self._id, }) return is_update
class CouchDocument(Document): """Main CouchDB document format model Usually a representation of DMS Document() stored in CouchDB""" id = StringProperty() metadata_doc_type_rule_id = StringProperty(default="") metadata_user_id = StringProperty(default="") metadata_user_name = StringProperty(default="") metadata_created_date = DateTimeProperty(default=datetime.utcnow()) metadata_description = StringProperty(default="") tags = ListProperty(default=[]) mdt_indexes = DictProperty(default={}) search_keywords = ListProperty(default=[]) revisions = DictProperty(default={}) index_revisions = DictProperty(default={}) class Meta: app_label = "dmscouch" def populate_from_dms(self, user, document): """Populates CouchDB Document fields from DMS Document object. @param user: django internal User() object instance @param document: DMS Document() instance""" # Setting document ID, based on filename. Using stripped (pure docrule regex readable) filename if possible. if document.get_code(): self.id = document.get_code() self._doc['_id'] = self.id self.metadata_doc_type_rule_id = str(document.docrule.pk) # setting provided user name/id if "metadata_user_name" in document.db_info and "metadata_user_id" in document.db_info: self.metadata_user_name = document.db_info["metadata_user_name"] self.metadata_user_id = document.db_info["metadata_user_id"] else: self.set_user_name_for_couch(user) self.set_doc_date(document) # adding description if exists if "description" in document.db_info: self.metadata_description = document.db_info["description"] else: self.metadata_description = "" self.tags = document.tags # populating secondary indexes if document.db_info: db_info = document.db_info # trying to cleanup irrelevant fields if exist... # (Bug #829 Files Secondary indexes contain username and user PK) del_keys = [] for key in db_info: if key in [ "date", "description", "metadata_user_name", "metadata_user_id", "mdt_indexes", "metadata_created_date", "metadata_doc_type_rule_id", "tags", ]: del_keys.append(key) for key in del_keys: del db_info[key] self.mdt_indexes = db_info self.search_keywords = [] # TODO: not implemented yet self.revisions = document.get_file_revisions_data() if document.index_revisions: self.index_revisions = document.index_revisions def populate_into_dms(self, document): """Updates DMS Document object with CouchDB fields data. @param document: DMS Document() instance""" document.set_file_revisions_data(self.revisions) if self.tags: document.tags = self.tags document.code = self.id document.db_info = self.construct_db_info() if 'index_revisions' in self: document.index_revisions = self.index_revisions if 'deleted' in self: if self['deleted'] == 'deleted': document.marked_deleted = True return document def construct_db_info(self, db_info=None): """Method to populate additional database info from CouchDB into DMS Document object. @param db_info: a set of CouchDB metadata info, extracted from CouchDB document""" if not db_info: db_info = {} db_info["description"] = self.metadata_description db_info["tags"] = self.tags db_info["metadata_doc_type_rule_id"] = self.metadata_doc_type_rule_id db_info["metadata_user_id"] = self.metadata_user_id db_info["metadata_user_name"] = self.metadata_user_name db_info["metadata_created_date"] = self.metadata_created_date db_info["mdt_indexes"] = self.mdt_indexes return db_info def construct_index_revision_dict(self, old_couchdoc_id=False): """Constructs current indexes revision and export into result dict @param old_couchdoc_id: either to include or not old document name into metadata list mus contain old couchdoc name""" current_index_data = { 'metadata_created_date': self.metadata_created_date, 'metadata_description': self.metadata_description, 'metadata_user_id': self.metadata_user_id, 'metadata_user_name': self.metadata_user_name, 'mdt_indexes': self.mdt_indexes, } if old_couchdoc_id: current_index_data['metadata_old_id'] = old_couchdoc_id return current_index_data def set_doc_date(self, document): """Unifies DB storage of date object received from document. @param document: DMS Document() instance""" doc_date = None # trying to get date from db_info dict first if 'date' in document.db_info: doc_date = datetime.strptime(str(document.db_info["date"]), settings.DATE_FORMAT) if not doc_date and document.revision: # Setting document current revision metadata date, except not exists using now() instead. revision = unicode(document.revision) if revision in document.file_revision_data: rev_dict = document.file_revision_data[revision] if 'created_date' in rev_dict[revision]: tmp_date = rev_dict[revision][u'created_date'] doc_date = datetime.strptime(tmp_date, "%Y-%m-%d %H:%M:%S") if not doc_date: doc_date = datetime.utcnow() self.metadata_created_date = doc_date def update_indexes_revision(self, document): """Updates CouchDB document with new revision of indexing data. @param document: DMS Document() instance Old indexing data is stored in revision. E.g.: Document only created: couchdoc.index_revisions = None Document updated once: couchdoc.index_revisions = { '1': { ... }, } Document updated again and farther: couchdoc.index_revisions = { '1': { ... }, '2': { ... }, ... } This method handles storing of indexing data changes (old one's) are stored into revisions. New data are populated into couchdoc thus making them current. """ if document.new_indexes: # Creating clean self.mdt_indexes secondary_indexes = {} for secondary_index_name, secondary_index_value in document.new_indexes.iteritems( ): if not secondary_index_name in [ 'description', 'metadata_user_name', 'metadata_user_id', ]: # Converting date format to couch if secondary index is DMS date type try: datetime.strptime(secondary_index_value, settings.DATE_FORMAT) secondary_indexes[ secondary_index_name] = str_date_to_couch( secondary_index_value) except ValueError: secondary_indexes[ secondary_index_name] = secondary_index_value pass # Only for update without docrule change (it makes it's own indexes backup) if not document.old_docrule: # Storing current index data into new revision if not 'index_revisions' in self: # Creating index_revisions initial data dictionary. self.index_revisions = { '1': self.construct_index_revision_dict(), } else: # Appending new document indexes revision to revisions dict new_revision = self.index_revisions.__len__() + 1 self.index_revisions[ new_revision] = self.construct_index_revision_dict() # Populating self with new provided data self.mdt_indexes = secondary_indexes # Making desc and user data optional, taking them from current user if 'description' in document.new_indexes: self.metadata_description = document.new_indexes['description'] else: self.metadata_description = 'N/A' if 'metadata_user_id' in document.new_indexes: self.metadata_user_id = document.new_indexes[ 'metadata_user_id'] else: self.metadata_user_id = unicode(document.user.id) if 'metadata_user_name' in document.new_indexes: self.metadata_user_id = document.new_indexes[ 'metadata_user_name'] else: self.metadata_user_name = document.user.username return document def update_file_revisions_metadata(self, document): """ Stores files revision data into CouchDB from DMS document object @param document: DMS Document() instance E.g.: Before this function: couchdoc.revisions = { '1': { ... }, } After: couchdoc.revisions = { '1': { ... }, '2': { ... }, } (Loaded from a Document() object) """ self.revisions = document.get_file_revisions_data() def migrate_metadata_for_docrule(self, document, old_couchdoc): """Moving a CouchDB document into another file @param document: DMS Document() instance @param old_couchdoc: CouchDocument instance""" if not old_couchdoc.index_revisions: # Creating index_revisions initial data dictionary. self.index_revisions = { '1': old_couchdoc.construct_index_revision_dict(old_couchdoc.id), } else: self.index_revisions = old_couchdoc.index_revisions # Appending new document indexes revision to revisions dict new_revision = self.index_revisions.__len__() + 1 self.index_revisions[str( new_revision)] = old_couchdoc.construct_index_revision_dict( old_couchdoc.id) self.revisions = document.get_file_revisions_data() self.metadata_description = old_couchdoc.metadata_description if document.user: self.set_user_name_for_couch(document.user) else: self.metadata_user_id = old_couchdoc.metadata_user_id self.metadata_user_name = old_couchdoc.metadata_user_name self.metadata_description = old_couchdoc.metadata_description self.metadata_created_date = old_couchdoc.metadata_created_date self.search_keywords = old_couchdoc.search_keywords self.tags = old_couchdoc.tags self.metadata_doc_type_rule_id = str(document.docrule.pk) self.id = document.get_filename() def set_user_name_for_couch(self, user): """ user name/id from Django user @param user: django internal User() object instance""" self.metadata_user_id = str(user.pk) if user.first_name: self.metadata_user_name = user.first_name + u' ' + user.last_name else: self.metadata_user_name = user.username
class Domain(Document, HQBillingDomainMixin, SnapshotMixin): """Domain is the highest level collection of people/stuff in the system. Pretty much everything happens at the domain-level, including user membership, permission to see data, reports, charts, etc.""" name = StringProperty() is_active = BooleanProperty() is_public = BooleanProperty(default=False) date_created = DateTimeProperty() default_timezone = StringProperty(default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) organization = StringProperty() hr_name = StringProperty() # the human-readable name for this project within an organization eula = SchemaProperty(LicenseAgreement) creating_user = StringProperty() # username of the user who created this domain # domain metadata project_type = StringProperty() # e.g. MCH, HIV customer_type = StringProperty() # plus, full, etc. is_test = BooleanProperty(default=True) description = StringProperty() short_description = StringProperty() is_shared = BooleanProperty(default=False) commtrack_enabled = BooleanProperty(default=False) call_center_config = SchemaProperty(CallCenterProperties) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings survey_management_enabled = BooleanProperty(default=False) sms_case_registration_enabled = BooleanProperty(default=False) # Whether or not a case can register via sms sms_case_registration_type = StringProperty() # Case type to apply to cases registered via sms sms_case_registration_owner_id = StringProperty() # Owner to apply to cases registered via sms sms_case_registration_user_id = StringProperty() # Submitting user to apply to cases registered via sms sms_mobile_worker_registration_enabled = BooleanProperty(default=False) # Whether or not a mobile worker can register via sms default_sms_backend_id = StringProperty() # exchange/domain copying stuff is_snapshot = BooleanProperty(default=False) is_approved = BooleanProperty(default=False) snapshot_time = DateTimeProperty() published = BooleanProperty(default=False) license = StringProperty(choices=LICENSES, default='cc') title = StringProperty() cda = SchemaProperty(LicenseAgreement) multimedia_included = BooleanProperty(default=True) downloads = IntegerProperty(default=0) author = StringProperty() phone_model = StringProperty() attribution_notes = StringProperty() publisher = StringProperty(choices=["organization", "user"], default="user") yt_id = StringProperty() deployment = SchemaProperty(Deployment) image_path = StringProperty() image_type = StringProperty() migrations = SchemaProperty(DomainMigrations) cached_properties = DictProperty() internal = SchemaProperty(InternalProperties) # extra user specified properties tags = StringListProperty() area = StringProperty(choices=AREA_CHOICES) sub_area = StringProperty(choices=SUB_AREA_CHOICES) launch_date = DateTimeProperty # to be eliminated from projects and related documents when they are copied for the exchange _dirty_fields = ('admin_password', 'admin_password_charset', 'city', 'country', 'region', 'customer_type') @classmethod def wrap(cls, data): # for domains that still use original_doc should_save = False if 'original_doc' in data: original_doc = data['original_doc'] del data['original_doc'] should_save = True if original_doc: original_doc = Domain.get_by_name(original_doc) data['copy_history'] = [original_doc._id] # for domains that have a public domain license if 'license' in data: if data.get("license", None) == "public": data["license"] = "cc" should_save = True # if not 'creating_user' in data: # should_save = True # from corehq.apps.users.models import CouchUser # admins = CouchUser.view("users/admins_by_domain", key=data["name"], reduce=False, include_docs=True).all() # if len(admins) == 1: # data["creating_user"] = admins[0].username # else: # data["creating_user"] = None if 'slug' in data and data["slug"]: data["hr_name"] = data["slug"] del data["slug"] self = super(Domain, cls).wrap(data) if self.get_id: self.apply_migrations() if should_save: self.save() return self @staticmethod def active_for_user(user, is_active=True): if isinstance(user, AnonymousUser): return [] from corehq.apps.users.models import CouchUser if isinstance(user, CouchUser): couch_user = user else: couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return Domain.view("domain/by_status", keys=[[is_active, d] for d in domain_names], reduce=False, include_docs=True, stale=settings.COUCH_STALE_QUERY, ).all() else: return [] @classmethod def field_by_prefix(cls, field, prefix='', is_approved=True): # unichr(0xfff8) is something close to the highest character available res = cls.view("domain/fields_by_prefix", group=True, startkey=[field, is_approved, prefix], endkey=[field, is_approved, "%s%c" % (prefix, unichr(0xfff8)), {}]) vals = [(d['value'], d['key'][2]) for d in res] vals.sort(reverse=True) return [(v[1], v[0]) for v in vals] @classmethod def get_by_field(cls, field, value, is_approved=True): return cls.view('domain/fields_by_prefix', key=[field, is_approved, value], reduce=False, include_docs=True).all() def apply_migrations(self): self.migrations.apply(self) @staticmethod def all_for_user(user): if not hasattr(user,'get_profile'): # this had better be an anonymous user return [] from corehq.apps.users.models import CouchUser couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return Domain.view("domain/domains", keys=domain_names, reduce=False, include_docs=True).all() else: return [] def add(self, model_instance, is_active=True): """ Add something to this domain, through the generic relation. Returns the created membership object """ # Add membership info to Couch couch_user = model_instance.get_profile().get_couch_user() couch_user.add_domain_membership(self.name) couch_user.save() def applications(self): from corehq.apps.app_manager.models import ApplicationBase return ApplicationBase.view('app_manager/applications_brief', startkey=[self.name], endkey=[self.name, {}]).all() def full_applications(self, include_builds=True): from corehq.apps.app_manager.models import Application, RemoteApp WRAPPERS = {'Application': Application, 'RemoteApp': RemoteApp} def wrap_application(a): return WRAPPERS[a['doc']['doc_type']].wrap(a['doc']) if include_builds: startkey = [self.name] endkey = [self.name, {}] else: startkey = [self.name, None] endkey = [self.name, None, {}] return get_db().view('app_manager/applications', startkey=startkey, endkey=endkey, include_docs=True, wrapper=wrap_application).all() @cached_property def versions(self): apps = self.applications() return list(set(a.application_version for a in apps)) @cached_property def has_case_management(self): for app in self.full_applications(): if app.doc_type == 'Application': if app.has_case_management(): return True return False @cached_property def has_media(self): for app in self.full_applications(): if app.doc_type == 'Application' and app.has_media(): return True return False def all_users(self): from corehq.apps.users.models import CouchUser return CouchUser.by_domain(self.name) def has_shared_media(self): return False def recent_submissions(self): from corehq.apps.reports.util import make_form_couch_key key = make_form_couch_key(self.name) res = get_db().view('reports_forms/all_forms', startkey=key+[{}], endkey=key, descending=True, reduce=False, include_docs=False, limit=1).all() if len(res) > 0: # if there have been any submissions in the past 30 days return (datetime.now() <= datetime.strptime(res[0]['value']['submission_time'], "%Y-%m-%dT%H:%M:%SZ") + timedelta(days=30)) else: return False @cached_property def languages(self): apps = self.applications() return set(chain.from_iterable([a.langs for a in apps])) def readable_languages(self): return ', '.join(lang_lookup[lang] or lang for lang in self.languages()) def __unicode__(self): return self.name @classmethod def get_by_name(cls, name, strict=False): if not name: # get_by_name should never be called with name as None (or '', etc) # I fixed the code in such a way that if I raise a ValueError # all tests pass and basic pages load, # but in order not to break anything in the wild, # I'm opting to notify by email if/when this happens # but fall back to the previous behavior of returning None try: raise ValueError('%r is not a valid domain name' % name) except ValueError: if settings.DEBUG: raise else: notify_exception(None, '%r is not a valid domain name' % name) return None extra_args = {'stale': settings.COUCH_STALE_QUERY} if not strict else {} result = cls.view("domain/domains", key=name, reduce=False, include_docs=True, **extra_args ).first() if result is None and not strict: # on the off chance this is a brand new domain, try with strict return cls.get_by_name(name, strict=True) return result @classmethod def get_by_organization(cls, organization): result = cls.view("domain/by_organization", startkey=[organization], endkey=[organization, {}], reduce=False, include_docs=True) return result @classmethod def get_by_organization_and_hrname(cls, organization, hr_name): result = cls.view("domain/by_organization", key=[organization, hr_name], reduce=False, include_docs=True) return result @classmethod def get_or_create_with_name(cls, name, is_active=False): result = cls.view("domain/domains", key=name, reduce=False, include_docs=True).first() if result: return result else: new_domain = Domain(name=name, is_active=is_active, date_created=datetime.utcnow()) new_domain.save(**get_safe_write_kwargs()) return new_domain def password_format(self): """ This was a performance hit, so for now we'll just return 'a' no matter what # If a single application is alphanumeric, return alphanumeric; otherwise, return numeric """ # for app in self.full_applications(): # if hasattr(app, 'profile'): # format = app.profile.get('properties', {}).get('password_format', 'n') # if format == 'a': # return 'a' # return 'n' return 'a' @classmethod def get_all(cls, include_docs=True): return Domain.view("domain/not_snapshots", include_docs=include_docs).all() def case_sharing_included(self): return self.case_sharing or reduce(lambda x, y: x or y, [getattr(app, 'case_sharing', False) for app in self.applications()], False) def save_copy(self, new_domain_name=None, user=None): from corehq.apps.app_manager.models import get_app if new_domain_name is not None and Domain.get_by_name(new_domain_name): return None db = get_db() new_id = db.copy_doc(self.get_id)['id'] if new_domain_name is None: new_domain_name = new_id new_domain = Domain.get(new_id) new_domain.name = new_domain_name new_domain.copy_history = self.get_updated_history() new_domain.is_snapshot = False new_domain.snapshot_time = None new_domain.organization = None # TODO: use current user's organization (?) # reset the cda new_domain.cda.signed = False new_domain.cda.date = None new_domain.cda.type = None new_domain.cda.user_id = None new_domain.cda.user_ip = None for field in self._dirty_fields: if hasattr(new_domain, field): delattr(new_domain, field) for res in db.view('domain/related_to_domain', key=[self.name, True]): if not self.is_snapshot and res['value']['doc_type'] in ('Application', 'RemoteApp'): app = get_app(self.name, res['value']['_id']).get_latest_saved() if app: self.copy_component(app.doc_type, app._id, new_domain_name, user=user) else: self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) else: self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) new_domain.save() if user: def add_dom_to_user(user): user.add_domain_membership(new_domain_name, is_admin=True) apply_update(user, add_dom_to_user) return new_domain def copy_component(self, doc_type, id, new_domain_name, user=None): from corehq.apps.app_manager.models import import_app from corehq.apps.users.models import UserRole str_to_cls = { 'UserRole': UserRole, } db = get_db() if doc_type in ('Application', 'RemoteApp'): new_doc = import_app(id, new_domain_name) new_doc.copy_history.append(id) else: cls = str_to_cls[doc_type] new_id = db.copy_doc(id)['id'] new_doc = cls.get(new_id) for field in self._dirty_fields: if hasattr(new_doc, field): delattr(new_doc, field) if hasattr(cls, '_meta_fields'): for field in cls._meta_fields: if not field.startswith('_') and hasattr(new_doc, field): delattr(new_doc, field) new_doc.domain = new_domain_name if self.is_snapshot and doc_type == 'Application': new_doc.prepare_multimedia_for_exchange() new_doc.save() return new_doc def save_snapshot(self): if self.is_snapshot: return self else: copy = self.save_copy() if copy is None: return None copy.is_snapshot = True copy.organization = self.organization # i don't think we want this? copy.snapshot_time = datetime.now() del copy.deployment copy.save() return copy def from_snapshot(self): return not self.is_snapshot and self.original_doc is not None def snapshots(self): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, descending=True) @memoized def published_snapshot(self): snapshots = self.snapshots().all() for snapshot in snapshots: if snapshot.published: return snapshot return None @classmethod def published_snapshots(cls, include_unapproved=False, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page if include_unapproved: return cls.view('domain/published_snapshots', startkey=[False, {}], include_docs=True, descending=True, limit=limit, skip=skip) else: return cls.view('domain/published_snapshots', endkey=[True], include_docs=True, descending=True, limit=limit, skip=skip) @classmethod def snapshot_search(cls, query, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page results = get_db().search('domain/snapshot_search', q=json.dumps(query), limit=limit, skip=skip, stale='ok') return map(cls.get, [r['id'] for r in results]), results.total_rows @memoized def get_organization(self): from corehq.apps.orgs.models import Organization return Organization.get_by_name(self.organization) @memoized def organization_title(self): if self.organization: return self.get_organization().title else: return '' def update_deployment(self, **kwargs): self.deployment.update(kwargs) self.save() def update_internal(self, **kwargs): self.internal.update(kwargs) self.save() def display_name(self): if self.is_snapshot: return "Snapshot of %s" % self.copied_from.display_name() if self.hr_name and self.organization: return self.hr_name else: return self.name def long_display_name(self): if self.is_snapshot: return format_html( "Snapshot of {0} > {1}", self.get_organization().title, self.copied_from.display_name() ) if self.organization: return format_html( '{0} > {1}', self.get_organization().title, self.hr_name or self.name ) else: return self.name __str__ = long_display_name def get_license_display(self): return LICENSES.get(self.license) def copies(self): return Domain.view('domain/copied_from_snapshot', key=self._id, include_docs=True) def copies_of_parent(self): return Domain.view('domain/copied_from_snapshot', keys=[s._id for s in self.copied_from.snapshots()], include_docs=True) def delete(self): # delete all associated objects db = get_db() related_docs = db.view('domain/related_to_domain', startkey=[self.name], endkey=[self.name, {}], include_docs=True) for doc in related_docs: db.delete_doc(doc['doc']) super(Domain, self).delete() def all_media(self, from_apps=None): #todo add documentation or refactor from corehq.apps.hqmedia.models import CommCareMultimedia dom_with_media = self if not self.is_snapshot else self.copied_from if self.is_snapshot: app_ids = [app.copied_from.get_id for app in self.full_applications()] if from_apps: from_apps = set([a_id for a_id in app_ids if a_id in from_apps]) else: from_apps = app_ids if from_apps: media = [] media_ids = set() apps = [app for app in dom_with_media.full_applications() if app.get_id in from_apps] for app in apps: for _, m in app.get_media_objects(): if m.get_id not in media_ids: media.append(m) media_ids.add(m.get_id) return media return CommCareMultimedia.view('hqmedia/by_domain', key=dom_with_media.name, include_docs=True).all() def most_restrictive_licenses(self, apps_to_check=None): from corehq.apps.hqmedia.utils import most_restrictive licenses = [m.license['type'] for m in self.all_media(from_apps=apps_to_check) if m.license] return most_restrictive(licenses) @classmethod def popular_sort(cls, domains): sorted_list = [] MIN_REVIEWS = 1.0 domains = [(domain, Review.get_average_rating_by_app(domain.copied_from._id), Review.get_num_ratings_by_app(domain.copied_from._id)) for domain in domains] domains = [(domain, avg or 0.0, num or 0) for domain, avg, num in domains] total_average_sum = sum(avg for domain, avg, num in domains) total_average_count = len(domains) if not total_average_count: return [] total_average = (total_average_sum / total_average_count) for domain, average_rating, num_ratings in domains: if num_ratings == 0: sorted_list.append((0.0, domain)) else: weighted_rating = ((num_ratings / (num_ratings + MIN_REVIEWS)) * average_rating + (MIN_REVIEWS / (num_ratings + MIN_REVIEWS)) * total_average) sorted_list.append((weighted_rating, domain)) sorted_list = [domain for weighted_rating, domain in sorted(sorted_list, key=lambda domain: domain[0], reverse=True)] return sorted_list @classmethod def hit_sort(cls, domains): domains = list(domains) domains = sorted(domains, key=lambda domain: domain.downloads, reverse=True) return domains @classmethod def public_deployments(cls): return Domain.view('domain/with_deployment', include_docs=True).all() @classmethod def get_module_by_name(cls, domain_name): """ import and return the python module corresponding to domain_name, or None if it doesn't exist. """ module_name = get_domain_module_map().get(domain_name, domain_name) try: return __import__(module_name) if module_name else None except ImportError: return None @property def commtrack_settings(self): # this import causes some dependency issues so lives in here from corehq.apps.commtrack.models import CommtrackConfig if self.commtrack_enabled: return CommtrackConfig.for_domain(self.name) else: return None def get_case_display(self, case): """Get the properties display definition for a given case""" return self.case_display.case_details.get(case.type) def get_form_display(self, form): """Get the properties display definition for a given XFormInstance""" return self.case_display.form_details.get(form.xmlns)
class CasePropertySpec(DocumentSchema): key = StringProperty() label = DictProperty() type = StringProperty(choices=['string', 'select', 'date', 'group'], default='string') choices = SchemaListProperty(SelectChoice)
class DynamicReportConfig(DocumentSchema): """configurations of generic/template reports to be set up for this domain""" report = StringProperty() # fully-qualified path to template report class name = StringProperty() # report display name in sidebar kwargs = DictProperty() # arbitrary settings to configure report previewers_only = BooleanProperty()
class FixtureDataItem(Document): domain = StringProperty() data_type_id = StringProperty() fields = DictProperty() @property def data_type(self): if not hasattr(self, '_data_type'): self._data_type = FixtureDataType.get(self.data_type_id) return self._data_type def add_owner(self, owner, owner_type, transaction=None): assert (owner.domain == self.domain) with transaction or CouchTransaction() as transaction: o = FixtureOwnership(domain=self.domain, owner_type=owner_type, owner_id=owner.get_id, data_item_id=self.get_id) transaction.save(o) return o def remove_owner(self, owner, owner_type): for ownership in FixtureOwnership.view( 'fixtures/ownership', key=[ self.domain, 'by data_item and ' + owner_type, self.get_id, owner.get_id ], reduce=False, include_docs=True): ownership.delete() def add_user(self, user, transaction=None): return self.add_owner(user, 'user', transaction=transaction) def remove_user(self, user): return self.remove_owner(user, 'user') def add_group(self, group, transaction=None): return self.add_owner(group, 'group', transaction=transaction) def remove_group(self, group): return self.remove_owner(group, 'group') def type_check(self): fields = set(self.fields.keys()) for field in self.data_type.fields: if field in fields: fields.remove(field) else: raise FixtureTypeCheckError("field %s not in fixture data %s" % (field, self.get_id)) if fields: raise FixtureTypeCheckError( "fields %s from fixture data %s not in fixture data type" % (', '.join(fields), self.get_id)) def to_xml(self): xData = ElementTree.Element(self.data_type.tag) for field in self.data_type.fields: xField = ElementTree.SubElement(xData, field) xField.text = unicode( self.fields[field]) if self.fields.has_key(field) else "" return xData def get_groups(self, wrap=True): group_ids = set(get_db().view( 'fixtures/ownership', key=[self.domain, 'group by data_item', self.get_id], reduce=False, wrapper=lambda r: r['value'])) if wrap: return set( Group.view('_all_docs', keys=list(group_ids), include_docs=True)) else: return group_ids def get_users(self, wrap=True, include_groups=False): user_ids = set(get_db().view( 'fixtures/ownership', key=[self.domain, 'user by data_item', self.get_id], reduce=False, wrapper=lambda r: r['value'])) if include_groups: group_ids = self.get_groups(wrap=False) else: group_ids = set() users_in_groups = [ group.get_users(only_commcare=True) for group in Group.view( '_all_docs', keys=list(group_ids), include_docs=True) ] if wrap: return set( CommCareUser.view('_all_docs', keys=list(user_ids), include_docs=True)).union(*users_in_groups) else: return user_ids | set([user.get_id for user in users_in_groups]) def get_all_users(self, wrap=True): return self.get_users(wrap=wrap, include_groups=True) @classmethod def by_user(cls, user, wrap=True, domain=None): group_ids = Group.by_user(user, wrap=False) if isinstance(user, dict): user_id = user.get('user_id') user_domain = domain else: user_id = user.user_id user_domain = user.domain fixture_ids = set(FixtureOwnership.get_db().view( 'fixtures/ownership', keys=[[user_domain, 'data_item by user', user_id]] + [[user_domain, 'data_item by group', group_id] for group_id in group_ids], reduce=False, wrapper=lambda r: r['value'], )) if wrap: results = cls.get_db().view('_all_docs', keys=list(fixture_ids), include_docs=True) # sort the results into those corresponding to real documents # and those corresponding to deleted or non-existent documents docs = [] deleted_fixture_ids = set() for result in results: if result.get('doc'): docs.append(cls.wrap(result['doc'])) elif result.get('error'): assert result['error'] == 'not_found' deleted_fixture_ids.add(result['key']) else: assert result['value']['deleted'] is True deleted_fixture_ids.add(result['id']) # fetch and delete ownership documents pointing # to deleted or non-existent fixture documents # this cleanup is necessary since we used to not do this bad_ownerships = FixtureOwnership.for_all_item_ids( deleted_fixture_ids, user_domain) FixtureOwnership.get_db().bulk_delete(bad_ownerships) return docs else: return fixture_ids @classmethod def by_group(cls, group, wrap=True): fixture_ids = get_db().view( 'fixtures/ownership', key=[group.domain, 'data_item by group', group.get_id], reduce=False, wrapper=lambda r: r['value'], ).all() return cls.view('_all_docs', keys=list(fixture_ids), include_docs=True) if wrap else fixture_ids @classmethod def by_data_type(cls, domain, data_type): data_type_id = _id_from_doc(data_type) return cls.view('fixtures/data_items_by_domain_type', key=[domain, data_type_id], reduce=False, include_docs=True) @classmethod def by_domain(cls, domain): return cls.view('fixtures/data_items_by_domain_type', startkey=[domain], endkey=[domain, {}], reduce=False, include_docs=True) @classmethod def by_field_value(cls, domain, data_type, field_name, field_value): data_type_id = _id_from_doc(data_type) return cls.view('fixtures/data_items_by_field_value', key=[domain, data_type_id, field_name, field_value], reduce=False, include_docs=True) def delete_ownerships(self, transaction): ownerships = FixtureOwnership.by_item_id(self.get_id, self.domain) transaction.delete_all(ownerships) def recursive_delete(self, transaction): self.delete_ownerships(transaction) transaction.delete(self)
class LegacyWeeklyReport(Document): """ This doc stores the aggregate weekly results per site. Example: domain: 'mikesproject', site: 'Pennsylvania State Elementary School', week_end_date: Saturday Sept 28, 2013, site_strategy: [3, -1, 0, 4, 2], site_game: [2, 4, 3, 1, 0], individual: { 'mikeo': { 'strategy': [2, 4, 0, 1, 3], 'game': [1, 2, 4, 1, 0], 'weekly_totals': [ ['Sept 9', 3], ['Sept 16', 2], ['Sept 23', 5], # current week ], }, }, 'weekly_totals': [ ['Sept 9', 11], ['Sept 16', 6], ['Sept 23', 9], # current week ], Where each week is a 5 element list. 0 indicates that no strategies/games were recorded, -1 indicates an off day (nothing recorded, but that's okay). """ domain = StringProperty() site = StringProperty() week_end_date = DateProperty() site_strategy = ListProperty() site_game = ListProperty() individual = DictProperty() weekly_totals = ListProperty() @classmethod def by_site(cls, site, date=None): if isinstance(site, Group): site = site.name if date is None: # get the most recent saturday (isoweekday==6) days = [6, 7, 1, 2, 3, 4, 5] today = datetime.date.today() date = today - datetime.timedelta( days=days.index(today.isoweekday())) report = cls.view( 'penn_state/smiley_weekly_reports', key=[DOMAIN, site, str(date)], reduce=False, include_docs=True, ).first() return report @classmethod def by_user(cls, user, date=None): # Users should only have one group, and it should be a report group groups = Group.by_user(user).all() # if len(groups) != 1 or not groups[0].reporting: if len(groups) == 0 or not groups[0].reporting: return site = groups[0].name return cls.by_site(site, date)
class ExportSchema(Document, UnicodeMixIn): """ An export schema that can store intermittent contents of the export so that the entire doc list doesn't have to be used to generate the export """ index = JsonProperty() seq = StringProperty() # semi-deprecated schema = DictProperty() timestamp = TimeStampProperty() def __unicode__(self): return "%s: %s" % (json.dumps(self.index), self.seq) @property def is_bigcouch(self): try: int(self.seq) return False except ValueError: return True @classmethod def wrap(cls, data): if isinstance(data.get('seq'), (int, long)): data['seq'] = unicode(data['seq']) ret = super(ExportSchema, cls).wrap(data) if not ret.timestamp: # these won't work on bigcouch so we want to know if this happens notify_exception( None, 'an export without a timestamp was accessed! %s (%s)' % (ret.index, ret._id)) # this isn't the cleanest nor is it perfect but in the event # this doc traversed databases somehow and now has a bad seq # id, make sure to just reset it to 0. # This won't catch if the seq is bad but not greater than the # current one). current_seq = cls.get_db().info()["update_seq"] try: if int(current_seq) < int(ret.seq): ret.seq = "0" ret.save() except ValueError: # seqs likely weren't ints (e.g. bigcouch) # this should never be possible (anything on bigcouch should # have a timestamp) so let's fail hard raise Exception( 'export %s is in a bad state (no timestamp or integer seq)' % ret._id) # TODO? handle seq -> datetime migration return ret @classmethod def last(cls, index): # search first by timestamp, then fall back to seq id shared_kwargs = { 'descending': True, 'limit': 1, 'include_docs': True, 'reduce': False, } ret = cls.view("couchexport/schema_checkpoints", startkey=['by_timestamp', json.dumps(index), {}], endkey=['by_timestamp', json.dumps(index)], **shared_kwargs).one() if ret and not ret.timestamp: # we found a bunch of old checkpoints but they only # had seq ids, so use those instead ret = cls.view("couchexport/schema_checkpoints", startkey=['by_seq', json.dumps(index), {}], endkey=['by_seq', json.dumps(index)], **shared_kwargs).one() return ret @classmethod def get_all_indices(cls): ret = cls.get_db().view("couchexport/schema_checkpoints", startkey=['by_timestamp'], endkey=['by_timestamp', {}], reduce=True, group=True, group_level=2) for row in ret: index = row['key'][1] try: yield json.loads(index) except ValueError: # ignore this for now - should just be garbage data # print "poorly formatted index key %s" % index pass @classmethod def get_all_checkpoints(cls, index): return cls.view("couchexport/schema_checkpoints", startkey=['by_timestamp', json.dumps(index)], endkey=['by_timestamp', json.dumps(index), {}], include_docs=True, reduce=False) _tables = None @property def tables(self): if self._tables is None: from couchexport.export import get_headers headers = get_headers(self.schema, separator=".") self._tables = [(index, row[0]) for index, row in headers] return self._tables @property def table_dict(self): return dict(self.tables) def get_columns(self, index): return ['id'] + self.table_dict[index].data def get_all_ids(self, database=None): database = database or self.get_db() return set([ result['id'] for result in database.view( "couchexport/schema_index", reduce=False, **get_schema_index_view_keys(self.index)).all() ]) def get_new_ids(self, database=None): # TODO: deprecate/remove old way of doing this database = database or self.get_db() if self.timestamp: return self._ids_by_timestamp(database) else: return self._ids_by_seq(database) def _ids_by_seq(self, database): if self.seq == "0" or self.seq is None: return self.get_all_ids() consumer = Consumer(database) view_results = consumer.fetch(since=self.seq) if view_results: include_ids = set([res["id"] for res in view_results["results"]]) return include_ids.intersection(self.get_all_ids()) else: # sometimes this comes back empty. I think it might be a bug # in couchdbkit, but it's impossible to consistently reproduce. # For now, just assume this is fine. return set() def _ids_by_timestamp(self, database): tag_as_list = force_tag_to_list(self.index) startkey = tag_as_list + [self.timestamp.isoformat()] endkey = tag_as_list + [{}] return set([ result['id'] for result in database.view("couchexport/schema_index", reduce=False, startkey=startkey, endkey=endkey) ]) def get_new_docs(self, database=None): return iter_docs(self.get_new_ids(database))
class SelectChoice(DocumentSchema): label = DictProperty() stringValue = StringProperty() value = Property()
class ReportConfig(CachedCouchDocumentMixin, Document): """ This model represents a "Saved Report." That is, a saved set of filters for a given report. """ domain = StringProperty() # the prefix of the report dispatcher class for this report, used to # get route name for url reversing, and report names report_type = StringProperty() report_slug = StringProperty() subreport_slug = StringProperty(default=None) name = StringProperty() description = StringProperty() owner_id = StringProperty() filters = DictProperty() date_range = StringProperty(choices=get_all_daterange_slugs()) days = IntegerProperty(default=None) start_date = DateProperty(default=None) end_date = DateProperty(default=None) datespan_slug = StringProperty(default=None) def delete(self, *args, **kwargs): notifications = self.view('reportconfig/notifications_by_config', reduce=False, include_docs=True, key=self._id).all() for n in notifications: n.config_ids.remove(self._id) if n.config_ids: n.save() else: n.delete() return super(ReportConfig, self).delete(*args, **kwargs) @classmethod def by_domain_and_owner(cls, domain, owner_id, report_slug=None, stale=True, skip=None, limit=None): kwargs = {} if stale: kwargs['stale'] = settings.COUCH_STALE_QUERY if report_slug is not None: key = ["name slug", domain, owner_id, report_slug] else: key = ["name", domain, owner_id] db = cls.get_db() if skip is not None: kwargs['skip'] = skip if limit is not None: kwargs['limit'] = limit result = cache_core.cached_view(db, "reportconfig/configs_by_domain", reduce=False, include_docs=True, startkey=key, endkey=key + [{}], wrapper=cls.wrap, **kwargs) return result @classmethod def default(self): return { 'name': '', 'description': '', #'date_range': 'last7', 'days': None, 'start_date': None, 'end_date': None, 'filters': {} } def to_complete_json(self, lang=None): result = super(ReportConfig, self).to_json() result.update({ 'url': self.url, 'report_name': self.report_name, 'date_description': self.date_description, 'datespan_filters': self.datespan_filter_choices(self.datespan_filters, lang or ucr_default_language()), 'has_ucr_datespan': self.has_ucr_datespan, }) return result @property @memoized def _dispatcher(self): from corehq.apps.userreports.models import CUSTOM_REPORT_PREFIX from corehq.apps.userreports.reports.view import ( ConfigurableReportView, CustomConfigurableReportDispatcher, ) dispatchers = [ ProjectReportDispatcher, CustomProjectReportDispatcher, ] for dispatcher in dispatchers: if dispatcher.prefix == self.report_type: return dispatcher() if self.report_type == 'configurable': if self.subreport_slug.startswith(CUSTOM_REPORT_PREFIX): return CustomConfigurableReportDispatcher() else: return ConfigurableReportView() if self.doc_type != 'ReportConfig-Deleted': self.doc_type += '-Deleted' self.save() notify_exception( None, "This saved-report (id: %s) is unknown (report_type: %s) and so we have archived it" % (self._id, self.report_type)) raise UnsupportedSavedReportError("Unknown dispatcher: %s" % self.report_type) def get_date_range(self): date_range = self.date_range # allow old report email notifications to represent themselves as a # report config by leaving the default date range up to the report # dispatcher if not date_range: return {} try: start_date, end_date = get_daterange_start_end_dates( date_range, start_date=self.start_date, end_date=self.end_date, days=self.days, ) except InvalidDaterangeException: # this is due to bad validation. see: http://manage.dimagi.com/default.asp?110906 logging.error( 'saved report %s is in a bad state - date range is misconfigured' % self._id) return {} dates = { 'startdate': start_date.isoformat(), 'enddate': end_date.isoformat(), } if self.is_configurable_report: filter_slug = self.datespan_slug if filter_slug: return { '%s-start' % filter_slug: start_date.isoformat(), '%s-end' % filter_slug: end_date.isoformat(), filter_slug: '%(startdate)s to %(enddate)s' % dates, } return dates @property @memoized def query_string(self): params = {} if self._id != 'dummy': params['config_id'] = self._id params.update(self.filters) params.update(self.get_date_range()) return urlencode(params, True) @property @memoized def url_kwargs(self): kwargs = { 'domain': self.domain, 'report_slug': self.report_slug, } if self.subreport_slug: kwargs['subreport_slug'] = self.subreport_slug return immutabledict(kwargs) @property @memoized def view_kwargs(self): if not self.is_configurable_report: return self.url_kwargs.union({ 'permissions_check': self._dispatcher.permissions_check, }) return self.url_kwargs @property @memoized def url(self): try: if self.is_configurable_report: url_base = absolute_reverse( self.report_slug, args=[self.domain, self.subreport_slug]) else: url_base = absolute_reverse(self._dispatcher.name(), kwargs=self.url_kwargs) return url_base + '?' + self.query_string except UnsupportedSavedReportError: return "#" except Exception as e: logging.exception(str(e)) return "#" @property @memoized def report(self): """ Returns None if no report is found for that report slug, which happens when a report is no longer available. All callers should handle this case. """ try: return self._dispatcher.get_report(self.domain, self.report_slug, self.subreport_slug) except UnsupportedSavedReportError: return None @property def report_name(self): try: if self.report is None: return _("Deleted Report") else: return _(self.report.name) except Exception: return _("Unsupported Report") @property def full_name(self): if self.name: return "%s (%s)" % (self.name, self.report_name) else: return self.report_name @property def date_description(self): if self.date_range == 'lastmonth': return "Last Month" elif self.days and not self.start_date: day = 'day' if self.days == 1 else 'days' return "Last %d %s" % (self.days, day) elif self.end_date: return "From %s to %s" % (self.start_date, self.end_date) elif self.start_date: return "Since %s" % self.start_date else: return '' @property @memoized def owner(self): return CouchUser.get_by_user_id(self.owner_id) def get_report_content(self, lang, attach_excel=False): """ Get the report's HTML content as rendered by the static view format. """ from corehq.apps.locations.middleware import LocationAccessMiddleware try: if self.report is None: return ReportContent( _("The report used to create this scheduled report is no" " longer available on CommCare HQ. Please delete this" " scheduled report and create a new one using an available" " report."), None, ) except Exception: pass if getattr(self.report, 'is_deprecated', False): return ReportContent( self.report.deprecation_email_message or _("[DEPRECATED] %s report has been deprecated and will stop working soon. " "Please update your saved reports email settings if needed." % self.report.name), None, ) mock_request = HttpRequest() mock_request.couch_user = self.owner mock_request.user = self.owner.get_django_user() mock_request.domain = self.domain mock_request.couch_user.current_domain = self.domain mock_request.couch_user.language = lang mock_request.method = 'GET' mock_request.bypass_two_factor = True mock_query_string_parts = [self.query_string, 'filterSet=true'] if self.is_configurable_report: mock_query_string_parts.append(urlencode(self.filters, True)) mock_query_string_parts.append( urlencode(self.get_date_range(), True)) mock_request.GET = QueryDict('&'.join(mock_query_string_parts)) # Make sure the request gets processed by PRBAC Middleware CCHQPRBACMiddleware.apply_prbac(mock_request) LocationAccessMiddleware.apply_location_access(mock_request) try: dispatch_func = functools.partial( self._dispatcher.__class__.as_view(), mock_request, **self.view_kwargs) email_response = dispatch_func(render_as='email') if email_response.status_code == 302: return ReportContent( _("We are sorry, but your saved report '%(config_name)s' " "is no longer accessible because the owner %(username)s " "is no longer active.") % { 'config_name': self.name, 'username': self.owner.username }, None, ) try: content_json = json.loads(email_response.content) except ValueError: email_text = email_response.content else: email_text = content_json['report'] excel_attachment = dispatch_func( render_as='excel') if attach_excel else None return ReportContent(email_text, excel_attachment) except PermissionDenied: return ReportContent( _("We are sorry, but your saved report '%(config_name)s' " "is no longer accessible because your subscription does " "not allow Custom Reporting. Please talk to your Project " "Administrator about enabling Custom Reports. If you " "want CommCare HQ to stop sending this message, please " "visit %(saved_reports_url)s to remove this " "Emailed Report.") % { 'config_name': self.name, 'saved_reports_url': absolute_reverse('saved_reports', args=[mock_request.domain]), }, None, ) except Http404: return ReportContent( _("We are sorry, but your saved report '%(config_name)s' " "can not be generated since you do not have the correct permissions. " "Please talk to your Project Administrator about getting permissions for this" "report.") % { 'config_name': self.name, }, None, ) except UnsupportedSavedReportError: return ReportContent( _("We are sorry, but your saved report '%(config_name)s' " "is no longer available. If you think this is a mistake, please report an issue." ) % { 'config_name': self.name, }, None, ) @property def is_active(self): """ Returns True if the report has a start_date that is in the past or there is no start date :return: boolean """ return self.start_date is None or self.start_date <= datetime.today( ).date() @property def is_configurable_report(self): from corehq.apps.userreports.reports.view import ConfigurableReportView return self.report_type == ConfigurableReportView.prefix @property def supports_translations(self): if self.report_type == CustomProjectReportDispatcher.prefix: return self.report.get_supports_translations() else: return self.is_configurable_report @property @memoized def languages(self): if self.is_configurable_report: return frozenset(self.report.spec.get_languages()) elif self.supports_translations: return frozenset(self.report.languages) return frozenset() @property @memoized def configurable_report(self): from corehq.apps.userreports.reports.view import ConfigurableReportView return ConfigurableReportView.get_report(self.domain, self.report_slug, self.subreport_slug) @property def datespan_filters(self): return (self.configurable_report.datespan_filters if self.is_configurable_report else []) @property def has_ucr_datespan(self): return self.is_configurable_report and self.datespan_filters @staticmethod def datespan_filter_choices(datespan_filters, lang): localized_datespan_filters = [] for f in datespan_filters: copy = dict(f) copy['display'] = ucr_localize(copy['display'], lang) localized_datespan_filters.append(copy) with localize(lang): return [{ 'display': _('Choose a date filter...'), 'slug': None, }] + localized_datespan_filters
class Domain(Document, HQBillingDomainMixin, SnapshotMixin): """Domain is the highest level collection of people/stuff in the system. Pretty much everything happens at the domain-level, including user membership, permission to see data, reports, charts, etc.""" name = StringProperty() is_active = BooleanProperty() is_public = BooleanProperty(default=False) date_created = DateTimeProperty() default_timezone = StringProperty( default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) secure_submissions = BooleanProperty(default=False) ota_restore_caching = BooleanProperty(default=False) cloudcare_releases = StringProperty( choices=['stars', 'nostars', 'default'], default='default') organization = StringProperty() hr_name = StringProperty( ) # the human-readable name for this project within an organization creating_user = StringProperty( ) # username of the user who created this domain # domain metadata project_type = StringProperty() # e.g. MCH, HIV customer_type = StringProperty() # plus, full, etc. is_test = StringProperty(choices=["true", "false", "none"], default="none") description = StringProperty() short_description = StringProperty() is_shared = BooleanProperty(default=False) commtrack_enabled = BooleanProperty(default=False) call_center_config = SchemaProperty(CallCenterProperties) has_careplan = BooleanProperty(default=False) restrict_superusers = BooleanProperty(default=False) location_restriction_for_users = BooleanProperty(default=True) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings commconnect_enabled = BooleanProperty(default=False) survey_management_enabled = BooleanProperty(default=False) sms_case_registration_enabled = BooleanProperty( default=False) # Whether or not a case can register via sms sms_case_registration_type = StringProperty( ) # Case type to apply to cases registered via sms sms_case_registration_owner_id = StringProperty( ) # Owner to apply to cases registered via sms sms_case_registration_user_id = StringProperty( ) # Submitting user to apply to cases registered via sms sms_mobile_worker_registration_enabled = BooleanProperty( default=False) # Whether or not a mobile worker can register via sms default_sms_backend_id = StringProperty() use_default_sms_response = BooleanProperty(default=False) default_sms_response = StringProperty() chat_message_count_threshold = IntegerProperty() custom_chat_template = StringProperty( ) # See settings.CUSTOM_CHAT_TEMPLATES custom_case_username = StringProperty( ) # Case property to use when showing the case's name in a chat window # If empty, sms can be sent at any time. Otherwise, only send during # these windows of time. SMS_QUEUE_ENABLED must be True in localsettings # for this be considered. restricted_sms_times = SchemaListProperty(DayTimeWindow) # If empty, this is ignored. Otherwise, the framework will make sure # that during these days/times, no automated outbound sms will be sent # to someone if they have sent in an sms within sms_conversation_length # minutes. Outbound sms sent from a user in a chat window, however, will # still be sent. This is meant to prevent chat conversations from being # interrupted by automated sms reminders. # SMS_QUEUE_ENABLED must be True in localsettings for this to be # considered. sms_conversation_times = SchemaListProperty(DayTimeWindow) # In minutes, see above. sms_conversation_length = IntegerProperty(default=10) # Set to True to prevent survey questions and answers form being seen in # SMS chat windows. filter_surveys_from_chat = BooleanProperty(default=False) # The below option only matters if filter_surveys_from_chat = True. # If set to True, invalid survey responses will still be shown in the chat # window, while questions and valid responses will be filtered out. show_invalid_survey_responses_in_chat = BooleanProperty(default=False) # If set to True, if a message is read by anyone it counts as being read by # everyone. Set to False so that a message is only counted as being read # for a user if only that user has read it. count_messages_as_read_by_anyone = BooleanProperty(default=False) # Set to True to allow sending sms and all-label surveys to cases whose # phone number is duplicated with another contact send_to_duplicated_case_numbers = BooleanProperty(default=False) # exchange/domain copying stuff is_snapshot = BooleanProperty(default=False) is_approved = BooleanProperty(default=False) snapshot_time = DateTimeProperty() published = BooleanProperty(default=False) license = StringProperty(choices=LICENSES, default='cc') title = StringProperty() cda = SchemaProperty(LicenseAgreement) multimedia_included = BooleanProperty(default=True) downloads = IntegerProperty( default=0) # number of downloads for this specific snapshot full_downloads = IntegerProperty( default=0) # number of downloads for all snapshots from this domain author = StringProperty() phone_model = StringProperty() attribution_notes = StringProperty() publisher = StringProperty(choices=["organization", "user"], default="user") yt_id = StringProperty() deployment = SchemaProperty(Deployment) image_path = StringProperty() image_type = StringProperty() migrations = SchemaProperty(DomainMigrations) cached_properties = DictProperty() internal = SchemaProperty(InternalProperties) dynamic_reports = SchemaListProperty(DynamicReportSet) # extra user specified properties tags = StringListProperty() area = StringProperty(choices=AREA_CHOICES) sub_area = StringProperty(choices=SUB_AREA_CHOICES) launch_date = DateTimeProperty # to be eliminated from projects and related documents when they are copied for the exchange _dirty_fields = ('admin_password', 'admin_password_charset', 'city', 'country', 'region', 'customer_type') @property def domain_type(self): """ The primary type of this domain. Used to determine site-specific branding. """ if self.commtrack_enabled: return 'commtrack' else: return 'commcare' @classmethod def wrap(cls, data): # for domains that still use original_doc should_save = False if 'original_doc' in data: original_doc = data['original_doc'] del data['original_doc'] should_save = True if original_doc: original_doc = Domain.get_by_name(original_doc) data['copy_history'] = [original_doc._id] # for domains that have a public domain license if 'license' in data: if data.get("license", None) == "public": data["license"] = "cc" should_save = True if 'slug' in data and data["slug"]: data["hr_name"] = data["slug"] del data["slug"] if 'is_test' in data and isinstance(data["is_test"], bool): data["is_test"] = "true" if data["is_test"] else "false" should_save = True if 'cloudcare_releases' not in data: data['cloudcare_releases'] = 'nostars' # legacy default setting self = super(Domain, cls).wrap(data) if self.deployment is None: self.deployment = Deployment() if self.get_id: self.apply_migrations() if should_save: self.save() return self @staticmethod def active_for_user(user, is_active=True): if isinstance(user, AnonymousUser): return [] from corehq.apps.users.models import CouchUser if isinstance(user, CouchUser): couch_user = user else: couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return cache_core.cached_view(Domain.get_db(), "domain/by_status", keys=[[is_active, d] for d in domain_names], reduce=False, include_docs=True, wrapper=Domain.wrap) else: return [] @classmethod def field_by_prefix(cls, field, prefix='', is_approved=True): # unichr(0xfff8) is something close to the highest character available res = cls.view( "domain/fields_by_prefix", group=True, startkey=[field, is_approved, prefix], endkey=[field, is_approved, "%s%c" % (prefix, unichr(0xfff8)), {}]) vals = [(d['value'], d['key'][2]) for d in res] vals.sort(reverse=True) return [(v[1], v[0]) for v in vals] @classmethod def get_by_field(cls, field, value, is_approved=True): return cls.view('domain/fields_by_prefix', key=[field, is_approved, value], reduce=False, include_docs=True).all() def apply_migrations(self): self.migrations.apply(self) @staticmethod def all_for_user(user): if not hasattr(user, 'get_profile'): # this had better be an anonymous user return [] from corehq.apps.users.models import CouchUser couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return Domain.view("domain/domains", keys=domain_names, reduce=False, include_docs=True).all() else: return [] def add(self, model_instance, is_active=True): """ Add something to this domain, through the generic relation. Returns the created membership object """ # Add membership info to Couch couch_user = model_instance.get_profile().get_couch_user() couch_user.add_domain_membership(self.name) couch_user.save() def applications(self): from corehq.apps.app_manager.models import ApplicationBase return ApplicationBase.view('app_manager/applications_brief', startkey=[self.name], endkey=[self.name, {}]).all() def full_applications(self, include_builds=True): from corehq.apps.app_manager.models import Application, RemoteApp WRAPPERS = {'Application': Application, 'RemoteApp': RemoteApp} def wrap_application(a): return WRAPPERS[a['doc']['doc_type']].wrap(a['doc']) if include_builds: startkey = [self.name] endkey = [self.name, {}] else: startkey = [self.name, None] endkey = [self.name, None, {}] return get_db().view('app_manager/applications', startkey=startkey, endkey=endkey, include_docs=True, wrapper=wrap_application).all() @cached_property def versions(self): apps = self.applications() return list(set(a.application_version for a in apps)) @cached_property def has_case_management(self): for app in self.full_applications(): if app.doc_type == 'Application': if app.has_case_management(): return True return False @cached_property def has_media(self): for app in self.full_applications(): if app.doc_type == 'Application' and app.has_media(): return True return False @property def use_cloudcare_releases(self): return self.cloudcare_releases != 'nostars' def all_users(self): from corehq.apps.users.models import CouchUser return CouchUser.by_domain(self.name) def has_shared_media(self): return False def recent_submissions(self): from corehq.apps.reports.util import make_form_couch_key key = make_form_couch_key(self.name) res = get_db().view('reports_forms/all_forms', startkey=key + [{}], endkey=key, descending=True, reduce=False, include_docs=False, limit=1).all() if len(res ) > 0: # if there have been any submissions in the past 30 days return (datetime.now() <= datetime.strptime(res[0]['key'][2], "%Y-%m-%dT%H:%M:%SZ") + timedelta(days=30)) else: return False @cached_property def languages(self): apps = self.applications() return set(chain.from_iterable([a.langs for a in apps])) def readable_languages(self): return ', '.join(lang_lookup[lang] or lang for lang in self.languages()) def __unicode__(self): return self.name @classmethod def get_by_name(cls, name, strict=False): if not name: # get_by_name should never be called with name as None (or '', etc) # I fixed the code in such a way that if I raise a ValueError # all tests pass and basic pages load, # but in order not to break anything in the wild, # I'm opting to notify by email if/when this happens # but fall back to the previous behavior of returning None try: raise ValueError('%r is not a valid domain name' % name) except ValueError: if settings.DEBUG: raise else: notify_exception(None, '%r is not a valid domain name' % name) return None cache_key = _domain_cache_key(name) MISSING = object() res = cache.get(cache_key, MISSING) if res != MISSING: return res else: domain = cls._get_by_name(name, strict) # 30 mins, so any unforeseen invalidation bugs aren't too bad. cache.set(cache_key, domain, 30 * 60) return domain @classmethod def _get_by_name(cls, name, strict=False): extra_args = { 'stale': settings.COUCH_STALE_QUERY } if not strict else {} db = cls.get_db() res = cache_core.cached_view(db, "domain/domains", key=name, reduce=False, include_docs=True, wrapper=cls.wrap, force_invalidate=strict, **extra_args) if len(res) > 0: result = res[0] else: result = None if result is None and not strict: # on the off chance this is a brand new domain, try with strict return cls.get_by_name(name, strict=True) return result @classmethod def get_by_organization(cls, organization): result = cache_core.cached_view(cls.get_db(), "domain/by_organization", startkey=[organization], endkey=[organization, {}], reduce=False, include_docs=True, wrapper=cls.wrap) from corehq.apps.accounting.utils import domain_has_privilege from corehq import privileges result = filter( lambda x: domain_has_privilege(x.name, privileges. CROSS_PROJECT_REPORTS), result) return result @classmethod def get_by_organization_and_hrname(cls, organization, hr_name): result = cls.view("domain/by_organization", key=[organization, hr_name], reduce=False, include_docs=True) return result @classmethod def get_or_create_with_name(cls, name, is_active=False, secure_submissions=True): result = cls.view("domain/domains", key=name, reduce=False, include_docs=True).first() if result: return result else: new_domain = Domain( name=name, is_active=is_active, date_created=datetime.utcnow(), secure_submissions=secure_submissions, ) new_domain.migrations = DomainMigrations( has_migrated_permissions=True) new_domain.save(**get_safe_write_kwargs()) return new_domain def password_format(self): """ This was a performance hit, so for now we'll just return 'a' no matter what If a single application is alphanumeric, return alphanumeric; otherwise, return numeric """ return 'a' @classmethod def get_all(cls, include_docs=True): # todo: this should use iter_docs return Domain.view("domain/not_snapshots", include_docs=include_docs).all() def case_sharing_included(self): return self.case_sharing or reduce(lambda x, y: x or y, [ getattr(app, 'case_sharing', False) for app in self.applications() ], False) def save(self, **params): super(Domain, self).save(**params) cache.delete(_domain_cache_key(self.name)) from corehq.apps.domain.signals import commcare_domain_post_save results = commcare_domain_post_save.send_robust(sender='domain', domain=self) for result in results: # Second argument is None if there was no error if result[1]: notify_exception( None, message="Error occured during domain post_save %s: %s" % (self.name, str(result[1]))) def save_copy(self, new_domain_name=None, user=None, ignore=None): from corehq.apps.app_manager.models import get_app from corehq.apps.reminders.models import CaseReminderHandler ignore = ignore if ignore is not None else [] if new_domain_name is not None and Domain.get_by_name(new_domain_name): return None db = get_db() new_id = db.copy_doc(self.get_id)['id'] if new_domain_name is None: new_domain_name = new_id new_domain = Domain.get(new_id) new_domain.name = new_domain_name new_domain.copy_history = self.get_updated_history() new_domain.is_snapshot = False new_domain.snapshot_time = None new_domain.organization = None # TODO: use current user's organization (?) # reset stuff new_domain.cda.signed = False new_domain.cda.date = None new_domain.cda.type = None new_domain.cda.user_id = None new_domain.cda.user_ip = None new_domain.is_test = "none" new_domain.internal = InternalProperties() new_domain.creating_user = user.username if user else None for field in self._dirty_fields: if hasattr(new_domain, field): delattr(new_domain, field) new_comps = {} # a mapping of component's id to it's copy for res in db.view('domain/related_to_domain', key=[self.name, True]): if not self.is_snapshot and res['value']['doc_type'] in ( 'Application', 'RemoteApp'): app = get_app(self.name, res['value']['_id']).get_latest_saved() if app: comp = self.copy_component(app.doc_type, app._id, new_domain_name, user=user) else: comp = self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) elif res['value']['doc_type'] not in ignore: comp = self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) else: comp = None if comp: new_comps[res['value']['_id']] = comp new_domain.save() if user: def add_dom_to_user(user): user.add_domain_membership(new_domain_name, is_admin=True) apply_update(user, add_dom_to_user) def update_events(handler): """ Change the form_unique_id to the proper form for each event in a newly copied CaseReminderHandler """ from corehq.apps.app_manager.models import FormBase for event in handler.events: if not event.form_unique_id: continue form = FormBase.get_form(event.form_unique_id) form_app = form.get_app() m_index, f_index = form_app.get_form_location(form.unique_id) form_copy = new_comps[form_app._id].get_module( m_index).get_form(f_index) event.form_unique_id = form_copy.unique_id def update_for_copy(handler): handler.active = False update_events(handler) if 'CaseReminderHandler' not in ignore: for handler in CaseReminderHandler.get_handlers(new_domain_name): apply_update(handler, update_for_copy) return new_domain def reminder_should_be_copied(self, handler): from corehq.apps.reminders.models import ON_DATETIME return (handler.start_condition_type != ON_DATETIME and handler.user_group_id is None) def copy_component(self, doc_type, id, new_domain_name, user=None): from corehq.apps.app_manager.models import import_app from corehq.apps.users.models import UserRole from corehq.apps.reminders.models import CaseReminderHandler str_to_cls = { 'UserRole': UserRole, 'CaseReminderHandler': CaseReminderHandler, } db = get_db() if doc_type in ('Application', 'RemoteApp'): new_doc = import_app(id, new_domain_name) new_doc.copy_history.append(id) else: cls = str_to_cls[doc_type] if doc_type == 'CaseReminderHandler': cur_doc = cls.get(id) if not self.reminder_should_be_copied(cur_doc): return None new_id = db.copy_doc(id)['id'] new_doc = cls.get(new_id) for field in self._dirty_fields: if hasattr(new_doc, field): delattr(new_doc, field) if hasattr(cls, '_meta_fields'): for field in cls._meta_fields: if not field.startswith('_') and hasattr(new_doc, field): delattr(new_doc, field) new_doc.domain = new_domain_name if self.is_snapshot and doc_type == 'Application': new_doc.prepare_multimedia_for_exchange() new_doc.save() return new_doc def save_snapshot(self, ignore=None): if self.is_snapshot: return self else: copy = self.save_copy(ignore=ignore) if copy is None: return None copy.is_snapshot = True copy.snapshot_time = datetime.now() del copy.deployment copy.save() return copy def from_snapshot(self): return not self.is_snapshot and self.original_doc is not None def snapshots(self): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, reduce=False, descending=True) @memoized def published_snapshot(self): snapshots = self.snapshots().all() for snapshot in snapshots: if snapshot.published: return snapshot return None @classmethod def published_snapshots(cls, include_unapproved=False, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page if include_unapproved: return cls.view('domain/published_snapshots', startkey=[False, {}], include_docs=True, descending=True, limit=limit, skip=skip) else: return cls.view('domain/published_snapshots', endkey=[True], include_docs=True, descending=True, limit=limit, skip=skip) @classmethod def snapshot_search(cls, query, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page results = get_db().search( 'domain/snapshot_search', q=json.dumps(query), limit=limit, skip=skip, #stale='ok', ) return map(cls.get, [r['id'] for r in results]), results.total_rows @memoized def get_organization(self): from corehq.apps.orgs.models import Organization return Organization.get_by_name(self.organization) @memoized def organization_title(self): if self.organization: return self.get_organization().title else: return '' def update_deployment(self, **kwargs): self.deployment.update(kwargs) self.save() def update_internal(self, **kwargs): self.internal.update(kwargs) self.save() def display_name(self): if self.is_snapshot: return "Snapshot of %s" % self.copied_from.display_name() if self.hr_name and self.organization: return self.hr_name else: return self.name def long_display_name(self): if self.is_snapshot: return format_html("Snapshot of {0} > {1}", self.get_organization().title, self.copied_from.display_name()) if self.organization: return format_html('{0} > {1}', self.get_organization().title, self.hr_name or self.name) else: return self.name __str__ = long_display_name def get_license_display(self): return LICENSES.get(self.license) def copies(self): return Domain.view('domain/copied_from_snapshot', key=self._id, include_docs=True) def copies_of_parent(self): return Domain.view('domain/copied_from_snapshot', keys=[s._id for s in self.copied_from.snapshots()], include_docs=True) def delete(self): # delete all associated objects db = get_db() related_docs = db.view('domain/related_to_domain', startkey=[self.name], endkey=[self.name, {}], include_docs=True) for doc in related_docs: db.delete_doc(doc['doc']) super(Domain, self).delete() def all_media(self, from_apps=None): #todo add documentation or refactor from corehq.apps.hqmedia.models import CommCareMultimedia dom_with_media = self if not self.is_snapshot else self.copied_from if self.is_snapshot: app_ids = [ app.copied_from.get_id for app in self.full_applications() ] if from_apps: from_apps = set( [a_id for a_id in app_ids if a_id in from_apps]) else: from_apps = app_ids if from_apps: media = [] media_ids = set() apps = [ app for app in dom_with_media.full_applications() if app.get_id in from_apps ] for app in apps: if app.doc_type != 'Application': continue for _, m in app.get_media_objects(): if m.get_id not in media_ids: media.append(m) media_ids.add(m.get_id) return media return CommCareMultimedia.view('hqmedia/by_domain', key=dom_with_media.name, include_docs=True).all() def most_restrictive_licenses(self, apps_to_check=None): from corehq.apps.hqmedia.utils import most_restrictive licenses = [ m.license['type'] for m in self.all_media(from_apps=apps_to_check) if m.license ] return most_restrictive(licenses) @classmethod def popular_sort(cls, domains): sorted_list = [] MIN_REVIEWS = 1.0 domains = [(domain, Review.get_average_rating_by_app(domain.copied_from._id), Review.get_num_ratings_by_app(domain.copied_from._id)) for domain in domains] domains = [(domain, avg or 0.0, num or 0) for domain, avg, num in domains] total_average_sum = sum(avg for domain, avg, num in domains) total_average_count = len(domains) if not total_average_count: return [] total_average = (total_average_sum / total_average_count) for domain, average_rating, num_ratings in domains: if num_ratings == 0: sorted_list.append((0.0, domain)) else: weighted_rating = ( (num_ratings / (num_ratings + MIN_REVIEWS)) * average_rating + (MIN_REVIEWS / (num_ratings + MIN_REVIEWS)) * total_average) sorted_list.append((weighted_rating, domain)) sorted_list = [ domain for weighted_rating, domain in sorted( sorted_list, key=lambda domain: domain[0], reverse=True) ] return sorted_list @classmethod def hit_sort(cls, domains): domains = list(domains) domains = sorted(domains, key=lambda domain: domain.download_count, reverse=True) return domains @classmethod def public_deployments(cls): return Domain.view('domain/with_deployment', include_docs=True).all() @classmethod def get_module_by_name(cls, domain_name): """ import and return the python module corresponding to domain_name, or None if it doesn't exist. """ from corehq.apps.domain.utils import get_domain_module_map module_name = get_domain_module_map().get(domain_name, domain_name) try: return import_module(module_name) if module_name else None except ImportError: return None @property @memoized def commtrack_settings(self): # this import causes some dependency issues so lives in here from corehq.apps.commtrack.models import CommtrackConfig if self.commtrack_enabled: return CommtrackConfig.for_domain(self.name) else: return None @property def has_custom_logo(self): return (self['_attachments'] and LOGO_ATTACHMENT in self['_attachments']) def get_custom_logo(self): if not self.has_custom_logo: return None return (self.fetch_attachment(LOGO_ATTACHMENT), self['_attachments'][LOGO_ATTACHMENT]['content_type']) def get_case_display(self, case): """Get the properties display definition for a given case""" return self.case_display.case_details.get(case.type) def get_form_display(self, form): """Get the properties display definition for a given XFormInstance""" return self.case_display.form_details.get(form.xmlns) @property def total_downloads(self): """ Returns the total number of downloads from every snapshot created from this domain """ return get_db().view( "domain/snapshots", startkey=[self.get_id], endkey=[self.get_id, {}], reduce=True, include_docs=False, ).one()["value"] @property @memoized def download_count(self): """ Updates and returns the total number of downloads from every sister snapshot. """ if self.is_snapshot: self.full_downloads = self.copied_from.total_downloads return self.full_downloads @property @memoized def published_by(self): from corehq.apps.users.models import CouchUser pb_id = self.cda.user_id return CouchUser.get_by_user_id(pb_id) if pb_id else None @property def name_of_publisher(self): return self.published_by.human_friendly_name if self.published_by else ""