示例#1
0
文件: models.py 项目: wangeek/zamboni
class Website(ModelBase):
    # Identifier used for the initial e.me import.
    moz_id = models.PositiveIntegerField(null=True, unique=True, blank=True)

    # The default_locale used for translated fields. See get_fallback() method
    # below.
    default_locale = models.CharField(max_length=10,
                                      default=settings.LANGUAGE_CODE)
    # The Website URL.
    url = models.URLField(max_length=255, blank=True, null=True)

    # The Website mobile-specific URL, if one exists.
    mobile_url = models.URLField(max_length=255, blank=True, null=True)

    # The Website TV-specific URL, if one exists.
    tv_url = models.URLField(max_length=255, blank=True, null=True)

    # The <title> for the Website, used in search, not exposed to the frontend.
    title = TranslatedField()

    # The name and optionnal short name for the Website, used in the detail
    # page and listing pages, respectively.
    name = TranslatedField()
    short_name = TranslatedField()

    # Developer name (only used for TV)
    developer_name = TranslatedField(null=True)

    # Description.
    description = TranslatedField()

    # Website keywords.
    keywords = models.ManyToManyField(Tag)

    # Regions the website is known to be relevant in, used for search boosting.
    # Stored as a JSON list of ids.
    preferred_regions = JSONField(default=None)

    # Categories, similar to apps. Stored as a JSON list of names.
    categories = JSONField(default=None)

    # Devices, similar to apps. Stored a JSON list of ids.
    devices = JSONField(default=None)

    # Icon content-type.
    icon_type = models.CharField(max_length=25, blank=True)

    # Icon cache-busting hash.
    icon_hash = models.CharField(max_length=8, blank=True)

    # Promo image cache-busting hash.
    promo_img_hash = models.CharField(max_length=8, blank=True, null=True)

    # Date & time the entry was last updated.
    last_updated = models.DateTimeField(db_index=True, auto_now_add=True)

    # Status, similar to apps. See WebsiteManager.valid() above.
    status = models.PositiveIntegerField(choices=STATUS_CHOICES.items(),
                                         default=STATUS_NULL)

    # Whether the website entry is disabled (not shown in frontend, regardless
    # of status) or not.
    is_disabled = models.BooleanField(default=False)

    tv_featured = models.PositiveIntegerField(null=True)
    objects = WebsiteManager()

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

    @classmethod
    def get_fallback(cls):
        return cls._meta.get_field('default_locale')

    @classmethod
    def get_indexer(self):
        return WebsiteIndexer

    def __unicode__(self):
        return unicode(self.url or '(no url set)')

    @property
    def device_names(self):
        return [DEVICE_TYPES[device_id].api_name for device_id in self.devices]

    @property
    def device_types(self):
        return [DEVICE_TYPES[device_id] for device_id in self.devices]

    def is_dummy_content_for_qa(self):
        """
        Returns whether this app is a dummy app used for testing only or not.
        """
        # Change this when we start having dummy websites for QA purposes, see
        # Webapp implementation.
        return False

    def get_icon_dir(self):
        return os.path.join(settings.WEBSITE_ICONS_PATH, str(self.pk / 1000))

    def get_icon_url(self, size):
        icon_name = '{icon}-{{size}}.png'.format(
            icon=DEFAULT_ICONS[self.pk % len(DEFAULT_ICONS)])
        return get_icon_url(static_url('WEBSITE_ICON_URL'),
                            self,
                            size,
                            default_format=icon_name)

    def get_promo_img_dir(self):
        return os.path.join(settings.WEBSITE_PROMO_IMG_PATH,
                            str(self.id / 1000))

    def get_promo_img_url(self, size):
        if not self.promo_img_hash:
            return ''
        return get_promo_img_url(static_url('WEBSITE_PROMO_IMG_URL'),
                                 self,
                                 size,
                                 default_format='website-promo-{size}.png')

    def get_url_path(self):
        return reverse('website.detail', kwargs={'pk': self.pk})

    def get_preferred_regions(self, sort_by='slug'):
        """
        Return a list of region objects the website is preferred in, e.g.::

             [<class 'mkt.constants.regions.GBR'>, ...]

        """
        _regions = map(mkt.regions.REGIONS_CHOICES_ID_DICT.get,
                       self.preferred_regions)
        return sorted(_regions, key=operator.attrgetter(sort_by))
