Exemplo n.º 1
0
class TranslatedModel(ModelBase):
    name = TranslatedField()
    description = TranslatedField()
    default_locale = models.CharField(max_length=10)
    no_locale = TranslatedField(require_locale=False)

    objects = ManagerBase()
Exemplo n.º 2
0
def test_user_foreign_key_field_deconstruct():
    field = TranslatedField(require_locale=False)
    name, path, args, kwargs = field.deconstruct()
    new_field_instance = TranslatedField(require_locale=False)

    assert kwargs['require_locale'] == new_field_instance.require_locale
    assert kwargs['to'] == new_field_instance.to
    assert kwargs['short'] == new_field_instance.short
Exemplo n.º 3
0
def test_user_foreign_key_field_deconstruct():
    field = TranslatedField(require_locale=False)
    name, path, args, kwargs = field.deconstruct()
    new_field_instance = TranslatedField(require_locale=False)

    assert kwargs['require_locale'] == new_field_instance.require_locale
    assert kwargs['to'] == new_field_instance.to
    assert kwargs['short'] == new_field_instance.short
Exemplo n.º 4
0
class TranslatedModel(ModelBase):
    name = TranslatedField()
    description = TranslatedField()
    default_locale = models.CharField(max_length=10)
    no_locale = TranslatedField(require_locale=False)

    translated_through_fk = models.ForeignKey(
        'TranslatedModelLinkedAsForeignKey', null=True, default=None)
    objects = ManagerBase()
Exemplo n.º 5
0
class CannedResponse(ModelBase):

    name = TranslatedField()
    response = TranslatedField(short=False)
    sort_group = models.CharField(max_length=255)
    type = models.PositiveIntegerField(
        choices=amo.CANNED_RESPONSE_CHOICES.items(), db_index=True, default=0)

    class Meta:
        db_table = 'cannedresponses'

    def __unicode__(self):
        return unicode(self.name)
Exemplo n.º 6
0
def test_translated_field_supports_migration():
    """Tests serializing translated field in a simple migration.

    Since `TranslatedField` is a ForeignKey migrations pass `to=` explicitly
    and we have to pop it in our __init__.
    """
    fields = {
        'charfield': TranslatedField(),
    }

    migration = type(
        str('Migration'),
        (migrations.Migration,),
        {
            'operations': [
                migrations.CreateModel(
                    name='MyModel', fields=tuple(fields.items()), bases=(models.Model,)
                ),
            ],
        },
    )
    writer = MigrationWriter(migration)
    output = writer.as_string()

    # Just make sure it runs and that things look alright.
    result = safe_exec(output, globals_=globals())

    assert 'Migration' in result
Exemplo n.º 7
0
class License(ModelBase):
    OTHER = 0

    name = TranslatedField(db_column='name')
    url = models.URLField(null=True, db_column='url')
    builtin = models.PositiveIntegerField(default=OTHER)
    text = LinkifiedField()
    on_form = models.BooleanField(
        default=False, help_text='Is this a license choice in the devhub?')
    some_rights = models.BooleanField(
        default=False,
        help_text='Show "Some Rights Reserved" instead of the license name?')
    icons = models.CharField(
        max_length=255, null=True,
        help_text='Space-separated list of icon identifiers.')
    creative_commons = models.BooleanField(default=False)

    objects = LicenseManager()

    class Meta:
        db_table = 'licenses'

    def __unicode__(self):
        license = self._constant or self
        return unicode(license.name)

    @property
    def _constant(self):
        return LICENSES_BY_BUILTIN.get(self.builtin)
Exemplo n.º 8
0
class License(ModelBase):
    OTHER = 0

    id = PositiveAutoField(primary_key=True)
    name = TranslatedField()
    url = models.URLField(null=True)
    builtin = models.PositiveIntegerField(default=OTHER)
    text = LinkifiedField()
    on_form = models.BooleanField(
        default=False, help_text='Is this a license choice in the devhub?')
    some_rights = models.BooleanField(
        default=False,
        help_text='Show "Some Rights Reserved" instead of the license name?',
    )
    icons = models.CharField(
        max_length=255,
        null=True,
        help_text='Space-separated list of icon identifiers.')
    creative_commons = models.BooleanField(default=False)

    objects = LicenseManager()

    class Meta:
        db_table = 'licenses'
        indexes = [models.Index(fields=('builtin', ), name='builtin_idx')]

    def __str__(self):
        license = self._constant or self
        return str(license.name)

    @property
    def _constant(self):
        return LICENSES_BY_BUILTIN.get(self.builtin)
Exemplo n.º 9
0
class WebextPermissionDescription(ModelBase):
    MATCH_ALL_REGEX = r'^\<all_urls\>|(\*|http|https):\/\/\*\/'
    ALL_URLS_PERMISSION = Permission(u'all_urls',
                                     _(u'Access your data for all websites'))
    name = models.CharField(max_length=255, unique=True)
    description = TranslatedField()

    class Meta:
        db_table = 'webext_permission_descriptions'
        ordering = ['name']
