Exemple #1
0
class FeedCollection(GroupedAppsMixin, BaseFeedCollection, BaseFeedImage):
    """
    Model for "Collections", a type of curated collection that allows more
    complex grouping of apps than an Editorial Brand.
    """
    _apps = models.ManyToManyField(Webapp,
                                   through=FeedCollectionMembership,
                                   related_name='app_feed_collections')
    background_color = models.CharField(max_length=7, null=True, blank=True)
    name = PurifiedField()
    description = PurifiedField(blank=True, null=True)
    type = models.CharField(choices=COLLECTION_TYPE_CHOICES,
                            max_length=30,
                            null=True)

    membership_class = FeedCollectionMembership
    membership_relation = 'feedcollectionmembership'

    class Meta(BaseFeedCollection.Meta):
        abstract = False
        db_table = 'mkt_feed_collection'

    @classmethod
    def get_indexer(self):
        return indexers.FeedCollectionIndexer

    def image_path(self, suffix=''):
        return os.path.join(
            settings.FEED_COLLECTION_BG_PATH, str(self.pk / 1000),
            'feed_collection{suffix}_{pk}.png'.format(suffix=suffix,
                                                      pk=self.pk))
Exemple #2
0
class FeedApp(BaseFeedImage, ModelBase):
    """
    Model for "Custom Featured Apps", a feed item highlighting a single app
    and some additional metadata (e.g. a review or a screenshot).
    """
    app = models.ForeignKey(Webapp)
    description = PurifiedField()
    slug = models.CharField(max_length=30, unique=True)
    color = models.CharField(max_length=20, null=True, blank=True)
    type = models.CharField(choices=FEEDAPP_TYPE_CHOICES, max_length=30)

    # Optionally linked to a Preview (screenshot or video).
    preview = models.ForeignKey(Preview, null=True, blank=True)

    # Optionally linked to a pull quote.
    pullquote_attribution = models.CharField(max_length=50, null=True,
                                             blank=True)
    pullquote_rating = models.PositiveSmallIntegerField(
        null=True, blank=True, validators=[validate_rating])
    pullquote_text = PurifiedField(null=True)

    # Deprecated.
    background_color = ColorField(null=True)

    class Meta:
        db_table = 'mkt_feed_app'

    @classmethod
    def get_indexer(self):
        return indexers.FeedAppIndexer

    def clean(self):
        """
        Require `pullquote_text` if `pullquote_rating` or
        `pullquote_attribution` are set.
        """
        if not self.pullquote_text and (self.pullquote_rating or
                                        self.pullquote_attribution):
            raise ValidationError('Pullquote text required if rating or '
                                  'attribution is defined.')
        super(FeedApp, self).clean()

    def image_path(self, suffix=''):
        return os.path.join(settings.FEATURED_APP_BG_PATH,
                            str(self.pk / 1000),
                            'featured_app{suffix}_{pk}.png'.format(
                                suffix=suffix, pk=self.pk))
Exemple #3
0
class FeedCollectionMembership(BaseFeedCollectionMembership):
    """
    An app's membership to a `FeedBrand` class, used as the through model for
    `FeedBrand._apps`.
    """
    obj = models.ForeignKey('FeedCollection')
    group = PurifiedField(blank=True, null=True)

    class Meta(BaseFeedCollectionMembership.Meta):
        abstract = False
        db_table = 'mkt_feed_collection_membership'
Exemple #4
0
class FeedShelf(BaseFeedCollection, BaseFeedImage):
    """
    Model for "Operator Shelves", a special type of collection that gives
    operators a place to centralize content they wish to feature.
    """
    _apps = models.ManyToManyField(Webapp,
                                   through=FeedShelfMembership,
                                   related_name='app_shelves')
    carrier = models.IntegerField(choices=mkt.carriers.CARRIER_CHOICES)
    description = PurifiedField(null=True)
    name = PurifiedField()
    region = models.PositiveIntegerField(
        choices=mkt.regions.REGIONS_CHOICES_ID)

    # Shelf landing image.
    image_landing_hash = models.CharField(default=None,
                                          max_length=8,
                                          null=True,
                                          blank=True)

    membership_class = FeedShelfMembership
    membership_relation = 'feedshelfmembership'

    class Meta(BaseFeedCollection.Meta):
        abstract = False
        db_table = 'mkt_feed_shelf'

    @classmethod
    def get_indexer(self):
        return indexers.FeedShelfIndexer

    def image_path(self, suffix=''):
        return os.path.join(
            settings.FEED_SHELF_BG_PATH, str(self.pk / 1000),
            'feed_shelf{suffix}_{pk}.png'.format(suffix=suffix, pk=self.pk))

    @property
    def is_published(self):
        return self.feeditem_set.exists()