示例#2
0
文件: models.py 项目: wangeek/zamboni
class Extension(ModelBase):
    # Automatically handled fields.
    deleted = models.BooleanField(default=False, editable=False)
    icon_hash = models.CharField(max_length=8, blank=True)
    last_updated = models.DateTimeField(blank=True, null=True, editable=False)
    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES.items(),
                                              default=STATUS_NULL,
                                              editable=False)
    uuid = UUIDField(auto=True, editable=False)

    # Fields for which the manifest is the source of truth - can't be
    # overridden by the API.
    author = models.CharField(default='', editable=False, max_length=128)
    default_language = models.CharField(default=settings.LANGUAGE_CODE,
                                        editable=False,
                                        max_length=10)
    description = TranslatedField(default=None, editable=False)
    name = TranslatedField(default=None, editable=False)

    # Fields that can be modified using the API.
    authors = models.ManyToManyField('users.UserProfile')
    disabled = models.BooleanField(default=False)
    slug = models.CharField(max_length=35, null=True, unique=True)

    objects = ManagerBase.from_queryset(ExtensionQuerySet)()

    manifest_is_source_of_truth_fields = ('author', 'description',
                                          'default_language', 'name')

    class Meta:
        ordering = ('-id', )
        index_together = (('deleted', 'disabled', 'status'), )

    @cached_property(writable=True)
    def latest_public_version(self):
        return self.versions.without_deleted().public().latest('pk')

    @cached_property(writable=True)
    def latest_version(self):
        return self.versions.without_deleted().latest('pk')

    def block(self):
        """Block this Extension.

        When in this state the Extension should not be editable by the
        developers at all; not visible publicly; not searchable by users; but
        should be shown in the developer's dashboard, as 'Blocked'."""
        self.update(status=STATUS_BLOCKED)

    def clean_slug(self):
        return clean_slug(self, slug_field='slug')

    def delete(self, *args, **kwargs):
        """Delete this instance.

        By default, a soft-delete is performed, only hiding the instance from
        the custom manager methods without actually removing it from the
        database. pre_delete and post_delete signals are *not* sent in that
        case. The slug will be set to None during the process.

        Can be overridden by passing `hard_delete=True` keyword argument, in
        which case it behaves like a regular delete() call instead."""
        if self.is_blocked():
            raise BlockedExtensionError
        hard_delete = kwargs.pop('hard_delete', False)
        if hard_delete:
            # Real, hard delete.
            return super(Extension, self).delete(*args, **kwargs)
        # Soft delete.
        # Since we have a unique constraint with slug, set it to None when
        # deleting. Undelete should re-generate it - it might differ from the
        # original slug, but that's why you should be careful when deleting...
        self.update(deleted=True, slug=None)

    @property
    def devices(self):
        """Device ids the Extension is compatible with.

        For now, hardcoded to only return Firefox OS."""
        return [DEVICE_GAIA.id]

    @property
    def device_names(self):
        """Device names the Extension is compatible with.

        Used by the API."""
        return [DEVICE_TYPES[device_id].api_name for device_id in self.devices]

    @classmethod
    def extract_and_validate_upload(cls, upload):
        """Validate and extract manifest from a FileUpload instance.

        Can raise ParseError."""
        with private_storage.open(upload.path) as file_obj:
            # The file will already have been uploaded at this point, so force
            # the content type to make the ExtensionValidator happy. We just
            # need to validate the contents.
            file_obj.content_type = 'application/zip'
            manifest_contents = ExtensionValidator(file_obj).validate()
        return manifest_contents

    @classmethod
    def extract_manifest_fields(cls, manifest_data, fields=None):
        """Extract the specified `fields` from `manifest_data`, applying
        transformations if necessary. If `fields` is absent, then use
        `cls.manifest_is_source_of_truth_fields`."""
        if fields is None:
            fields = cls.manifest_is_source_of_truth_fields
        data = {k: manifest_data[k] for k in fields if k in manifest_data}

        # Determine default language to use for translations.
        # Web Extensions Manifest contains locales (e.g. "en_US"), not
        # languages (e.g. "en-US"). The field is also called differently as a
        # result (default_locale vs default_language), so we need to transform
        # both the key and the value before adding it to data. A default value
        # needs to be set to correctly generate the translated fields below.
        default_language = to_language(
            manifest_data.get('default_locale',
                              cls._meta.get_field('default_language').default))
        if 'default_language' in fields:
            data['default_language'] = default_language

        # Be nice and strip leading / trailing whitespace chars from
        # strings.
        for key, value in data.items():
            if isinstance(value, basestring):
                data[key] = value.strip()

        # Translated fields should not be extracted as simple strings,
        # otherwise we end up setting a locale on the translation that is
        # dependent on the locale of the thread. Use dicts instead, always
        # setting default_language as the language for now (since we don't
        # support i18n in web extensions yet).
        for field in cls._meta.translated_fields:
            field_name = field.name
            if field_name in data:
                data[field_name] = {
                    default_language: manifest_data[field_name]
                }

        return data

    @classmethod
    def from_upload(cls, upload, user=None):
        """Handle creating/editing the Extension instance and saving it to db,
        as well as file operations, from a FileUpload instance. Can throw
        a ParseError or SigningError, so should always be called within a
        try/except."""
        manifest_contents = cls.extract_and_validate_upload(upload)
        data = cls.extract_manifest_fields(manifest_contents)

        # Check for name collision in the same locale for the same user.
        default_language = data['default_language']
        name = data['name'][default_language]
        if user.extension_set.without_deleted().filter(
                name__locale=default_language,
                name__localized_string=name).exists():
            raise ParseError(
                _(u'An Add-on with the same name already exists in your '
                  u'submissions.'))

        # Build a new instance.
        instance = cls.objects.create(**data)

        # Now that the instance has been saved, we can add the author and start
        # saving version data. If everything checks out, a status will be set
        # on the ExtensionVersion we're creating which will automatically be
        # replicated on the Extension instance.
        instance.authors.add(user)
        version = ExtensionVersion.from_upload(
            upload, parent=instance, manifest_contents=manifest_contents)

        # Trigger icon fetch task asynchronously if necessary now that we have
        # an extension and a version.
        if 'icons' in manifest_contents:
            fetch_icon.delay(instance.pk, version.pk)
        return instance

    @classmethod
    def get_fallback(cls):
        """Class method returning the field holding the default language to use
        in translations for this instance.

        *Needs* to be called get_fallback() and *needs* to be a classmethod,
        that's what the translations app requires."""
        return cls._meta.get_field('default_language')

    def get_icon_dir(self):
        return os.path.join(settings.EXTENSION_ICONS_PATH, str(self.pk / 1000))

    def get_icon_url(self, size):
        return get_icon_url(static_url('EXTENSION_ICON_URL'), self, size)

    @classmethod
    def get_indexer(cls):
        return ExtensionIndexer

    def get_url_path(self):
        return reverse('extension.detail', kwargs={'app_slug': self.slug})

    @property
    def icon_type(self):
        return 'png' if self.icon_hash else ''

    def is_blocked(self):
        return self.status == STATUS_BLOCKED

    def is_dummy_content_for_qa(self):
        """
        Returns whether this extension is a dummy extension used for testing
        only or not.

        Used by mkt.search.utils.extract_popularity_trending_boost() - the
        method needs to exist, but we are not using it yet.
        """
        return False

    def is_public(self):
        return (not self.deleted and not self.disabled
                and self.status == STATUS_PUBLIC)

    @property
    def mini_manifest(self):
        """Mini-manifest used for install/update on FxOS devices, in dict form.

        It follows the Mozilla App Manifest format (because that's what FxOS
        requires to install/update add-ons), *not* the Web Extension manifest
        format.
        """
        if self.is_blocked():
            return {}
        # Platform "translates" back the mini-manifest into an app manifest and
        # verifies that some specific key properties in the real manifest match
        # what's found in the mini-manifest. To prevent manifest mismatch
        # errors, we need to copy those properties from the real manifest:
        # name, description and author. To help Firefox OS display useful info
        # to the user we also copy content_scripts and version.
        # We don't bother with locales at the moment, this probably breaks
        # extensions using https://developer.chrome.com/extensions/i18n but
        # we'll deal with that later.
        try:
            version = self.latest_public_version
        except ExtensionVersion.DoesNotExist:
            return {}
        manifest = version.manifest
        mini_manifest = {
            # 'id' here is the uuid, like in sign_file(). This is used by
            # platform to do blocklisting.
            'id': self.uuid,
            'name': manifest['name'],
            'package_path': version.download_url,
            'size': version.size,
            'version': manifest['version']
        }
        if 'author' in manifest:
            # author is copied as a different key to match app manifest format.
            mini_manifest['developer'] = {'name': manifest['author']}
        if 'content_scripts' in manifest:
            mini_manifest['content_scripts'] = manifest['content_scripts']
        if 'description' in manifest:
            mini_manifest['description'] = manifest['description']
        return mini_manifest

    @property
    def mini_manifest_url(self):
        return absolutify(
            reverse('extension.mini_manifest', kwargs={'uuid': self.uuid}))

    def save(self, *args, **kwargs):
        if not self.deleted:
            # Always clean slug before saving, to avoid clashes.
            self.clean_slug()
        return super(Extension, self).save(*args, **kwargs)

    def __unicode__(self):
        return u'%s: %s' % (self.pk, self.name)

    def unblock(self):
        """Unblock this Extension. The original status is restored."""
        self.status = STATUS_NULL
        self.update_status_according_to_versions()

    def undelete(self):
        """Undelete this instance, making it available to all manager methods
        again and restoring its version number.

        Return False if it was not marked as deleted, True otherwise.
        Will re-generate a slug, that might differ from the original one if it
        was taken in the meantime."""
        if not self.deleted:
            return False
        self.clean_slug()
        self.update(deleted=False, slug=self.slug)
        return True

    def update_manifest_fields_from_latest_public_version(self):
        """Update all fields for which the manifest is the source of truth
        with the manifest from the latest public add-on."""
        if self.is_blocked():
            raise BlockedExtensionError
        try:
            version = self.latest_public_version
        except ExtensionVersion.DoesNotExist:
            return
        if not version.manifest:
            return
        # Trigger icon fetch task asynchronously if necessary now that we have
        # an extension and a version.
        if 'icons' in version.manifest:
            fetch_icon.delay(self.pk, version.pk)

        # We need to re-extract the fields from manifest contents because some
        # fields like default_language are transformed before being stored.
        data = self.extract_manifest_fields(version.manifest)
        return self.update(**data)

    def update_status_according_to_versions(self):
        """Update `status`, `latest_version` and `latest_public_version`
        properties depending on the `status` on the ExtensionVersion
        instances attached to this Extension."""
        if self.is_blocked():
            raise BlockedExtensionError

        # If there is a public version available, the extension should be
        # public. If not, and if there is a pending version available, it
        # should be pending. If not, and if there is a rejected version
        # available, it should be rejected. Otherwise it should just be
        # incomplete.
        versions = self.versions.without_deleted()
        if versions.public().exists():
            self.update(status=STATUS_PUBLIC)
        elif versions.pending().exists():
            self.update(status=STATUS_PENDING)
        elif versions.rejected().exists():
            self.update(status=STATUS_REJECTED)
        else:
            self.update(status=STATUS_NULL)
        # Delete latest_version and latest_public_version properties, since
        # they are writable cached_properties they will be reset the next time
        # they are accessed.
        try:
            if self.latest_version:
                del self.latest_version
        except ExtensionVersion.DoesNotExist:
            pass
        try:
            if self.latest_public_version:
                del self.latest_public_version
        except ExtensionVersion.DoesNotExist:
            pass