Exemplo n.º 10
0
class HubPromo(ModelBase):
    VISIBILITY_CHOICES = (
        (0, 'Nobody'),
        (1, 'Visitors'),
        (2, 'Developers'),
        (3, 'Visitors and Developers'),
    )

    heading = TranslatedField()
    body = TranslatedField()
    visibility = models.SmallIntegerField(choices=VISIBILITY_CHOICES)

    class Meta:
        db_table = 'hubpromos'

    def __unicode__(self):
        return unicode(self.heading)

    def flush_urls(self):
        return ['*/developers*']
Exemplo n.º 11
0
class License(ModelBase):
    OTHER = 0

    id = PositiveAutoField(primary_key=True)
    name = TranslatedField()
    url = models.URLField(null=True)
    builtin = models.PositiveIntegerField(default=OTHER)
    text = LinkifiedField()
    on_form = models.BooleanField(
        default=False, help_text='Is this a license choice in the devhub?')

    objects = LicenseManager()

    class Meta:
        db_table = 'licenses'
        indexes = [models.Index(fields=('builtin', ), name='builtin_idx')]

    def __str__(self):
        license = self._constant or self
        return str(license.name)

    @property
    def _constant(self):
        return LICENSES_BY_BUILTIN.get(self.builtin)

    @property
    def creative_commons(self):
        return bool((constant := self._constant) and constant.creative_commons)

    @property
    def icons(self):
        return ((constant := self._constant) and constant.icons) or ''

    @property
    def slug(self):
        return ((constant := self._constant) and constant.slug) or None
Exemplo n.º 12
0
class Collection(ModelBase):
    id = PositiveAutoField(primary_key=True)
    TYPE_CHOICES = amo.COLLECTION_CHOICES.items()

    uuid = models.UUIDField(blank=True, unique=True, null=True)
    name = TranslatedField(require_locale=False)
    # nickname is deprecated.  Use slug.
    nickname = models.CharField(max_length=30,
                                blank=True,
                                unique=True,
                                null=True)
    slug = models.CharField(max_length=30, blank=True, null=True)

    description = NoLinksNoMarkupField(require_locale=False)
    default_locale = models.CharField(max_length=10,
                                      default='en-US',
                                      db_column='defaultlocale')
    type = models.PositiveIntegerField(db_column='collection_type',
                                       choices=TYPE_CHOICES,
                                       default=0)

    listed = models.BooleanField(
        default=True, help_text='Collections are either listed or private.')

    application = models.PositiveIntegerField(choices=amo.APPS_CHOICES,
                                              db_column='application_id',
                                              blank=True,
                                              null=True,
                                              db_index=True)
    addon_count = models.PositiveIntegerField(default=0,
                                              db_column='addonCount')

    all_personas = models.BooleanField(
        default=False, help_text='Does this collection only contain Themes?')

    addons = models.ManyToManyField(Addon,
                                    through='CollectionAddon',
                                    related_name='collections')
    author = models.ForeignKey(UserProfile,
                               null=True,
                               related_name='collections')

    objects = CollectionManager()

    class Meta(ModelBase.Meta):
        db_table = 'collections'
        unique_together = (('author', 'slug'), )

    def __str__(self):
        return u'%s (%s)' % (self.name, self.addon_count)

    def save(self, **kw):
        if not self.uuid:
            self.uuid = uuid.uuid4()
        if not self.slug:
            # Work with both, strings (if passed manually on .create()
            # and UUID instances)
            self.slug = str(self.uuid).replace('-', '')[:30]
        self.clean_slug()

        super(Collection, self).save(**kw)

    def clean_slug(self):
        if self.type in SPECIAL_SLUGS:
            self.slug = SPECIAL_SLUGS[self.type]
            return

        if self.slug in SPECIAL_SLUGS.values():
            self.slug += '~'

        if not self.author:
            return

        qs = self.author.collections.using('default')
        slugs = dict((slug, id) for slug, id in qs.values_list('slug', 'id'))
        if self.slug in slugs and slugs[self.slug] != self.id:
            for idx in range(len(slugs)):
                new = '%s-%s' % (self.slug, idx + 1)
                if new not in slugs:
                    self.slug = new
                    return

    def get_url_path(self):
        return reverse('collections.detail', args=[self.author_id, self.slug])

    def get_abs_url(self):
        return absolutify(self.get_url_path())

    def edit_url(self):
        return reverse('collections.edit', args=[self.author_id, self.slug])

    def delete_url(self):
        return reverse('collections.delete', args=[self.author_id, self.slug])

    def share_url(self):
        return reverse('collections.share', args=[self.author_id, self.slug])

    def stats_url(self):
        return reverse('collections.stats', args=[self.author_id, self.slug])

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

    def set_addons(self, addon_ids, comments=None):
        """Replace the current add-ons with a new list of add-on ids."""
        if comments is None:
            comments = {}
        order = {a: idx for idx, a in enumerate(addon_ids)}

        # Partition addon_ids into add/update/remove buckets.
        existing = set(
            self.addons.using('default').values_list('id', flat=True))
        add, update = [], []
        for addon in addon_ids:
            bucket = update if addon in existing else add
            bucket.append((addon, order[addon]))
        remove = existing.difference(addon_ids)
        now = datetime.now()

        with connection.cursor() as cursor:
            if remove:
                cursor.execute("DELETE FROM addons_collections "
                               "WHERE collection_id=%s AND addon_id IN (%s)" %
                               (self.id, ','.join(map(str, remove))))
                if self.listed:
                    for addon in remove:
                        activity.log_create(amo.LOG.REMOVE_FROM_COLLECTION,
                                            (Addon, addon), self)
            if add:
                insert = '(%s, %s, %s, NOW(), NOW())'
                values = [insert % (a, self.id, idx) for a, idx in add]
                cursor.execute("""
                    INSERT INTO addons_collections
                        (addon_id, collection_id, ordering, created, modified)
                    VALUES %s""" % ','.join(values))
                if self.listed:
                    for addon_id, idx in add:
                        activity.log_create(amo.LOG.ADD_TO_COLLECTION,
                                            (Addon, addon_id), self)
        for addon, ordering in update:
            (CollectionAddon.objects.filter(
                collection=self.id, addon=addon).update(ordering=ordering,
                                                        modified=now))

        for addon, comment in six.iteritems(comments):
            try:
                c = (CollectionAddon.objects.using('default').get(
                    collection=self.id, addon=addon))
            except CollectionAddon.DoesNotExist:
                pass
            else:
                c.comments = comment
                c.save(force_update=True)

        self.save()

    def add_addon(self, addon):
        "Adds an addon to the collection."
        CollectionAddon.objects.get_or_create(addon=addon, collection=self)
        if self.listed:
            activity.log_create(amo.LOG.ADD_TO_COLLECTION, addon, self)
        self.save()  # To invalidate Collection.

    def remove_addon(self, addon):
        CollectionAddon.objects.filter(addon=addon, collection=self).delete()
        if self.listed:
            activity.log_create(amo.LOG.REMOVE_FROM_COLLECTION, addon, self)
        self.save()  # To invalidate Collection.

    def owned_by(self, user):
        return user.id == self.author_id

    def can_view_stats(self, request):
        if request and request.user:
            return (self.owned_by(request.user) or acl.action_allowed(
                request, amo.permissions.COLLECTION_STATS_VIEW))
        return False

    def is_public(self):
        return self.listed

    def is_featured(self):
        return FeaturedCollection.objects.filter(collection=self).exists()

    @staticmethod
    def transformer(collections):
        if not collections:
            return
        author_ids = set(c.author_id for c in collections)
        authors = dict(
            (u.id, u) for u in UserProfile.objects.filter(id__in=author_ids))
        for c in collections:
            c.author = authors.get(c.author_id)

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        if kwargs.get('raw'):
            return
        tasks.collection_meta.delay(instance.id)
        if instance.is_featured():
            Collection.update_featured_status(sender, instance, **kwargs)

    @staticmethod
    def post_delete(sender, instance, **kwargs):
        if kwargs.get('raw'):
            return
        if instance.is_featured():
            Collection.update_featured_status(sender, instance, **kwargs)

    @staticmethod
    def update_featured_status(sender, instance, **kwargs):
        from olympia.addons.tasks import index_addons
        addons = kwargs.get('addons',
                            [addon.id for addon in instance.addons.all()])
        if addons:
            clear_get_featured_ids_cache(None, None)
            index_addons.delay(addons)

    def check_ownership(self, request, require_owner, require_author,
                        ignore_disabled, admin):
        """
        Used by acl.check_ownership to see if request.user has permissions for
        the collection.
        """
        from olympia.access import acl
        return acl.check_collection_ownership(request, self, require_owner)
