addons-server/apps/reviews/models.py

186 строки
6.4 KiB
Python

from datetime import datetime, timedelta
from django.conf import settings
from django.core.cache import cache
from django.db import models
import bleach
from celeryutils import task
from tower import ugettext_lazy as _
import amo.models
from amo.urlresolvers import reverse
from translations.fields import TranslatedField
from users.models import UserProfile
from versions.models import Version
class ReviewManager(amo.models.ManagerBase):
def get_query_set(self):
qs = super(ReviewManager, self).get_query_set()
return qs.select_related('user', 'version')
def valid(self):
"""Get all reviews with rating > 0 that aren't replies."""
return self.filter(reply_to=None, rating__gt=0)
def latest(self):
"""Get all the latest valid reviews."""
return self.valid().filter(is_latest=True)
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')
models.signals.post_save.connect(Review.post_save, sender=Review)
models.signals.post_delete.connect(Review.post_delete, sender=Review)
# TODO: translate old flags.
class ReviewFlag(amo.models.ModelBase):
SPAM = 'review_flag_reason_spam'
LANGUAGE = 'review_flag_reason_language'
SUPPORT = 'review_flag_reason_bug_support'
OTHER = 'review_flag_reason_other'
FLAGS = ((SPAM, _(u'Spam or otherwise non-review content')),
(LANGUAGE, _(u'Inappropriate language/dialog')),
(SUPPORT, _(u'Misplaced bug report or support request')),
(OTHER, _(u'Other (please specify)')),
)
review = models.ForeignKey(Review)
user = models.ForeignKey('users.UserProfile')
flag = models.CharField(max_length=64, default=OTHER,
choices=FLAGS, db_column='flag_name')
note = models.CharField(max_length=100, db_column='flag_notes', blank=True,
default='')
class Meta:
db_table = 'reviews_moderation_flags'
unique_together = (('review', 'user'),)
def flush_urls(self):
return self.review.flush_urls()
class GroupedRating(object):
"""
Group an add-on's ratings so we can have a graph of rating counts.
SELECT rating, COUNT(rating) FROM reviews where addon=:id
"""
# Non-critical data, so we always leave it in memcached. Updated through
# cron daily, so we cache for two days.
@classmethod
def key(cls, addon):
return '%s:%s:%s' % (settings.CACHE_PREFIX, cls.__name__, addon)
@classmethod
def get(cls, addon):
return cache.get(cls.key(addon))
@classmethod
def set(cls, addon, using=None):
q = (Review.objects.latest().filter(addon=addon).using(using)
.values_list('rating').annotate(models.Count('rating')))
counts = dict(q)
ratings = [(rating, counts.get(rating, 0)) for rating in range(1, 6)]
two_days = 60 * 60 * 24 * 2
cache.set(cls.key(addon), ratings, two_days)
class Spam(object):
def __init__(self):
from caching.invalidation import get_redis_backend
self.redis = get_redis_backend()
def add(self, review, reason):
reason = 'amo:review:spam:%s' % reason
self.redis.sadd(reason, review.id)
self.redis.sadd('amo:review:spam:reasons', reason)
def reasons(self):
return self.redis.smembers('amo:review:spam:reasons')
@task
def check_spam(review_id, **kw):
spam = Spam()
review = Review.objects.using('default').get(id=review_id)
thirty_days = datetime.now() - timedelta(days=30)
others = (Review.objects.no_cache().exclude(id=review.id)
.filter(user=review.user, created__gte=thirty_days))
if len(others) > 10:
spam.add(review, 'numbers')
if bleach.url_re.search(review.body.localized_string):
spam.add(review, 'urls')
for other in others:
if ((review.title and review.title == other.title)
or review.body == other.body):
spam.add(review, 'matches')
break