193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
import logging
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import models
|
|
from django.template.defaultfilters import truncatechars
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.urls import reverse
|
|
|
|
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin
|
|
from constants.base import RATING_STATUS_CHOICES, RATING_SCORE_CHOICES
|
|
|
|
User = get_user_model()
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class RatingManager(models.Manager):
|
|
# TODO: figure out how to retrieve reviews "annotated" with replies, if any
|
|
@property
|
|
def listed(self):
|
|
return self.filter(status=self.model.STATUSES.APPROVED, reply_to__isnull=True)
|
|
|
|
@property
|
|
def unlisted(self):
|
|
return self.exclude(status=self.models.STATUSES.APPROVED, reply_to__isnull=True)
|
|
|
|
@property
|
|
def listed_texts(self):
|
|
return self.listed.filter(text__isnull=False)
|
|
|
|
|
|
class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|
track_changes_to_fields = {'status'}
|
|
|
|
STATUSES = RATING_STATUS_CHOICES
|
|
SCORES = RATING_SCORE_CHOICES
|
|
|
|
extension = models.ForeignKey(
|
|
'extensions.Extension', related_name='ratings', on_delete=models.CASCADE
|
|
)
|
|
version = models.ForeignKey(
|
|
'extensions.Version', related_name='ratings', on_delete=models.CASCADE
|
|
)
|
|
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
|
|
reply_to = models.OneToOneField(
|
|
'self',
|
|
null=True,
|
|
related_name='reply',
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
score = models.PositiveSmallIntegerField(null=True, choices=SCORES)
|
|
text = models.TextField(null=True)
|
|
ip_address = models.GenericIPAddressField(protocol='both', null=True)
|
|
|
|
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.APPROVED)
|
|
|
|
# Denormalized fields for easy lookup queries.
|
|
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?',
|
|
)
|
|
|
|
objects = RatingManager()
|
|
|
|
class Meta:
|
|
ordering = ('-date_created',)
|
|
indexes = [
|
|
models.Index(fields=('version',), name='rating_version_id'),
|
|
models.Index(fields=('user',), name='rating_user_idx'),
|
|
models.Index(
|
|
fields=('reply_to', 'is_latest', 'date_created'),
|
|
name='rating_latest_idx',
|
|
),
|
|
models.Index(fields=('ip_address',), name='rating_ip_address_idx'),
|
|
]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=('version', 'user', 'reply_to'), name='rating_one_review_per_user_key'
|
|
),
|
|
]
|
|
|
|
def __str__(self):
|
|
return truncatechars(str(self.text), 10)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user_responsible = kwargs.pop('user_responsible', None)
|
|
super().__init__(*args, **kwargs)
|
|
if user_responsible is not None:
|
|
self.user_responsible = user_responsible
|
|
|
|
@classmethod
|
|
def get_for(cls, user_id: int, extension_id: int):
|
|
"""Get rating left by a given user for a given extension."""
|
|
return cls.objects.filter(
|
|
reply_to=None,
|
|
user_id=user_id,
|
|
extension_id=extension_id,
|
|
).first()
|
|
|
|
@property
|
|
def is_listed(self) -> bool:
|
|
return self.status == self.STATUSES.APPROVED
|
|
|
|
@property
|
|
def user_responsible(self):
|
|
"""Return user responsible for the current changes being made on this
|
|
model. Only set by the views when they are about to save a Review
|
|
instance, to track if the original author or an admin was responsible
|
|
for the change.
|
|
|
|
Having this as a @property with a setter makes update_or_create() work,
|
|
otherwise it rejects the property, causing an error.
|
|
"""
|
|
return self._user_responsible
|
|
|
|
@user_responsible.setter
|
|
def user_responsible(self, value):
|
|
self._user_responsible = value
|
|
|
|
def approve(self, user):
|
|
for flag in self.ratingflag_set.all():
|
|
flag.delete()
|
|
self.status = self.STATUSES.APPROVED
|
|
# We've already logged what we want to log, no need to pass
|
|
# user_responsible=user.
|
|
self.save()
|
|
|
|
@classmethod
|
|
def get_replies(cls, ratings):
|
|
ratings = [r.id for r in ratings]
|
|
qs = Rating.objects.filter(reply_to__in=ratings)
|
|
return {r.reply_to_id: r for r in qs}
|
|
|
|
def post_save(sender, instance, created, **kwargs):
|
|
if kwargs.get('raw'):
|
|
return
|
|
|
|
if getattr(instance, 'user_responsible', None):
|
|
# 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.info(f'{action} reply to {instance.reply_to_id}: {instance.pk}')
|
|
else:
|
|
log.info(f'{action} rating: {instance.pk}')
|
|
|
|
def get_report_url(self):
|
|
return reverse(
|
|
'abuse:report-ratings',
|
|
args=[
|
|
self.extension.type_slug,
|
|
self.extension.slug,
|
|
self.version.version,
|
|
self.id,
|
|
],
|
|
)
|
|
|
|
|
|
class RatingFlag(CreatedModifiedMixin, models.Model):
|
|
SPAM = 'review_flag_reason_spam'
|
|
LANGUAGE = 'review_flag_reason_language'
|
|
SUPPORT = 'review_flag_reason_bug_support'
|
|
OTHER = 'review_flag_reason_other'
|
|
FLAGS = (
|
|
(SPAM, _('Spam or otherwise non-review content')),
|
|
(LANGUAGE, _('Inappropriate language/dialog')),
|
|
(SUPPORT, _('Misplaced bug report or support request')),
|
|
(OTHER, _('Other (please specify)')),
|
|
)
|
|
|
|
rating = models.ForeignKey(Rating, on_delete=models.CASCADE)
|
|
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
|
flag = models.CharField(max_length=64, default=OTHER, choices=FLAGS)
|
|
note = models.CharField(max_length=100, blank=True, default='')
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=('user',), name='ratingflag_user_idx'),
|
|
models.Index(fields=('rating',), name='ratingflag_rating_idx'),
|
|
models.Index(fields=('date_modified',), name='ratingflag_date_modified_idx'),
|
|
]
|
|
constraints = [
|
|
models.UniqueConstraint(fields=('rating', 'user'), name='ratingflag_review_user_key')
|
|
]
|