Exemplo n.º 13
0
class TranslatedModelWithDefaultNull(ModelBase):
    name = TranslatedField(default=None)

    objects = ManagerBase()
Exemplo n.º 14
0
class Rating(ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='_ratings')
    version = models.ForeignKey('versions.Version',
                                related_name='ratings',
                                null=True)
    user = models.ForeignKey('users.UserProfile', related_name='_ratings_all')
    reply_to = models.OneToOneField('self',
                                    null=True,
                                    related_name='reply',
                                    db_column='reply_to')

    rating = models.PositiveSmallIntegerField(null=True)
    title = TranslatedField(require_locale=False)
    body = TranslatedField(require_locale=False)
    ip_address = models.CharField(max_length=255, default='0.0.0.0')

    editorreview = models.BooleanField(default=False)
    flag = models.BooleanField(default=False)

    deleted = models.BooleanField(default=False)

    # Denormalized fields for easy lookup queries.
    # TODO: index on addon, user, latest
    is_latest = models.BooleanField(
        default=True,
        editable=False,
        help_text="Is this the user's latest rating for the add-on?")
    previous_count = models.PositiveIntegerField(
        default=0,
        editable=False,
        help_text="How many previous ratings by the user for this add-on?")

    # The order of those managers is very important: please read the lengthy
    # comment above the Addon managers declaration/instantiation.
    unfiltered = RatingManager(include_deleted=True)
    objects = RatingManager()
    without_replies = WithoutRepliesRatingManager()

    class Meta:
        db_table = 'reviews'
        ordering = ('-created', )

    def __unicode__(self):
        if self.title:
            return unicode(self.title)
        return truncate(unicode(self.body), 10)

    def __init__(self, *args, **kwargs):
        user_responsible = kwargs.pop('user_responsible', None)
        super(Rating, self).__init__(*args, **kwargs)
        if user_responsible is not None:
            self.user_responsible = user_responsible

    def get_url_path(self):
        return jinja_helpers.url('addons.ratings.detail', self.addon.slug,
                                 self.id)

    def approve(self, user):
        from olympia.reviewers.models import ReviewerScore

        activity.log_create(amo.LOG.APPROVE_RATING,
                            self.addon,
                            self,
                            user=user,
                            details=dict(
                                title=unicode(self.title),
                                body=unicode(self.body),
                                addon_id=self.addon.pk,
                                addon_title=unicode(self.addon.name),
                                is_flagged=self.ratingflag_set.exists()))
        for flag in self.ratingflag_set.all():
            flag.delete()
        self.editorreview = False
        # We've already logged what we want to log, no need to pass
        # user_responsible=user.
        self.save()
        ReviewerScore.award_moderation_points(user, self.addon, self.pk)

    def delete(self, user_responsible=None):
        if user_responsible is None:
            user_responsible = self.user

        rating_was_moderated = False
        # Log deleting ratings to moderation log,
        # except if the author deletes it
        if user_responsible != self.user:
            # Remember moderation state
            rating_was_moderated = True
            from olympia.reviewers.models import ReviewerScore

            activity.log_create(amo.LOG.DELETE_RATING,
                                self.addon,
                                self,
                                user=user_responsible,
                                details=dict(
                                    title=unicode(self.title),
                                    body=unicode(self.body),
                                    addon_id=self.addon.pk,
                                    addon_title=unicode(self.addon.name),
                                    is_flagged=self.ratingflag_set.exists()))
            for flag in self.ratingflag_set.all():
                flag.delete()

        log.info(u'Rating deleted: %s deleted id:%s by %s ("%s": "%s")',
                 user_responsible.name, self.pk, self.user.name,
                 unicode(self.title), unicode(self.body))
        self.update(deleted=True)
        # Force refreshing of denormalized data (it wouldn't happen otherwise
        # because we're not dealing with a creation).
        self.update_denormalized_fields()

        if rating_was_moderated:
            ReviewerScore.award_moderation_points(user_responsible, self.addon,
                                                  self.pk)

    def undelete(self):
        self.update(deleted=False)
        # Force refreshing of denormalized data (it wouldn't happen otherwise
        # because we're not dealing with a creation).
        self.update_denormalized_fields()

    @classmethod
    def get_replies(cls, ratings):
        ratings = [r.id for r in ratings]
        qs = Rating.objects.filter(reply_to__in=ratings)
        return dict((r.reply_to_id, r) for r in qs)

    def send_notification_email(self):
        if self.reply_to:
            # It's a reply.
            reply_url = jinja_helpers.url('addons.ratings.detail',
                                          self.addon.slug,
                                          self.reply_to.pk,
                                          add_prefix=False)
            data = {
                'name': self.addon.name,
                'reply_title': self.title,
                'reply': self.body,
                'rating_url': jinja_helpers.absolutify(reply_url)
            }
            recipients = [self.reply_to.user.email]
            subject = u'Mozilla Add-on Developer Reply: %s' % self.addon.name
            template = 'ratings/emails/reply_review.ltxt'
            perm_setting = 'reply'
        else:
            # It's a new rating.
            rating_url = jinja_helpers.url('addons.ratings.detail',
                                           self.addon.slug,
                                           self.pk,
                                           add_prefix=False)
            data = {
                'name': self.addon.name,
                'rating': self,
                'rating_url': jinja_helpers.absolutify(rating_url)
            }
            recipients = [author.email for author in self.addon.authors.all()]
            subject = u'Mozilla Add-on User Rating: %s' % self.addon.name
            template = 'ratings/emails/new_rating.txt'
            perm_setting = 'new_review'
        send_mail_jinja(subject,
                        template,
                        data,
                        recipient_list=recipients,
                        perm_setting=perm_setting)

    @staticmethod
    def post_save(sender, instance, created, **kwargs):
        if kwargs.get('raw'):
            return

        if hasattr(instance, 'user_responsible'):
            # user_responsible is not a field on the model, so it's not
            # persistent: it's just something the views will set temporarily
            # when manipulating a Rating that indicates a real user made that
            # change.
            action = 'New' if created else 'Edited'
            if instance.reply_to:
                log.debug('%s reply to %s: %s' %
                          (action, instance.reply_to_id, instance.pk))
            else:
                log.debug('%s rating: %s' % (action, instance.pk))

            # For new ratings - not replies - and all edits (including replies
            # this time) by users we want to insert a new ActivityLog.
            new_rating_or_edit = not instance.reply_to or not created
            if new_rating_or_edit:
                action = amo.LOG.ADD_RATING if created else amo.LOG.EDIT_RATING
                activity.log_create(action,
                                    instance.addon,
                                    instance,
                                    user=instance.user_responsible)

            # For new ratings and new replies we want to send an email.
            if created:
                instance.send_notification_email()

        instance.refresh(update_denorm=created)

    def refresh(self, update_denorm=False):
        from olympia.addons.models import update_search_index
        from . import tasks

        if update_denorm:
            # Do this immediately so is_latest is correct.
            self.update_denormalized_fields()

        # Rating counts have changed, so run the task and trigger a reindex.
        tasks.addon_rating_aggregates.delay(self.addon_id)
        update_search_index(self.addon.__class__, self.addon)

    def update_denormalized_fields(self):
        from . import tasks

        pair = self.addon_id, self.user_id
        tasks.update_denorm(pair)
