extensions-website/reviewers/models.py
Oleg Komarov 13ac2436ad ApprovalQueue: use a materialized table (#240)
see #238

This change improves the listing performance: old code had to process all
ApprovalActivity to compute an extension's moderation status and position in
the queue.

Now we maintain a sortkey, a reference to the latest "meaningful" activity
object, and a total comment count. These fields are updated in a post_save
signal.

"Meaningful" activity means moderation status changes:
approved, awaiting changes, awaiting review.

"Non-meaningful" activity shouldn't affect queue position anymore and
extensions without "meaningful" activity should not appear in the queue, but
their respective detail pages should still be reachable via a direct link.
This UX may still need improvement, and #210 may be relevant here.

Reviewed-on: #240
Reviewed-by: Anna Sirota <annasirota@noreply.localhost>
2024-08-23 15:11:45 +02:00

92 lines
3.1 KiB
Python

from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
import common.help_texts
from common.model_mixins import CreatedModifiedMixin, RecordDeletionMixin
from constants.base import EXTENSION_TYPE_CHOICES
from constants.reviewers import CANNED_RESPONSE_CATEGORY_CHOICES
User = get_user_model()
class CannedResponse(CreatedModifiedMixin, models.Model):
TYPES = EXTENSION_TYPE_CHOICES
CATEGORIES = CANNED_RESPONSE_CATEGORY_CHOICES
name = models.CharField(max_length=255)
response = models.TextField()
sort_group = models.CharField(max_length=255)
type = models.PositiveIntegerField(choices=TYPES, db_index=True, default=TYPES.BPY)
# Category is used only by code-manager
category = models.PositiveIntegerField(choices=CATEGORIES, default=CATEGORIES.OTHER)
def __str__(self):
return str(self.name)
class ApprovalActivity(CreatedModifiedMixin, RecordDeletionMixin, models.Model):
STATUS_CHANGE_TYPES = {"AWR": 0x02, "AWC": 0x01, "APR": 0x00}
class ActivityType(models.TextChoices):
COMMENT = "COM", _("Comment")
APPROVED = "APR", _("Approved")
AWAITING_CHANGES = "AWC", _("Awaiting Changes")
AWAITING_REVIEW = "AWR", _("Awaiting Review")
UPLOADED_NEW_VERSION = "UNV", _("Uploaded New Version")
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
extension = models.ForeignKey(
'extensions.Extension',
on_delete=models.CASCADE,
related_name='review_activity',
)
type = models.CharField(
max_length=3,
choices=ActivityType.choices,
default=ActivityType.COMMENT,
)
message = models.TextField(help_text=common.help_texts.markdown, blank=False, null=False)
class Meta:
verbose_name_plural = "Review activity"
def __str__(self):
return f"{self.extension.name}: {self.get_type_display()}"
@property
def queue_sortkey(self):
"""Sorting by moderation status and latest status change timestamp.
The queue is ordered by status: first "awaiting review', then "awaiting changes", then
"approved".
Within each group items with most recent status change are sorted to the top.
Integer timestamp representation takes 4 bytes, the resulting bigint is composed of
0x000000SSTTTTTTTT, where SS byte represents the status change type, and TT bytes represent
timestamp bytes.
"""
timestamp = int(self.date_created.timestamp())
return (self.STATUS_CHANGE_TYPES[self.type] << 32) | timestamp
class ApprovalQueue(models.Model):
activity_count = models.PositiveIntegerField()
extension = models.OneToOneField(
'extensions.Extension',
on_delete=models.CASCADE,
)
latest_activity = models.ForeignKey(
ApprovalActivity,
# we don't delete activity yet, if we start this needs to be updated via a pre_delete signal
on_delete=models.PROTECT,
)
sortkey = models.BigIntegerField()
class Meta:
indexes = [
models.Index(fields=['sortkey']),
]