class CannedResponse(amo.models.ModelBase): name = TranslatedField() response = TranslatedField() sort_group = models.CharField(max_length=255) class Meta: db_table = 'cannedresponses' def __unicode__(self): return unicode(self.name)
class CannedResponse(amo.models.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)
class Price(amo.models.ModelBase): active = models.BooleanField(default=True) name = TranslatedField() price = models.DecimalField(max_digits=5, decimal_places=2) objects = PriceManager() currency = 'USD' class Meta: db_table = 'prices' def __unicode__(self): return u'%s - $%s' % (self.name, self.price) def _price(self): """Return the price and currency for the current locale.""" lang = translation.get_language() locale = Locale(translation.to_locale(lang)) currency = amo.LOCALE_CURRENCY.get(locale.language) if currency: price_currency = self.pricecurrency_set.filter(currency=currency) if price_currency: return price_currency[0].price, currency, locale return self.price, self.currency, locale def get_price(self): """Return the price as a decimal for the current locale.""" return self._price()[0] def get_price_locale(self): """Return the price as a nicely localised string for the locale.""" price, currency, locale = self._price() return numbers.format_currency(price, currency, locale=locale)
class Price(amo.models.ModelBase): active = models.BooleanField(default=True) name = TranslatedField() price = models.DecimalField(max_digits=10, decimal_places=2) objects = PriceManager() currency = 'USD' class Meta: db_table = 'prices' def __unicode__(self): return u'%s - $%s' % (self.name, self.price) @staticmethod def transformer(prices): # There are a constrained number of price currencies, let's just # get them all. Price._currencies = dict([(p.currency, p.tier_id), p] for p in PriceCurrency.objects.all()) def _price(self): """ Return the price and currency for the current locale. This will take the locale and find the tier, should one exist. """ if not hasattr(self, '_currencies'): Price.transformer([]) lang = translation.get_language() locale = get_locale_from_lang(lang) currency = amo.LOCALE_CURRENCY.get(locale.language) if currency: price_currency = Price._currencies.get((currency, self.id), None) if price_currency: return price_currency.price, currency, locale return self.price, self.currency, locale def get_price(self): """Return the price as a decimal for the current locale.""" return self._price()[0] def get_price_locale(self): """Return the price as a nicely localised string for the locale.""" price, currency, locale = self._price() return numbers.format_currency(price, currency, locale=locale) def currencies(self): """A listing of all the currency objects for this tier.""" if not hasattr(self, '_currencies'): Price.transformer([]) currencies = [('USD', self)] currencies.extend([(c.currency, c) for c in self._currencies.values() if c.tier_id == self.pk]) return currencies
class HubPromo(amo.models.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*']
class CommunicationNote(amo.models.ModelBase): thread = models.ForeignKey(CommunicationThread, related_name='notes') author = models.ForeignKey('users.UserProfile', related_name='comm_notes') note_type = models.IntegerField() body = TranslatedField() class Meta: db_table = 'comm_thread_notes' def get_type(self): return const.NOTE_TYPES[self.note_type]
class License(amo.models.ModelBase): OTHER = 0 name = TranslatedField(db_column='name') 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.') objects = LicenseManager() class Meta: db_table = 'licenses' def __unicode__(self): return unicode(self.name)
class Collection(CollectionBase, amo.models.ModelBase): TYPE_CHOICES = amo.COLLECTION_CHOICES.items() 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) 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') addon_index = models.CharField( max_length=40, null=True, db_index=True, help_text='Custom index for the add-ons in this collection') # This gets overwritten in the transformer. share_counts = collections.defaultdict(int) objects = CollectionManager() top_tags = TopTags() class Meta(amo.models.ModelBase.Meta): db_table = 'collections' unique_together = (('author', 'slug'), ) def __unicode__(self): return u'%s (%s)' % (self.name, self.addon_count) def flush_urls(self): urls = ['*%s' % self.get_url_path(), self.icon_url] return urls 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() # Maintain our index of add-on ids. if self.id: ids = self.addons.values_list('id', flat=True) self.addon_index = self.make_index(ids) 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={}): """Replace the current add-ons with a new list of add-on ids.""" order = dict((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) cursor = connection.cursor() now = datetime.now() 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: amo.log(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: amo.log(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: amo.log(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: amo.log(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.amo_user: return (self.publishable_by(request.amo_user) or acl.action_allowed(request, 'CollectionStats', 'View')) return False @caching.cached_method def publishable_by(self, user): return bool(self.owned_by(user) or self.users.filter(pk=user.id)) @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) c_dict = dict((c.pk, c) for c in collections) sharing.attach_share_counts(CollectionShareCountTotal, 'collection', c_dict) @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]) @staticmethod def post_delete(sender, instance, **kwargs): from . import tasks if kwargs.get('raw'): return tasks.unindex_collections.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 access import acl return acl.check_collection_ownership(request, self, require_owner)
class CollectionFeature(amo.models.ModelBase): title = TranslatedField() tagline = TranslatedField() class Meta(amo.models.ModelBase.Meta): db_table = 'collection_features'
class TranslatedModel(amo.models.ModelBase): name = TranslatedField() description = TranslatedField() default_locale = models.CharField(max_length=10) no_locale = TranslatedField(require_locale=False)
class Rating(amo.models.ModelBase): addon = models.ForeignKey('addons.Addon', related_name='_ratings') user = models.ForeignKey('users.UserProfile', related_name='_ratings_all') reply_to = models.ForeignKey('self', null=True, unique=True, related_name='replies', db_column='reply_to') score = models.IntegerField(null=True) body = TranslatedField(require_locale=False) ip_address = models.IPAddressField(default='0.0.0.0') editorreview = models.BooleanField(default=False) flag = 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 app?") previous_count = models.PositiveIntegerField(default=0, editable=False, help_text="How many previous reviews by the user for this app?") objects = RatingManager() class Meta: db_table = 'ratings' ordering = ('-created',) def get_url_path(self): return self.addon.get_ratings_url('detail', args=[self.id]) def flush_urls(self): urls = ['*/app/%d/' % self.addon.app_slug, '*/app/%d/reviews/' % self.addon.app_slug, '*/app/%d/reviews/format:rss' % self.addon.app_slug, '*/app/%d/reviews/%d/' % (self.addon.app_slug, self.id), '*/user/%d/' % self.user_id] return urls @classmethod def get_replies(cls, reviews): reviews = [r.id for r in reviews] qs = Rating.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 if created: Rating.post_delete(sender, instance) # Avoid slave lag with the delay. #check_spam.apply_async(args=[instance.id], countdown=600) @staticmethod def post_delete(sender, instance, **kwargs): if kwargs.get('raw'): return #from . import tasks #pair = instance.addon_id, instance.user_id # Do this immediately so is_latest is correct. Use default to avoid # slave lag. #tasks.update_denorm(pair, using='default') #tasks.addon_review_aggregates.delay(instance.addon_id, # using='default') @staticmethod def transformer(reviews): user_ids = dict((r.user_id, r) for r in reviews) for user in UserProfile.uncached.filter(id__in=user_ids): user_ids[user.id].user = user
class Price(amo.models.ModelBase): active = models.BooleanField(default=True) name = TranslatedField() price = models.DecimalField(max_digits=10, decimal_places=2) objects = PriceManager() currency = 'USD' class Meta: db_table = 'prices' def __unicode__(self): return u'%s - $%s' % (self.name, self.price) @staticmethod def transformer(prices): # There are a constrained number of price currencies, let's just # get them all. Price._currencies = dict([(p.currency, p.tier_id), p] for p in PriceCurrency.objects.all()) def get_price_data(self, currency=None): """Returns a tuple of Decimal(price), currency, locale. The price is the actual price in the current locale. That is, if the instance is tier 1 ($0.99) and the current locale maps to Euros then you get 5,01 EUR or whatever the exchange is. If currency is None, the default currency from the current locale will be returned. If you do pass in an explicit currency, you will still get the currently active locale which may or may not match. """ if not hasattr(self, '_currencies'): Price.transformer([]) lang = translation.get_language() locale = get_locale_from_lang(lang) if not currency: currency = amo.LOCALE_CURRENCY.get(locale.language) if currency: price_currency = Price._currencies.get((currency, self.id), None) if price_currency: return price_currency.price, currency, locale return self.price, currency or self.currency, locale def get_price(self, currency=None): """Return the price as a decimal for the current locale.""" return self.get_price_data(currency=currency)[0] def get_price_locale(self, currency=None): """Return the price as a nicely localised string for the locale.""" price, currency, locale = self.get_price_data(currency=currency) return numbers.format_currency(price, currency, locale=locale) def currencies(self): """A listing of all the currency objects for this tier.""" if not hasattr(self, '_currencies'): Price.transformer([]) currencies = [('USD', self)] currencies.extend([(c.currency, c) for c in self._currencies.values() if c.tier_id == self.pk]) return currencies def prices(self, provider=None): """A list of dicts of all the currencies and prices for this tier.""" if provider: currencies = PROVIDER_CURRENCIES.get(provider, []) return [({ 'currency': o.currency, 'amount': o.price }) for c, o in self.currencies() if o.currency in currencies] else: return [({ 'currency': o.currency, 'amount': o.price }) for c, o in self.currencies()]
class Review(amo.models.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) # 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?") objects = ReviewManager() class Meta: db_table = 'reviews' ordering = ('-created', ) def get_url_path(self): if 'mkt.ratings' in settings.INSTALLED_APPS: return reverse('ratings.detail', args=[self.addon.app_slug, self.id]) return shared_url('reviews.detail', self.addon, 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 @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) @staticmethod def post_delete(sender, instance, **kwargs): if kwargs.get('raw'): return instance.refresh(update_denorm=True) def refresh(self, update_denorm=False): from 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.uncached.filter(id__in=user_ids): user_ids[user.id].user = user
class Review(amo.models.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) # 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?") objects = ReviewManager() class Meta: db_table = 'reviews' ordering = ('-created', ) def get_url_path(self): return reverse('reviews.detail', args=[self.addon_id, 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 @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 created: Review.post_delete(sender, instance) # Avoid slave lag with the delay. check_spam.apply_async(args=[instance.id], countdown=600) @staticmethod def post_delete(sender, instance, **kwargs): from . import tasks pair = instance.addon_id, instance.user_id # Do this immediately so is_latest is correct. Use default to avoid # slave lag. tasks.update_denorm(pair, using='default') tasks.addon_review_aggregates.delay(instance.addon_id, using='default')