Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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
Beispiel #5
0
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*']
Beispiel #6
0
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]
Beispiel #7
0
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)
Beispiel #8
0
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)
Beispiel #9
0
class CollectionFeature(amo.models.ModelBase):
    title = TranslatedField()
    tagline = TranslatedField()

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'collection_features'
Beispiel #10
0
class TranslatedModel(amo.models.ModelBase):
    name = TranslatedField()
    description = TranslatedField()
    default_locale = models.CharField(max_length=10)
    no_locale = TranslatedField(require_locale=False)
Beispiel #11
0
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
Beispiel #12
0
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()]
Beispiel #13
0
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
Beispiel #14
0
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')