示例#1
0
class AssetVersion(models.Model):
    uid = KpiUidField(uid_prefix='v')
    asset = models.ForeignKey('Asset', related_name='asset_versions')
    name = models.CharField(null=True, max_length=255)
    date_modified = models.DateTimeField(default=timezone.now)

    # preserving _reversion_version in case we don't save all that we
    # need to in the first migration from reversion to AssetVersion
    _reversion_version = models.OneToOneField(
        Version,
        null=True,
        on_delete=models.SET_NULL,
    )
    version_content = JSONBField()
    uid_aliases = JSONBField(null=True)
    deployed_content = JSONBField(null=True)
    _deployment_data = JSONBField(default=False)
    deployed = models.BooleanField(default=False)

    class Meta:
        ordering = ['-date_modified']

    def _deployed_content(self):
        if self.deployed_content is not None:
            return self.deployed_content
        legacy_names = self._reversion_version is not None
        if legacy_names:
            return to_xlsform_structure(self.version_content,
                                        deprecated_autoname=True)
        else:
            return to_xlsform_structure(self.version_content,
                                        move_autonames=True)

    def to_formpack_schema(self):
        return {
            'content': expand_content(self._deployed_content()),
            'version': self.uid,
            'version_id_key': '__version__',
        }

    def _content_hash(self):
        # used to determine changes in the content from version to version
        # not saved, only compared with other asset_versions (in tests and
        # migration scripts, initially)
        _json_string = json.dumps(
            {
                'version_content': self.version_content,
                'deployed_content': self.deployed_content,
                'deployed': self.deployed,
            },
            sort_keys=True)
        return hashlib.sha1(_json_string).hexdigest()

    def __unicode__(self):
        return '{}@{} T{}{}'.format(
            self.asset.uid, self.uid,
            self.date_modified.strftime('%Y-%m-%d %H:%m'),
            ' (deployed)' if self.deployed else '')
示例#2
0
class PerUserSetting(models.Model):
    """
    A configuration setting that has different values depending on whether not
    a user matches certain criteria
    """
    user_queries = JSONBField(
        help_text=_('A JSON representation of a *list* of Django queries, '
                    'e.g. `[{"email__iendswith": "@kobotoolbox.org"}, '
                    '{"email__iendswith": "@kbtdev.org"}]`. '
                    'A matching user is one who would be returned by ANY of '
                    'the queries in the list.'))
    name = models.CharField(max_length=255,
                            unique=True,
                            default='INTERCOM_APP_ID')  # The only one for now!
    value_when_matched = models.CharField(max_length=2048, blank=True)
    value_when_not_matched = models.CharField(max_length=2048, blank=True)

    def user_matches(self, user, ignore_invalid_queries=True):
        if user.is_anonymous():
            user = get_anonymous_user()
        manager = user._meta.model.objects
        queryset = manager.none()
        for user_query in self.user_queries:
            try:
                queryset |= manager.filter(**user_query)
            except (FieldError, TypeError):
                if ignore_invalid_queries:
                    return False
                else:
                    raise
        return queryset.filter(pk=user.pk).exists()

    def get_for_user(self, user):
        if self.user_matches(user):
            return self.value_when_matched
        else:
            return self.value_when_not_matched

    def clean(self):
        user = User.objects.first()
        if not user:
            return
        try:
            self.user_matches(user, ignore_invalid_queries=False)
        except FieldError as e:
            raise ValidationError({'user_queries': e.message})
        except TypeError:
            raise ValidationError(
                {'user_queries': _('JSON structure is incorrect.')})

    def __str__(self):
        return self.name
示例#3
0
class AssetFile(models.Model):
    # More to come!
    MAP_LAYER = 'map_layer'
    TYPE_CHOICES = ((MAP_LAYER, MAP_LAYER), )
    uid = KpiUidField(uid_prefix='af')
    asset = models.ForeignKey('Asset', related_name='asset_files')
    # Keep track of the uploading user, who could be anyone with `change_asset`
    # rights, not just the asset owner
    user = models.ForeignKey('auth.User', related_name='asset_files')
    file_type = models.CharField(choices=TYPE_CHOICES, max_length=32)
    name = models.CharField(max_length=255)
    date_created = models.DateTimeField(default=timezone.now)
    # TODO: Handle deletion! The file won't be deleted automatically when the
    # object is removed from the database
    content = PrivateFileField(upload_to=upload_to, max_length=380)
    metadata = JSONBField(default=dict)