Exemplo n.º 15
0
class TranslatedModelLinkedAsForeignKey(ModelBase):
    name = TranslatedField()
Exemplo n.º 16
0
class Review(ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='_reviews')
    version = models.ForeignKey('versions.Version', related_name='reviews',
                                null=True)
    user = models.ForeignKey('users.UserProfile', related_name='_reviews_all')
    reply_to = models.ForeignKey('self', null=True, unique=True,
                                 related_name='replies', db_column='reply_to')

    rating = models.PositiveSmallIntegerField(null=True)
    title = TranslatedField(require_locale=False)
    body = TranslatedField(require_locale=False)
    ip_address = models.CharField(max_length=255, default='0.0.0.0')

    editorreview = models.BooleanField(default=False)
    flag = models.BooleanField(default=False)
    sandbox = models.BooleanField(default=False)
    client_data = models.ForeignKey('stats.ClientData', null=True, blank=True)

    deleted = models.BooleanField(default=False)

    # Denormalized fields for easy lookup queries.
    # TODO: index on addon, user, latest
    is_latest = models.BooleanField(
        default=True, editable=False,
        help_text="Is this the user's latest review for the add-on?")
    previous_count = models.PositiveIntegerField(
        default=0, editable=False,
        help_text="How many previous reviews by the user for this add-on?")

    # The order of those managers is very important: please read the lengthy
    # comment above the Addon managers declaration/instanciation.
    unfiltered = ReviewManager(include_deleted=True)
    objects = ReviewManager()

    class Meta:
        db_table = 'reviews'
        ordering = ('-created',)

    def get_url_path(self):
        return helpers.url('addons.reviews.detail', self.addon.slug, self.id)

    def flush_urls(self):
        urls = ['*/addon/%d/' % self.addon_id,
                '*/addon/%d/reviews/' % self.addon_id,
                '*/addon/%d/reviews/format:rss' % self.addon_id,
                '*/addon/%d/reviews/%d/' % (self.addon_id, self.id),
                '*/user/%d/' % self.user_id, ]
        return urls

    def delete(self):
        self.update(deleted=True)
        # This should happen in the `post_save` hook.
        # self.refresh(update_denorm=True)

    def undelete(self):
        self.update(deleted=False)
        # This should happen in the `post_save` hook.
        # self.refresh(update_denorm=True)

    @classmethod
    def get_replies(cls, reviews):
        reviews = [r.id for r in reviews]
        qs = Review.objects.filter(reply_to__in=reviews)
        return dict((r.reply_to_id, r) for r in qs)

    @staticmethod
    def post_save(sender, instance, created, **kwargs):
        if kwargs.get('raw'):
            return
        instance.refresh(update_denorm=created)
        if created:
            # Avoid slave lag with the delay.
            check_spam.apply_async(args=[instance.id], countdown=600)

    def refresh(self, update_denorm=False):
        from olympia.addons.models import update_search_index
        from . import tasks

        if update_denorm:
            pair = self.addon_id, self.user_id
            # Do this immediately so is_latest is correct. Use default
            # to avoid slave lag.
            tasks.update_denorm(pair, using='default')

        # Review counts have changed, so run the task and trigger a reindex.
        tasks.addon_review_aggregates.delay(self.addon_id, using='default')
        update_search_index(self.addon.__class__, self.addon)

    @staticmethod
    def transformer(reviews):
        user_ids = dict((r.user_id, r) for r in reviews)
        for user in UserProfile.objects.no_cache().filter(id__in=user_ids):
            user_ids[user.id].user = user
