extensions-website/ratings/models.py

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