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)
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)
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)