Exemple #5
0
class Version(ModelBase):
    addon = models.ForeignKey('webapps.Webapp', related_name='versions')
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    has_info_request = models.BooleanField(default=False)
    has_editor_comment = models.BooleanField(default=False)

    deleted = models.BooleanField(default=False)

    supported_locales = models.CharField(max_length=255)

    _developer_name = models.CharField(max_length=255,
                                       default='',
                                       editable=False)

    objects = VersionManager()
    with_deleted = VersionManager(include_deleted=True)

    class Meta(ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        creating = not self.id
        super(Version, self).save(*args, **kw)
        if creating:
            # To avoid circular import.
            from mkt.webapps.models import AppFeatures
            AppFeatures.objects.create(version=self)
        return self

    @classmethod
    def from_upload(cls, upload, addon, send_signal=True):
        data = utils.parse_addon(upload, addon)
        max_len = cls._meta.get_field_by_name('_developer_name')[0].max_length
        developer = data.get('developer_name', '')[:max_len]
        v = cls.objects.create(addon=addon,
                               version=data['version'],
                               _developer_name=developer)
        log.info('New version: %r (%s) from %r' % (v, v.id, upload))

        # To avoid circular import.
        from mkt.webapps.models import AppManifest

        # Note: This must happen before we call `File.from_upload`.
        manifest = utils.WebAppParser().get_json_data(upload)
        AppManifest.objects.create(version=v, manifest=json.dumps(manifest))

        File.from_upload(upload, v, parse_data=data)

        # Update supported locales from manifest.
        # Note: This needs to happen after we call `File.from_upload`.
        update_supported_locales_single.apply_async(
            args=[addon.id],
            kwargs={'latest': True},
            eta=datetime.datetime.now() +
            datetime.timedelta(seconds=settings.NFS_LAG_DELAY))

        v.disable_old_files()
        # After the upload has been copied, remove the upload.
        storage.delete(upload.path)
        if send_signal:
            version_uploaded.send(sender=v)

        # If packaged app and app is blocked, put in escalation queue.
        if addon.is_packaged and addon.status == mkt.STATUS_BLOCKED:
            # To avoid circular import.
            from mkt.reviewers.models import EscalationQueue
            EscalationQueue.objects.create(addon=addon)

        return v

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

    def delete(self):
        log.info(u'Version deleted: %r (%s)' % (self, self.id))
        mkt.log(mkt.LOG.DELETE_VERSION, self.addon, str(self.version))

        models.signals.pre_delete.send(sender=Version, instance=self)

        was_current = False
        if self == self.addon.current_version:
            was_current = True

        self.update(deleted=True)

        # Set file status to disabled.
        f = self.all_files[0]
        f.update(status=mkt.STATUS_DISABLED, _signal=False)
        f.hide_disabled_file()

        # If version deleted was the current version and there now exists
        # another current_version, we need to call some extra methods to update
        # various bits for packaged apps.
        if was_current and self.addon.current_version:
            self.addon.update_name_from_package_manifest()
            self.addon.update_supported_locales()

        if self.addon.is_packaged:
            # Unlink signed packages if packaged app.
            storage.delete(f.signed_file_path)
            log.info(u'Unlinked file: %s' % f.signed_file_path)
            storage.delete(f.signed_reviewer_file_path)
            log.info(u'Unlinked file: %s' % f.signed_reviewer_file_path)

        models.signals.post_delete.send(sender=Version, instance=self)

    @cached_property(writable=True)
    def all_activity(self):
        from mkt.developers.models import VersionLog
        al = (VersionLog.objects.filter(
            version=self.id).order_by('created').select_related(
                'activity_log', 'version'))
        return al

    @cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    @property
    def status(self):
        status_choices = mkt.MKT_STATUS_FILE_CHOICES

        if self.deleted:
            return [status_choices[mkt.STATUS_DELETED]]
        else:
            return [status_choices[f.status] for f in self.all_files]

    @property
    def statuses(self):
        """Unadulterated statuses, good for an API."""
        return [(f.id, f.status) for f in self.all_files]

    def is_public(self):
        # To be public, a version must not be deleted, must belong to a public
        # addon, and all its attached files must have public status.
        try:
            return (not self.deleted and self.addon.is_public()
                    and all(f.status == mkt.STATUS_PUBLIC
                            for f in self.all_files))
        except ObjectDoesNotExist:
            return False

    @property
    def has_files(self):
        return bool(self.all_files)

    @classmethod
    def transformer(cls, versions):
        """Attach all the files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        def rollup(xs):
            groups = sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        file_dict = rollup(File.objects.filter(version__in=ids))

        for version in versions:
            v_id = version.id
            version.all_files = file_dict.get(v_id, [])
            for f in version.all_files:
                f.version = version

    @classmethod
    def transformer_activity(cls, versions):
        """Attach all the activity to the versions."""
        from mkt.developers.models import VersionLog

        ids = set(v.id for v in versions)
        if not versions:
            return

        al = (VersionLog.objects.filter(
            version__in=ids).order_by('created').select_related(
                'activity_log', 'version'))

        def rollup(xs):
            groups = sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        al_dict = rollup(al)

        for version in versions:
            v_id = version.id
            version.all_activity = al_dict.get(v_id, [])

    def disable_old_files(self):
        qs = File.objects.filter(version__addon=self.addon_id,
                                 version__lt=self.id,
                                 version__deleted=False,
                                 status__in=[mkt.STATUS_PENDING])
        # Use File.update so signals are triggered.
        for f in qs:
            f.update(status=mkt.STATUS_DISABLED)

    @property
    def developer_name(self):
        return self._developer_name

    @cached_property(writable=True)
    def is_privileged(self):
        """
        Return whether the corresponding addon is privileged by looking at
        the manifest file.

        This is a cached property, to avoid going in the manifest more than
        once for a given instance. It's also directly writable do allow you to
        bypass the manifest fetching if you *know* your app is privileged or
        not already and want to pass the instance to some code that will use
        that property.
        """
        if not self.addon.is_packaged or not self.all_files:
            return False
        data = self.addon.get_manifest_json(file_obj=self.all_files[0])
        return data.get('type') == 'privileged'

    @cached_property
    def manifest(self):
        # To avoid circular import.
        from mkt.webapps.models import AppManifest

        try:
            manifest = self.manifest_json.manifest
        except AppManifest.DoesNotExist:
            manifest = None

        return json.loads(manifest) if manifest else {}
Exemple #6
0
class FeedCollection(BaseFeedCollection, BaseFeedImage):
    """
    Model for "Collections", a type of curated collection that allows more
    complex grouping of apps than an Editorial Brand.
    """
    _apps = models.ManyToManyField(Webapp,
                                   through=FeedCollectionMembership,
                                   related_name='app_feed_collections')
    background_color = models.CharField(max_length=7, null=True, blank=True)
    name = PurifiedField()
    description = PurifiedField(blank=True, null=True)
    type = models.CharField(choices=COLLECTION_TYPE_CHOICES,
                            max_length=30,
                            null=True)

    membership_class = FeedCollectionMembership
    membership_relation = 'feedcollectionmembership'

    class Meta(BaseFeedCollection.Meta):
        abstract = False
        db_table = 'mkt_feed_collection'

    @classmethod
    def get_indexer(self):
        return indexers.FeedCollectionIndexer

    def image_path(self, suffix=''):
        return os.path.join(
            settings.FEED_COLLECTION_BG_PATH, str(self.pk / 1000),
            'feed_collection{suffix}_{pk}.png'.format(suffix=suffix,
                                                      pk=self.pk))

    def add_app_grouped(self, app, group, order=None):
        """
        Add an app to this collection, as a member of the passed `group`.

        If specified, the app will be created with the specified `order`. If
        not, it will be added to the end of the collection.
        """
        qs = self.membership_class.objects.filter(obj=self)
        if order is None:
            aggregate = qs.aggregate(models.Max('order'))['order__max']
            order = aggregate + 1 if aggregate is not None else 0

        rval = self.membership_class.objects.create(obj_id=self.id,
                                                    app_id=app,
                                                    group=group,
                                                    order=order)

        # Help django-cache-machine: it doesn't like many 2 many relations,
        # the cache is never invalidated properly when adding a new object.
        self.membership_class.objects.invalidate(*qs)

        index_webapps.delay([app])

        return rval

    def set_apps_grouped(self, new_apps):
        for app in self.apps().no_cache().values_list('pk', flat=True):
            self.remove_app(Webapp.objects.get(pk=app))
        for group in new_apps:
            for app in group['apps']:
                self.add_app_grouped(app, group['name'])
Exemple #7
0
class Collection(ModelBase):
    # `collection_type` for rocketfuel, not transonic.
    collection_type = models.IntegerField(choices=COLLECTION_TYPES)
    description = PurifiedField()
    name = PurifiedField()
    is_public = models.BooleanField(default=False)
    category = models.CharField(default=None,
                                null=True,
                                blank=True,
                                max_length=30,
                                choices=CATEGORY_CHOICES)
    region = models.PositiveIntegerField(
        default=None,
        null=True,
        blank=True,
        choices=mkt.regions.REGIONS_CHOICES_ID,
        db_index=True)
    carrier = models.IntegerField(default=None,
                                  null=True,
                                  blank=True,
                                  choices=mkt.carriers.CARRIER_CHOICES,
                                  db_index=True)
    author = models.CharField(max_length=255, default='', blank=True)
    slug = models.CharField(blank=True,
                            max_length=30,
                            help_text='Used in collection URLs.')
    default_language = models.CharField(
        max_length=10,
        choices=((to_language(lang), desc)
                 for lang, desc in settings.LANGUAGES.items()),
        default=to_language(settings.LANGUAGE_CODE))
    curators = models.ManyToManyField('users.UserProfile')
    background_color = ColorField(null=True)
    text_color = ColorField(null=True)
    image_hash = models.CharField(default=None, max_length=8, null=True)
    can_be_hero = models.BooleanField(
        default=False,
        help_text=
        ('Indicates whether an operator shelf collection can be displayed with'
         'a hero graphic'))
    _apps = models.ManyToManyField(Webapp,
                                   through='CollectionMembership',
                                   related_name='app_collections')

    objects = ManagerBase()
    public = PublicCollectionsManager()

    class Meta:
        db_table = 'app_collections'
        # This ordering will change soon since we'll need to be able to order
        # collections themselves, but this helps tests for now.
        ordering = ('-id', )

    def __unicode__(self):
        return self.name.localized_string_clean

    def save(self, **kw):
        self.clean_slug()
        return super(Collection, self).save(**kw)

    @use_master
    def clean_slug(self):
        clean_slug(self, 'slug')

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

    def image_path(self, suffix=''):
        # The argument `suffix` isn't used here but is in the feed.
        return os.path.join(settings.COLLECTIONS_ICON_PATH,
                            str(self.pk / 1000),
                            'app_collection_%s.png' % (self.pk, ))

    def apps(self):
        """
        Public apps on the collection, ordered by their position in the
        CollectionMembership model.

        Use this method everytime you want to display apps for a collection to
        an user.
        """
        return self._apps.filter(
            disabled_by_user=False,
            status=amo.STATUS_PUBLIC).order_by('collectionmembership')

    def add_app(self, app, order=None):
        """
        Add an app to this collection. If specified, the app will be created
        with the specified `order`. If not, it will be added to the end of the
        collection.
        """
        qs = CollectionMembership.objects.filter(collection=self)
        if order is None:
            aggregate = qs.aggregate(models.Max('order'))['order__max']
            order = aggregate + 1 if aggregate is not None else 0
        rval = CollectionMembership.objects.create(collection=self,
                                                   app=app,
                                                   order=order)
        # Help django-cache-machine: it doesn't like many 2 many relations,
        # the cache is never invalidated properly when adding a new object.
        CollectionMembership.objects.invalidate(*qs)
        index_webapps.delay([app.pk])
        return rval

    def remove_app(self, app):
        """
        Remove the passed app from this collection, returning a boolean
        indicating whether a successful deletion took place.
        """
        try:
            membership = self.collectionmembership_set.get(app=app)
        except CollectionMembership.DoesNotExist:
            return False
        else:
            membership.delete()
            index_webapps.delay([app.pk])
            return True

    def reorder(self, new_order):
        """
        Passed a list of app IDs, e.g.

        [18, 24, 9]

        will change the order of each item in the collection to match the
        passed order. A ValueError will be raised if each app in the
        collection is not included in the ditionary.
        """
        existing_pks = self.apps().no_cache().values_list('pk', flat=True)
        if set(existing_pks) != set(new_order):
            raise ValueError('Not all apps included')
        for order, pk in enumerate(new_order):
            CollectionMembership.objects.get(collection=self,
                                             app_id=pk).update(order=order)
        index_webapps.delay(new_order)

    def has_curator(self, userprofile):
        """
        Returns boolean indicating whether the passed user profile is a curator
        on this collection.

        ID comparison used instead of directly checking objects to ensure that
        UserProfile objects could be passed.
        """
        return userprofile.id in self.curators.values_list('id', flat=True)

    def add_curator(self, userprofile):
        ret = self.curators.add(userprofile)
        Collection.objects.invalidate(*self.curators.all())
        return ret

    def remove_curator(self, userprofile):
        ret = self.curators.remove(userprofile)
        Collection.objects.invalidate(*self.curators.all())
        return ret
Exemple #8
0
class FancyModel(ModelBase):
    """Mix it up with purified and linkified fields."""
    purified = PurifiedField()
    linkified = LinkifiedField()

    objects = ManagerWithTranslations()
Exemple #9
0
class FancyModel(ModelBase):
    """Mix it up with purified and linkified fields."""
    purified = PurifiedField()
    linkified = LinkifiedField()