Exemplo n.º 17
0
class Collection(ModelBase):
    TYPE_CHOICES = amo.COLLECTION_CHOICES.items()

    # TODO: Use models.UUIDField but it uses max_length=32 hex (no hyphen)
    # uuids so needs some migration.
    uuid = models.CharField(max_length=36, blank=True, unique=True)
    name = TranslatedField(require_locale=False)
    # nickname is deprecated.  Use slug.
    nickname = models.CharField(max_length=30,
                                blank=True,
                                unique=True,
                                null=True)
    slug = models.CharField(max_length=30, blank=True, null=True)

    description = NoLinksNoMarkupField(require_locale=False)
    default_locale = models.CharField(max_length=10,
                                      default='en-US',
                                      db_column='defaultlocale')
    type = models.PositiveIntegerField(db_column='collection_type',
                                       choices=TYPE_CHOICES,
                                       default=0)
    icontype = models.CharField(max_length=25, blank=True)

    listed = models.BooleanField(
        default=True, help_text='Collections are either listed or private.')

    subscribers = models.PositiveIntegerField(default=0)
    downloads = models.PositiveIntegerField(default=0)
    weekly_subscribers = models.PositiveIntegerField(default=0)
    monthly_subscribers = models.PositiveIntegerField(default=0)
    application = models.PositiveIntegerField(choices=amo.APPS_CHOICES,
                                              db_column='application_id',
                                              null=True,
                                              db_index=True)
    addon_count = models.PositiveIntegerField(default=0,
                                              db_column='addonCount')

    upvotes = models.PositiveIntegerField(default=0)
    downvotes = models.PositiveIntegerField(default=0)
    rating = models.FloatField(default=0)
    all_personas = models.BooleanField(
        default=False, help_text='Does this collection only contain Themes?')

    addons = models.ManyToManyField(Addon,
                                    through='CollectionAddon',
                                    related_name='collections')
    author = models.ForeignKey(UserProfile,
                               null=True,
                               related_name='collections')
    users = models.ManyToManyField(UserProfile,
                                   through='CollectionUser',
                                   related_name='collections_publishable')

    objects = CollectionManager()

    top_tags = TopTags()

    class Meta(ModelBase.Meta):
        db_table = 'collections'
        unique_together = (('author', 'slug'), )

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

    def save(self, **kw):
        if not self.uuid:
            self.uuid = unicode(uuid.uuid4())
        if not self.slug:
            self.slug = self.uuid[:30]
        self.clean_slug()

        super(Collection, self).save(**kw)

    def clean_slug(self):
        if self.type in SPECIAL_SLUGS:
            self.slug = SPECIAL_SLUGS[self.type]
            return

        if self.slug in SPECIAL_SLUGS.values():
            self.slug += '~'

        if not self.author:
            return

        qs = self.author.collections.using('default')
        slugs = dict((slug, id) for slug, id in qs.values_list('slug', 'id'))
        if self.slug in slugs and slugs[self.slug] != self.id:
            for idx in range(len(slugs)):
                new = '%s-%s' % (self.slug, idx + 1)
                if new not in slugs:
                    self.slug = new
                    return

    def get_url_path(self):
        return reverse('collections.detail',
                       args=[self.author_username, self.slug])

    def get_abs_url(self):
        return absolutify(self.get_url_path())

    def get_img_dir(self):
        return os.path.join(user_media_path('collection_icons'),
                            str(self.id / 1000))

    def upvote_url(self):
        return reverse('collections.vote',
                       args=[self.author_username, self.slug, 'up'])

    def downvote_url(self):
        return reverse('collections.vote',
                       args=[self.author_username, self.slug, 'down'])

    def edit_url(self):
        return reverse('collections.edit',
                       args=[self.author_username, self.slug])

    def watch_url(self):
        return reverse('collections.watch',
                       args=[self.author_username, self.slug])

    def delete_url(self):
        return reverse('collections.delete',
                       args=[self.author_username, self.slug])

    def delete_icon_url(self):
        return reverse('collections.delete_icon',
                       args=[self.author_username, self.slug])

    def share_url(self):
        return reverse('collections.share',
                       args=[self.author_username, self.slug])

    def feed_url(self):
        return reverse('collections.detail.rss',
                       args=[self.author_username, self.slug])

    def stats_url(self):
        return reverse('collections.stats',
                       args=[self.author_username, self.slug])

    @property
    def author_username(self):
        return self.author.username if self.author else 'anonymous'

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

    @property
    def url_slug(self):
        """uuid or nickname if chosen"""
        return self.nickname or self.uuid

    @property
    def icon_url(self):
        modified = int(time.mktime(self.modified.timetuple()))
        if self.icontype:
            # [1] is the whole ID, [2] is the directory
            split_id = re.match(r'((\d*?)\d{1,3})$', str(self.id))
            path = "/".join([
                split_id.group(2) or '0',
                "%s.png?m=%s" % (self.id, modified)
            ])
            return user_media_url('collection_icons') + path
        elif self.type == amo.COLLECTION_FAVORITES:
            return settings.STATIC_URL + 'img/icons/heart.png'
        else:
            return settings.STATIC_URL + 'img/icons/collection.png'

    def set_addons(self, addon_ids, comments=None):
        """Replace the current add-ons with a new list of add-on ids."""
        if comments is None:
            comments = {}
        order = {a: idx for idx, a in enumerate(addon_ids)}

        # Partition addon_ids into add/update/remove buckets.
        existing = set(
            self.addons.using('default').values_list('id', flat=True))
        add, update = [], []
        for addon in addon_ids:
            bucket = update if addon in existing else add
            bucket.append((addon, order[addon]))
        remove = existing.difference(addon_ids)
        now = datetime.now()

        with connection.cursor() as cursor:
            if remove:
                cursor.execute("DELETE FROM addons_collections "
                               "WHERE collection_id=%s AND addon_id IN (%s)" %
                               (self.id, ','.join(map(str, remove))))
                if self.listed:
                    for addon in remove:
                        activity.log_create(amo.LOG.REMOVE_FROM_COLLECTION,
                                            (Addon, addon), self)
            if add:
                insert = '(%s, %s, %s, NOW(), NOW(), 0)'
                values = [insert % (a, self.id, idx) for a, idx in add]
                cursor.execute("""
                    INSERT INTO addons_collections
                        (addon_id, collection_id, ordering, created,
                         modified, downloads)
                    VALUES %s""" % ','.join(values))
                if self.listed:
                    for addon_id, idx in add:
                        activity.log_create(amo.LOG.ADD_TO_COLLECTION,
                                            (Addon, addon_id), self)
        for addon, ordering in update:
            (CollectionAddon.objects.filter(
                collection=self.id, addon=addon).update(ordering=ordering,
                                                        modified=now))

        for addon, comment in comments.iteritems():
            try:
                c = (CollectionAddon.objects.using('default').get(
                    collection=self.id, addon=addon))
            except CollectionAddon.DoesNotExist:
                pass
            else:
                c.comments = comment
                c.save(force_update=True)

        self.save()

    def is_subscribed(self, user):
        """Determines if the user is subscribed to this collection."""
        return self.following.filter(user=user).exists()

    def add_addon(self, addon):
        "Adds an addon to the collection."
        CollectionAddon.objects.get_or_create(addon=addon, collection=self)
        if self.listed:
            activity.log_create(amo.LOG.ADD_TO_COLLECTION, addon, self)
        self.save()  # To invalidate Collection.

    def remove_addon(self, addon):
        CollectionAddon.objects.filter(addon=addon, collection=self).delete()
        if self.listed:
            activity.log_create(amo.LOG.REMOVE_FROM_COLLECTION, addon, self)
        self.save()  # To invalidate Collection.

    def owned_by(self, user):
        return (user.id == self.author_id)

    def can_view_stats(self, request):
        if request and request.user:
            return (self.publishable_by(request.user) or acl.action_allowed(
                request, amo.permissions.COLLECTION_STATS_VIEW))
        return False

    def publishable_by(self, user):
        return bool(self.owned_by(user) or self.users.filter(pk=user.id))

    def is_public(self):
        return self.listed

    def is_featured(self):
        return FeaturedCollection.objects.filter(collection=self).exists()

    @staticmethod
    def transformer(collections):
        if not collections:
            return
        author_ids = set(c.author_id for c in collections)
        authors = dict(
            (u.id, u) for u in UserProfile.objects.filter(id__in=author_ids))
        for c in collections:
            c.author = authors.get(c.author_id)

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        if kwargs.get('raw'):
            return
        tasks.collection_meta.delay(instance.id, using='default')
        tasks.index_collections.delay([instance.id])
        if instance.is_featured():
            Collection.update_featured_status(sender, instance, **kwargs)

    @staticmethod
    def post_delete(sender, instance, **kwargs):
        from . import tasks
        if kwargs.get('raw'):
            return
        tasks.unindex_collections.delay([instance.id])
        if instance.is_featured():
            Collection.update_featured_status(sender, instance, **kwargs)

    @staticmethod
    def update_featured_status(sender, instance, **kwargs):
        from olympia.addons.tasks import index_addons
        addons = [addon.id for addon in instance.addons.all()]
        if addons:
            clear_get_featured_ids_cache(None, None)
            index_addons.delay(addons)

    def check_ownership(self, request, require_owner, require_author,
                        ignore_disabled, admin):
        """
        Used by acl.check_ownership to see if request.user has permissions for
        the collection.
        """
        from olympia.access import acl
        return acl.check_collection_ownership(request, self, require_owner)