示例#3
0
class Extension(ModelBase):
    # Fields for which the manifest is the source of truth - can't be
    # overridden by the API.
    default_language = models.CharField(default=settings.LANGUAGE_CODE,
                                        max_length=10)
    manifest = JSONField()
    version = models.CharField(max_length=255, default='')

    # Fields that can be modified using the API.
    authors = models.ManyToManyField('users.UserProfile')
    name = TranslatedField(default=None)
    slug = models.CharField(max_length=35, unique=True)
    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES.items(),
                                              db_index=True,
                                              default=STATUS_NULL)

    def clean_slug(self):
        return clean_slug(self, slug_field='slug')

    @property
    def download_url(self):
        raise NotImplementedError

    @property
    def filename(self):
        return 'extension-%s.zip' % self.version

    @property
    def file_path(self):
        return os.path.join(self.path_prefix, nfd_str(self.filename))

    @classmethod
    def from_upload(cls, upload, instance=None):
        """Handle creating/editing the Extension instance and saving it to db,
        as well as file operations, from a FileUpload instance. Can throw
        a ValidationError or SigningError, so should always be called within a
        try/except."""
        if instance is not None:
            # Not implemented yet. Need to deal with versions correctly, we
            # don't know yet if we want to keep older versions around or not,
            # how status changes etc.
            raise NotImplementedError

        parser = ExtensionParser(upload, instance=instance)
        data = parser.parse()
        fields = ('version', 'name', 'default_language')
        default_locale = data.get('default_locale')

        if default_locale:
            # We actually need language (en-US) for translations, not locale
            # (en_US). The extension contains locales though, so transform the
            # field in the manifest before storing in db.
            data['default_language'] = to_language(default_locale)

        # Filter out parsing results to only keep fields we store in db.
        data = dict((k, v) for k, v in data.items() if k in fields)

        # Build a new instance.
        instance = cls(**data)
        instance.manifest = parser.manifest_contents
        instance.clean_slug()
        instance.save()

        # Now that the instance has been saved, we can generate a file path,
        # move the file and set it to PENDING.
        instance.handle_file_operations(upload)
        instance.update(status=STATUS_PENDING)
        return instance

    @classmethod
    def get_fallback(cls):
        # Class method needed by the translations app.
        return cls._meta.get_field('default_language')

    def get_minifest_contents(self, force=False):
        raise NotImplementedError

    def get_package_path(self):
        raise NotImplementedError

    def handle_file_operations(self, upload):
        """Copy the file attached to a FileUpload to the Extension instance."""
        upload.path = smart_path(nfd_str(upload.path))

        if not self.slug:
            raise RuntimeError(
                'Trying to upload a file belonging to a slugless extension')

        if private_storage.exists(self.file_path):
            # The filename should not exist. If it does, it means we are trying
            # to re-upload the same version. This should have been caught
            # before, so just raise an exception.
            raise RuntimeError(
                'Trying to upload a file to a destination that already exists')

        # Copy file from fileupload. This uses private_storage for now as the
        # unreviewed, unsigned filename is private.
        copy_stored_file(upload.path, self.file_path)

    def is_public(self):
        return self.status == STATUS_PUBLIC

    @property
    def manifest_url(self):
        raise NotImplementedError

    @property
    def path_prefix(self):
        return os.path.join(settings.ADDONS_PATH, 'extensions', str(self.pk))

    def __unicode__(self):
        return u'%s: %s' % (self.pk, self.name)