示例#4
0
class Hook(models.Model):

    # Export types
    XML = "xml"
    JSON = "json"

    # Authentication levels
    NO_AUTH = "no_auth"
    BASIC_AUTH = "basic_auth"

    # Export types list
    EXPORT_TYPE_CHOICES = ((XML, XML), (JSON, JSON))

    # Authentication levels list
    AUTHENTICATION_LEVEL_CHOICES = ((NO_AUTH, NO_AUTH), (BASIC_AUTH,
                                                         BASIC_AUTH))

    asset = models.ForeignKey("kpi.Asset",
                              related_name="hooks",
                              on_delete=models.CASCADE)
    uid = KpiUidField(uid_prefix="h")
    name = models.CharField(max_length=255, blank=False)
    endpoint = models.CharField(max_length=500, blank=False)
    active = models.BooleanField(default=True)
    export_type = models.CharField(choices=EXPORT_TYPE_CHOICES,
                                   default=JSON,
                                   max_length=10)
    auth_level = models.CharField(choices=AUTHENTICATION_LEVEL_CHOICES,
                                  default=NO_AUTH,
                                  max_length=10)
    settings = JSONBField(default=dict)
    date_created = models.DateTimeField(default=timezone.now)
    date_modified = models.DateTimeField(default=timezone.now)

    class Meta:
        ordering = ["name"]

    def __init__(self, *args, **kwargs):
        self.__totals = {}
        return super(Hook, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        # Update date_modified each time object is saved
        self.date_modified = timezone.now()
        super(Hook, self).save(*args, **kwargs)

    def __unicode__(self):
        return u"%s:%s - %s" % (self.asset, self.name, self.endpoint)

    def get_service_definition(self):
        mod = import_module("kobo.apps.hook.services.service_{}".format(
            self.export_type))
        return getattr(mod, "ServiceDefinition")

    @property
    def success_count(self):
        if not self.__totals:
            self._get_totals()
        return self.__totals.get(HOOK_LOG_SUCCESS)

    @property
    def failed_count(self):
        if not self.__totals:
            self._get_totals()
        return self.__totals.get(HOOK_LOG_FAILED)

    @property
    def pending_count(self):
        if not self.__totals:
            self._get_totals()
        return self.__totals.get(HOOK_LOG_PENDING)

    def _get_totals(self):
        # TODO add some cache
        queryset = self.logs.values("status").annotate(
            values_count=models.Count("status"))
        queryset.query.clear_ordering(True)

        # Initialize totals
        self.__totals = {
            HOOK_LOG_SUCCESS: 0,
            HOOK_LOG_FAILED: 0,
            HOOK_LOG_PENDING: 0
        }
        for record in queryset:
            self.__totals[record.get("status")] = record.get("values_count")

    def reset_totals(self):
        # TODO remove cache when it's enabled
        self.__totals = {}
示例#5
0
class Asset(ObjectPermissionMixin, TagStringMixin, DeployableMixin,
            XlsExportable, FormpackXLSFormUtils, models.Model):
    name = models.CharField(max_length=255, blank=True, default='')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    content = JSONField(null=True)
    summary = JSONField(null=True, default=dict)
    report_styles = JSONBField(default=dict)
    asset_type = models.CharField(choices=ASSET_TYPES,
                                  max_length=20,
                                  default='survey')
    parent = models.ForeignKey('Collection',
                               related_name='assets',
                               null=True,
                               blank=True)
    owner = models.ForeignKey('auth.User', related_name='assets', null=True)
    editors_can_change_permissions = models.BooleanField(default=True)
    uid = KpiUidField(uid_prefix='a')
    tags = TaggableManager(manager=KpiTaggableManager)
    settings = jsonbfield.fields.JSONField(default=dict)

    # _deployment_data should be accessed through the `deployment` property
    # provided by `DeployableMixin`
    _deployment_data = JSONField(default=dict)

    permissions = GenericRelation(ObjectPermission)

    objects = AssetManager()

    @property
    def kind(self):
        return self._meta.model_name

    class Meta:
        ordering = ('-date_modified', )

        permissions = (
            # change_, add_, and delete_asset are provided automatically
            # by Django
            ('view_asset', _('Can view asset')),
            ('share_asset', _("Can change asset's sharing settings")),
            # Permissions for collected data, i.e. submissions
            ('add_submissions', _('Can submit data to asset')),
            ('view_submissions', _('Can view submitted data for asset')),
            ('change_submissions', _('Can modify submitted data for asset')),
            ('delete_submissions', _('Can delete submitted data for asset')),
            ('share_submissions',
             _("Can change sharing settings for "
               "asset's submitted data")),
            # TEMPORARY Issue #1161: A flag to indicate that permissions came
            # solely from `sync_kobocat_xforms` and not from any user
            # interaction with KPI
            ('from_kc_only', 'INTERNAL USE ONLY; DO NOT ASSIGN'))

    # Assignable permissions that are stored in the database
    ASSIGNABLE_PERMISSIONS = ('view_asset', 'change_asset', 'add_submissions',
                              'view_submissions', 'change_submissions')
    # Calculated permissions that are neither directly assignable nor stored
    # in the database, but instead implied by assignable permissions
    CALCULATED_PERMISSIONS = ('share_asset', 'delete_asset',
                              'share_submissions', 'delete_submissions')
    # Certain Collection permissions carry over to Asset
    MAPPED_PARENT_PERMISSIONS = {
        'view_collection': 'view_asset',
        'change_collection': 'change_asset'
    }
    # Granting some permissions implies also granting other permissions
    IMPLIED_PERMISSIONS = {
        # Format: explicit: (implied, implied, ...)
        'change_asset': ('view_asset', ),
        'add_submissions': ('view_asset', ),
        'view_submissions': ('view_asset', ),
        'change_submissions': ('view_submissions', )
    }
    # Some permissions must be copied to KC
    KC_PERMISSIONS_MAP = {  # keys are KC's codenames, values are KPI's
        'change_submissions': 'change_xform',  # "Can Edit" in KC UI
        'view_submissions': 'view_xform',  # "Can View" in KC UI
        'add_submissions': 'report_xform',  # "Can submit to" in KC UI
    }
    KC_CONTENT_TYPE_KWARGS = {'app_label': 'logger', 'model': 'xform'}

    # todo: test and implement this method
    # def restore_version(self, uid):
    #     _version_to_restore = self.asset_versions.get(uid=uid)
    #     self.content = _version_to_restore.version_content
    #     self.name = _version_to_restore.name

    def to_ss_structure(self):
        return flatten_content(self.content, in_place=False)

    def _populate_summary(self):
        if self.content is None:
            self.content = {}
            self.summary = {}
            return
        analyzer = AssetContentAnalyzer(**self.content)
        self.summary = analyzer.summary

    def adjust_content_on_save(self):
        '''
        This is called on save by default if content exists.
        Can be disabled / skipped by calling with parameter:
        asset.save(adjust_content=False)
        '''
        self._standardize(self.content)

        self._adjust_active_translation(self.content)
        self._strip_empty_rows(self.content)
        self._assign_kuids(self.content)
        self._autoname(self.content)
        self._unlink_list_items(self.content)
        self._remove_empty_expressions(self.content)

        settings = self.content['settings']
        _title = settings.pop('form_title', None)
        id_string = settings.get('id_string')
        filename = self.summary.pop('filename', None)
        if filename:
            # if we have filename available, set the id_string
            # and/or form_title from the filename.
            if not id_string:
                id_string = sluggify_label(filename)
                settings['id_string'] = id_string
            if not _title:
                _title = filename
        if self.asset_type != 'survey':
            # instead of deleting the settings, simply clear them out
            self.content['settings'] = {}

        if _title is not None:
            self.name = _title

    def save(self, *args, **kwargs):
        if self.content is None:
            self.content = {}

        # in certain circumstances, we don't want content to
        # be altered on save. (e.g. on asset.deploy())
        if kwargs.pop('adjust_content', True):
            self.adjust_content_on_save()

        # populate summary
        self._populate_summary()

        # infer asset_type only between question and block
        if self.asset_type in ['question', 'block']:
            row_count = self.summary.get('row_count')
            if row_count == 1:
                self.asset_type = 'question'
            elif row_count > 1:
                self.asset_type = 'block'

        self._populate_report_styles()

        _create_version = kwargs.pop('create_version', True)
        super(Asset, self).save(*args, **kwargs)

        if _create_version:
            self.asset_versions.create(
                name=self.name,
                version_content=self.content,
                _deployment_data=self._deployment_data,
                # asset_version.deployed is set in the
                # DeploymentSerializer
                deployed=False,
            )

    def rename_translation(self, _from, _to):
        if not self._has_translations(self.content, 2):
            raise ValueError('no translations available')
        self._rename_translation(self.content, _from, _to)

    def to_clone_dict(self, version_uid=None):
        if version_uid:
            version = self.asset_versions.get(uid=version_uid)
        else:
            version = self.asset_versions.first()
        return {
            'name': version.name,
            'content': version.version_content,
            'asset_type': self.asset_type,
            'tag_string': self.tag_string,
        }

    def clone(self, version_uid=None):
        # not currently used, but this is how "to_clone_dict" should work
        return Asset.objects.create(**self.to_clone_dict(version_uid))

    def revert_to_version(self, version_uid):
        av = self.asset_versions.get(uid=version_uid)
        self.content = av.version_content
        self.save()

    def _populate_report_styles(self):
        default = self.report_styles.get(DEFAULT_REPORTS_KEY, {})
        specifieds = self.report_styles.get(SPECIFIC_REPORTS_KEY, {})
        kuids_to_variable_names = self.report_styles.get('kuid_names', {})
        for (index, row) in enumerate(self.content.get('survey', [])):
            if '$kuid' not in row:
                if 'name' in row:
                    row['$kuid'] = json_hash([self.uid, row['name']])
                else:
                    row['$kuid'] = json_hash([self.uid, index, row])
            _identifier = row.get('name', row['$kuid'])
            kuids_to_variable_names[_identifier] = row['$kuid']
            if _identifier not in specifieds:
                specifieds[_identifier] = {}
        self.report_styles = {
            DEFAULT_REPORTS_KEY: default,
            SPECIFIC_REPORTS_KEY: specifieds,
            'kuid_names': kuids_to_variable_names,
        }

    def get_ancestors_or_none(self):
        # ancestors are ordered from farthest to nearest
        if self.parent is not None:
            return self.parent.get_ancestors(include_self=True)
        else:
            return None

    @property
    def latest_version(self):
        return self.asset_versions.order_by('-date_modified').first()

    @property
    def deployed_versions(self):
        return self.asset_versions.filter(
            deployed=True).order_by('-date_modified')

    @property
    def latest_deployed_version(self):
        return self.deployed_versions.first()

    @property
    def version_id(self):
        latest_version = self.latest_version
        if latest_version:
            return latest_version.uid

    @property
    def snapshot(self):
        return self._snapshot(regenerate=False)

    @transaction.atomic
    def _snapshot(self, regenerate=True):
        asset_version = self.asset_versions.first()

        _note = None
        if self.asset_type in ['question', 'block']:
            _note = ('Note: This item is a {} and must be included in '
                     'a form before deploying'.format(self.asset_type))

        try:
            snapshot = AssetSnapshot.objects.get(asset=self,
                                                 asset_version=asset_version)
            if regenerate:
                snapshot.delete()
                snapshot = False
        except AssetSnapshot.MultipleObjectsReturned:
            # how did multiple snapshots get here?
            snaps = AssetSnapshot.objects.filter(asset=self,
                                                 asset_version=asset_version)
            snaps.delete()
            snapshot = False
        except AssetSnapshot.DoesNotExist:
            snapshot = False

        if not snapshot:
            if self.name != '':
                form_title = self.name
            else:
                _settings = self.content.get('settings', {})
                form_title = _settings.get('id_string', 'Untitled')

            self._append(self.content, settings={
                'form_title': form_title,
            })
            snapshot = AssetSnapshot.objects.create(
                asset=self, asset_version=asset_version, source=self.content)
        return snapshot

    def __unicode__(self):
        return u'{} ({})'.format(self.name, self.uid)
示例#6
0
文件: asset.py 项目: siturra/kpi-old
class Asset(ObjectPermissionMixin, TagStringMixin, DeployableMixin,
            XlsExportable, FormpackXLSFormUtils, models.Model):
    name = models.CharField(max_length=255, blank=True, default='')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    content = JSONField(null=True)
    summary = JSONField(null=True, default=dict)
    report_styles = JSONBField(default=dict)
    report_custom = JSONBField(default=dict)
    map_styles = LazyDefaultJSONBField(default=dict)
    map_custom = LazyDefaultJSONBField(default=dict)
    asset_type = models.CharField(choices=ASSET_TYPES,
                                  max_length=20,
                                  default=ASSET_TYPE_SURVEY)
    parent = models.ForeignKey('Collection',
                               related_name='assets',
                               null=True,
                               blank=True)
    owner = models.ForeignKey('auth.User', related_name='assets', null=True)
    editors_can_change_permissions = models.BooleanField(default=True)
    uid = KpiUidField(uid_prefix='a')
    tags = TaggableManager(manager=KpiTaggableManager)
    settings = jsonbfield.fields.JSONField(default=dict)

    # _deployment_data should be accessed through the `deployment` property
    # provided by `DeployableMixin`
    _deployment_data = JSONField(default=dict)

    permissions = GenericRelation(ObjectPermission)

    objects = AssetManager()

    @property
    def kind(self):
        return 'asset'

    class Meta:
        ordering = ('-date_modified', )

        permissions = (
            # change_, add_, and delete_asset are provided automatically
            # by Django
            ('view_asset', _('Can view asset')),
            ('share_asset', _("Can change asset's sharing settings")),
            # Permissions for collected data, i.e. submissions
            ('add_submissions', _('Can submit data to asset')),
            ('view_submissions', _('Can view submitted data for asset')),
            ('change_submissions', _('Can modify submitted data for asset')),
            ('delete_submissions', _('Can delete submitted data for asset')),
            ('share_submissions',
             _("Can change sharing settings for "
               "asset's submitted data")),
            ('validate_submissions', _("Can validate submitted data asset")),
            # TEMPORARY Issue #1161: A flag to indicate that permissions came
            # solely from `sync_kobocat_xforms` and not from any user
            # interaction with KPI
            ('from_kc_only', 'INTERNAL USE ONLY; DO NOT ASSIGN'))

    # Assignable permissions that are stored in the database
    ASSIGNABLE_PERMISSIONS = (
        'view_asset',
        'change_asset',
        'add_submissions',
        'view_submissions',
        'change_submissions',
        'validate_submissions',
    )
    # Calculated permissions that are neither directly assignable nor stored
    # in the database, but instead implied by assignable permissions
    CALCULATED_PERMISSIONS = ('share_asset', 'delete_asset',
                              'share_submissions', 'delete_submissions')
    # Certain Collection permissions carry over to Asset
    MAPPED_PARENT_PERMISSIONS = {
        'view_collection': 'view_asset',
        'change_collection': 'change_asset'
    }
    # Granting some permissions implies also granting other permissions
    IMPLIED_PERMISSIONS = {
        # Format: explicit: (implied, implied, ...)
        'change_asset': ('view_asset', ),
        'add_submissions': ('view_asset', ),
        'view_submissions': ('view_asset', ),
        'change_submissions': ('view_submissions', ),
        'validate_submissions': ('view_submissions', )
    }
    # Some permissions must be copied to KC
    KC_PERMISSIONS_MAP = {  # keys are KC's codenames, values are KPI's
        'change_submissions': 'change_xform',  # "Can Edit" in KC UI
        'view_submissions': 'view_xform',  # "Can View" in KC UI
        'add_submissions': 'report_xform',  # "Can submit to" in KC UI
        'validate_submissions': 'validate_xform',  # "Can Validate" in KC UI
    }
    KC_CONTENT_TYPE_KWARGS = {'app_label': 'logger', 'model': 'xform'}
    # KC records anonymous access as flags on the `XForm`
    KC_ANONYMOUS_PERMISSIONS_XFORM_FLAGS = {
        'view_submissions': {
            'shared': True,
            'shared_data': True
        }
    }

    # todo: test and implement this method
    # def restore_version(self, uid):
    #     _version_to_restore = self.asset_versions.get(uid=uid)
    #     self.content = _version_to_restore.version_content
    #     self.name = _version_to_restore.name

    def to_ss_structure(self):
        return flatten_content(self.content, in_place=False)

    def _populate_summary(self):
        if self.content is None:
            self.content = {}
            self.summary = {}
            return
        analyzer = AssetContentAnalyzer(**self.content)
        self.summary = analyzer.summary

    def adjust_content_on_save(self):
        '''
        This is called on save by default if content exists.
        Can be disabled / skipped by calling with parameter:
        asset.save(adjust_content=False)
        '''
        self._standardize(self.content)

        self._make_default_translation_first(self.content)
        self._strip_empty_rows(self.content)
        self._assign_kuids(self.content)
        self._autoname(self.content)
        self._unlink_list_items(self.content)
        self._remove_empty_expressions(self.content)

        settings = self.content['settings']
        _title = settings.pop('form_title', None)
        id_string = settings.get('id_string')
        filename = self.summary.pop('filename', None)
        if filename:
            # if we have filename available, set the id_string
            # and/or form_title from the filename.
            if not id_string:
                id_string = sluggify_label(filename)
                settings['id_string'] = id_string
            if not _title:
                _title = filename
        if not self.asset_type in [ASSET_TYPE_SURVEY, ASSET_TYPE_TEMPLATE]:
            # instead of deleting the settings, simply clear them out
            self.content['settings'] = {}

        if _title is not None:
            self.name = _title

    def save(self, *args, **kwargs):
        if self.content is None:
            self.content = {}

        # in certain circumstances, we don't want content to
        # be altered on save. (e.g. on asset.deploy())
        if kwargs.pop('adjust_content', True):
            self.adjust_content_on_save()

        # populate summary
        self._populate_summary()

        # infer asset_type only between question and block
        if self.asset_type in [ASSET_TYPE_QUESTION, ASSET_TYPE_BLOCK]:
            row_count = self.summary.get('row_count')
            if row_count == 1:
                self.asset_type = ASSET_TYPE_QUESTION
            elif row_count > 1:
                self.asset_type = ASSET_TYPE_BLOCK

        self._populate_report_styles()

        _create_version = kwargs.pop('create_version', True)
        super(Asset, self).save(*args, **kwargs)

        if _create_version:
            self.asset_versions.create(
                name=self.name,
                version_content=self.content,
                _deployment_data=self._deployment_data,
                # asset_version.deployed is set in the
                # DeploymentSerializer
                deployed=False,
            )

    def rename_translation(self, _from, _to):
        if not self._has_translations(self.content, 2):
            raise ValueError('no translations available')
        self._rename_translation(self.content, _from, _to)

    def to_clone_dict(self, version_uid=None, version=None):
        """
        Returns a dictionary of the asset based on version_uid or version.
        If `version` is specified, there are no needs to provide `version_uid` and make another request to DB.
        :param version_uid: string
        :param version: AssetVersion
        :return: dict
        """

        if not isinstance(version, AssetVersion):
            if version_uid:
                version = self.asset_versions.get(uid=version_uid)
            else:
                version = self.asset_versions.first()

        return {
            'name': version.name,
            'content': version.version_content,
            'asset_type': self.asset_type,
            'tag_string': self.tag_string,
        }

    def clone(self, version_uid=None):
        # not currently used, but this is how "to_clone_dict" should work
        return Asset.objects.create(**self.to_clone_dict(version_uid))

    def revert_to_version(self, version_uid):
        av = self.asset_versions.get(uid=version_uid)
        self.content = av.version_content
        self.save()

    def _populate_report_styles(self):
        default = self.report_styles.get(DEFAULT_REPORTS_KEY, {})
        specifieds = self.report_styles.get(SPECIFIC_REPORTS_KEY, {})
        kuids_to_variable_names = self.report_styles.get('kuid_names', {})
        for (index, row) in enumerate(self.content.get('survey', [])):
            if '$kuid' not in row:
                if 'name' in row:
                    row['$kuid'] = json_hash([self.uid, row['name']])
                else:
                    row['$kuid'] = json_hash([self.uid, index, row])
            _identifier = row.get('name', row['$kuid'])
            kuids_to_variable_names[_identifier] = row['$kuid']
            if _identifier not in specifieds:
                specifieds[_identifier] = {}
        self.report_styles = {
            DEFAULT_REPORTS_KEY: default,
            SPECIFIC_REPORTS_KEY: specifieds,
            'kuid_names': kuids_to_variable_names,
        }

    def get_ancestors_or_none(self):
        # ancestors are ordered from farthest to nearest
        if self.parent is not None:
            return self.parent.get_ancestors(include_self=True)
        else:
            return None

    @property
    def latest_version(self):
        versions = None
        try:
            versions = self.prefetched_latest_versions
        except AttributeError:
            versions = self.asset_versions.order_by('-date_modified')
        try:
            return versions[0]
        except IndexError:
            return None

    @property
    def deployed_versions(self):
        return self.asset_versions.filter(
            deployed=True).order_by('-date_modified')

    @property
    def latest_deployed_version(self):
        return self.deployed_versions.first()

    @property
    def version_id(self):
        # Avoid reading the propery `self.latest_version` more than once, since
        # it may execute a database query each time it's read
        latest_version = self.latest_version
        if latest_version:
            return latest_version.uid

    @property
    def version__content_hash(self):
        # Avoid reading the propery `self.latest_version` more than once, since
        # it may execute a database query each time it's read
        latest_version = self.latest_version
        if latest_version:
            return latest_version.content_hash

    @property
    def snapshot(self):
        return self._snapshot(regenerate=False)

    @transaction.atomic
    def _snapshot(self, regenerate=True):
        asset_version = self.latest_version

        try:
            snapshot = AssetSnapshot.objects.get(asset=self,
                                                 asset_version=asset_version)
            if regenerate:
                snapshot.delete()
                snapshot = False
        except AssetSnapshot.MultipleObjectsReturned:
            # how did multiple snapshots get here?
            snaps = AssetSnapshot.objects.filter(asset=self,
                                                 asset_version=asset_version)
            snaps.delete()
            snapshot = False
        except AssetSnapshot.DoesNotExist:
            snapshot = False

        if not snapshot:
            if self.name != '':
                form_title = self.name
            else:
                _settings = self.content.get('settings', {})
                form_title = _settings.get('id_string', 'Untitled')

            self._append(self.content, settings={
                'form_title': form_title,
            })
            snapshot = AssetSnapshot.objects.create(
                asset=self, asset_version=asset_version, source=self.content)
        return snapshot

    def __unicode__(self):
        return u'{} ({})'.format(self.name, self.uid)

    @property
    def has_active_hooks(self):
        """
        Returns if asset has active hooks.
        Useful to update `kc.XForm.has_kpi_hooks` field.
        :return: {boolean}
        """
        return self.hooks.filter(active=True).exists()

    @staticmethod
    def optimize_queryset_for_list(queryset):
        ''' Used by serializers to improve performance when listing assets '''
        queryset = queryset.defer(
            # Avoid pulling these `JSONField`s from the database because:
            #   * they are stored as plain text, and just deserializing them
            #     to Python objects is CPU-intensive;
            #   * they are often huge;
            #   * we don't need them for list views.
            'content',
            'report_styles'
        ).select_related('owner__username', ).prefetch_related(
            # We previously prefetched `permissions__content_object`, but that
            # actually pulled the entirety of each permission's linked asset
            # from the database! For now, the solution is to remove
            # `content_object` here *and* from
            # `ObjectPermissionNestedSerializer`.
            'permissions__permission',
            'permissions__user',
            # `Prefetch(..., to_attr='prefetched_list')` stores the prefetched
            # related objects in a list (`prefetched_list`) that we can use in
            # other methods to avoid additional queries; see:
            # https://docs.djangoproject.com/en/1.8/ref/models/querysets/#prefetch-objects
            Prefetch('tags', to_attr='prefetched_tags'),
            Prefetch(
                'asset_versions',
                queryset=AssetVersion.objects.order_by('-date_modified').only(
                    'uid', 'asset', 'date_modified', 'deployed'),
                to_attr='prefetched_latest_versions',
            ),
        )
        return queryset