Exemplo n.º 1
0
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")
Exemplo n.º 2
0
class FixtureItemField(DocumentSchema):
    """
        "field_value": "Delhi_IN_HIN",
        "properties": {"lang": "hin"} 
    """
    field_value = StringProperty()
    properties = DictProperty()
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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} &gt; {1}",
                self.get_organization().title,
                self.copied_from.display_name()
            )
        if self.organization:
            return format_html(
                '{0} &gt; {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)
Exemplo n.º 11
0
class CasePropertySpec(DocumentSchema):
    key = StringProperty()
    label = DictProperty()
    type = StringProperty(choices=['string', 'select', 'date', 'group'],
                          default='string')
    choices = SchemaListProperty(SelectChoice)
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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))
Exemplo n.º 16
0
class SelectChoice(DocumentSchema):
    label = DictProperty()
    stringValue = StringProperty()
    value = Property()
Exemplo n.º 17
0
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
Exemplo n.º 18
0
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} &gt; {1}",
                               self.get_organization().title,
                               self.copied_from.display_name())
        if self.organization:
            return format_html('{0} &gt; {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 ""