Exemplo n.º 18
0
class CollectionFeature(ModelBase):
    title = TranslatedField()
    tagline = TranslatedField()

    class Meta(ModelBase.Meta):
        db_table = 'collection_features'
Exemplo n.º 19
0
class Collection(ModelBase):
    id = PositiveAutoField(primary_key=True)

    uuid = models.UUIDField(blank=True, unique=True, null=True)
    name = TranslatedField(require_locale=False)
    slug = models.CharField(max_length=30, blank=True, null=True)

    # description can (and sometimes does) contain html and other unsanitized
    # content. It must be cleaned before display - NoURLsField just strips the
    # URL without doing any escaping.
    description = NoURLsField(require_locale=False)
    default_locale = models.CharField(
        max_length=10, default='en-US', db_column='defaultlocale'
    )
    listed = models.BooleanField(
        default=True, help_text='Collections are either listed or private.'
    )

    addon_count = models.PositiveIntegerField(default=0, db_column='addonCount')

    addons = models.ManyToManyField(
        Addon, through='CollectionAddon', related_name='collections'
    )
    author = models.ForeignKey(
        UserProfile, null=True, related_name='collections', on_delete=models.CASCADE
    )

    objects = CollectionManager()

    class Meta(ModelBase.Meta):
        db_table = 'collections'
        indexes = [
            models.Index(fields=('created',), name='collections_created_idx'),
            models.Index(fields=('listed',), name='collections_listed_idx'),
            models.Index(fields=('slug',), name='collections_slug_idx'),
        ]
        constraints = [
            models.UniqueConstraint(fields=('author', 'slug'), name='author_id'),
        ]

    def __str__(self):
        return f'{self.name} ({self.addon_count})'

    def save(self, **kw):
        if not self.uuid:
            self.uuid = uuid.uuid4()
        if not self.slug:
            # Work with both, strings (if passed manually on .create()
            # and UUID instances)
            self.slug = str(self.uuid).replace('-', '')[:30]
        self.clean_slug()

        super().save(**kw)

    def clean_slug(self):
        if not self.author:
            return

        qs = self.author.collections.using('default')
        slugs = {slug: id for slug, id in qs.values_list('slug', 'id')}
        if self.slug in slugs and slugs[self.slug] != self.id:
            for idx in range(len(slugs)):
                new = f'{self.slug}-{idx + 1}'
                if new not in slugs:
                    self.slug = new
                    return

    def get_url_path(self):
        return reverse('collections.detail', args=[self.author_id, self.slug])

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

    def add_addon(self, addon):
        CollectionAddon.objects.get_or_create(addon=addon, collection=self)

    def remove_addon(self, addon):
        CollectionAddon.objects.filter(addon=addon, collection=self).delete()

    def owned_by(self, user):
        return user.id == self.author_id

    def is_public(self):
        return self.listed

    @staticmethod
    def transformer(collections):
        if not collections:
            return
        author_ids = {c.author_id for c in collections}
        authors = {u.id: u for u in UserProfile.objects.filter(id__in=author_ids)}
        for c in collections:
            c.author = authors.get(c.author_id)

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks

        if kwargs.get('raw'):
            return
        tasks.collection_meta.delay(instance.id)

    def check_ownership(
        self, request, require_owner, require_author, ignore_disabled, admin
    ):
        """
        Used by acl.check_ownership to see if request.user has permissions for
        the collection.
        """
        from olympia.access import acl

        return acl.check_collection_ownership(request, self, require_owner)
Exemplo n.º 20
0
class Collection(ModelBase):
    id = PositiveAutoField(primary_key=True)
    TYPE_CHOICES = amo.COLLECTION_CHOICES.items()

    uuid = models.UUIDField(blank=True, unique=True, null=True)
    name = TranslatedField(require_locale=False)
    # nickname is deprecated.  Use slug.
    nickname = models.CharField(max_length=30, blank=True, unique=True,
                                null=True)
    slug = models.CharField(max_length=30, blank=True, null=True)

    description = NoLinksNoMarkupField(require_locale=False)
    default_locale = models.CharField(max_length=10, default='en-US',
                                      db_column='defaultlocale')
    type = models.PositiveIntegerField(db_column='collection_type',
                                       choices=TYPE_CHOICES, default=0)

    listed = models.BooleanField(
        default=True, help_text='Collections are either listed or private.')

    application = models.PositiveIntegerField(choices=amo.APPS_CHOICES,
                                              db_column='application_id',
                                              blank=True, null=True)
    addon_count = models.PositiveIntegerField(default=0,
                                              db_column='addonCount')

    addons = models.ManyToManyField(
        Addon, through='CollectionAddon', related_name='collections')
    author = models.ForeignKey(
        UserProfile, null=True, related_name='collections',
        on_delete=models.CASCADE)

    objects = CollectionManager()

    class Meta(ModelBase.Meta):
        db_table = 'collections'
        indexes = [
            models.Index(fields=('application',), name='application_id'),
            models.Index(fields=('created',), name='created_idx'),
            models.Index(fields=('listed',), name='listed'),
            models.Index(fields=('slug',), name='slug_idx'),
            models.Index(fields=('type',), name='type_idx'),
        ]
        constraints = [
            models.UniqueConstraint(fields=('author', 'slug'),
                                    name='author_id'),
        ]

    def __str__(self):
        return u'%s (%s)' % (self.name, self.addon_count)

    def save(self, **kw):
        if not self.uuid:
            self.uuid = uuid.uuid4()
        if not self.slug:
            # Work with both, strings (if passed manually on .create()
            # and UUID instances)
            self.slug = str(self.uuid).replace('-', '')[:30]
        self.clean_slug()

        super(Collection, self).save(**kw)

    def clean_slug(self):
        if self.type in SPECIAL_SLUGS:
            self.slug = SPECIAL_SLUGS[self.type]
            return

        if self.slug in SPECIAL_SLUGS.values():
            self.slug += '~'

        if not self.author:
            return

        qs = self.author.collections.using('default')
        slugs = dict((slug, id) for slug, id in qs.values_list('slug', 'id'))
        if self.slug in slugs and slugs[self.slug] != self.id:
            for idx in range(len(slugs)):
                new = '%s-%s' % (self.slug, idx + 1)
                if new not in slugs:
                    self.slug = new
                    return

    def get_url_path(self):
        return reverse('collections.detail',
                       args=[self.author_id, self.slug])

    def get_abs_url(self):
        return absolutify(self.get_url_path())

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

    def add_addon(self, addon):
        CollectionAddon.objects.get_or_create(addon=addon, collection=self)

    def remove_addon(self, addon):
        CollectionAddon.objects.filter(addon=addon, collection=self).delete()

    def owned_by(self, user):
        return user.id == self.author_id

    def can_view_stats(self, request):
        if request and request.user:
            return (self.owned_by(request.user) or
                    acl.action_allowed(request,
                                       amo.permissions.COLLECTION_STATS_VIEW))
        return False

    def is_public(self):
        return self.listed

    @staticmethod
    def transformer(collections):
        if not collections:
            return
        author_ids = set(c.author_id for c in collections)
        authors = dict((u.id, u) for u in
                       UserProfile.objects.filter(id__in=author_ids))
        for c in collections:
            c.author = authors.get(c.author_id)

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        if kwargs.get('raw'):
            return
        tasks.collection_meta.delay(instance.id)

    def index_addons(self, addons=None):
        """Index add-ons belonging to that collection."""
        from olympia.addons.tasks import index_addons
        addon_ids = [addon.id for addon in (addons or self.addons.all())]
        if addon_ids:
            index_addons.delay(addon_ids)

    def check_ownership(self, request, require_owner, require_author,
                        ignore_disabled, admin):
        """
        Used by acl.check_ownership to see if request.user has permissions for
        the collection.
        """
        from olympia.access import acl
        return acl.check_collection_ownership(request, self, require_owner)