extensions-website/reviewers/views.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

144 lines
5.2 KiB
Python

import logging
from actstream.actions import follow, is_following, unfollow
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.list import ListView
from django.views.generic import DetailView, FormView
from django.shortcuts import redirect, reverse
import django.forms
from constants.activity import Flag
from extensions.models import Extension
from reviewers.forms import CommentForm
from reviewers.models import ApprovalActivity, ApprovalQueue
log = logging.getLogger(__name__)
class ApprovalQueueView(ListView):
model = Extension
paginate_by = 50
def get_queryset(self):
return (
ApprovalQueue.objects.select_related(
'extension',
'latest_activity',
)
.prefetch_related(
'extension__authors',
'extension__icon',
'extension__versions',
'extension__versions__files',
'extension__versions__files__validation',
)
.order_by('-sortkey')
)
template_name = 'reviewers/extensions_review_list.html'
class ExtensionsApprovalDetailView(DetailView):
model = Extension
template_name = 'reviewers/extensions_review_detail.html'
def get_queryset(self):
return self.model.objects.prefetch_related(
'authors',
'latest_version__files',
'latest_version__files__validation',
'latest_version__permissions',
'latest_version__platforms',
'latest_version__tags',
'preview_set',
'preview_set__file',
).all()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
review_activity = (
self.object.review_activity.select_related('user')
.prefetch_related('user__groups')
.order_by('date_created')
.all()
)
maintainers = self.object.all_maintainers()
for activity in review_activity:
if activity.user in maintainers:
activity.user_is_maintainer = True
ctx['review_activity'] = review_activity
ctx['status_change_types'] = ApprovalActivity.STATUS_CHANGE_TYPES
if self.request.user.is_authenticated:
initial = {}
if 'report_compatibility_issue' in self.request.GET:
version = self.request.GET.get('version')
initial['message'] = (
f'Compatibility range for version {version} is outdated\n'
'Platform: ...\n'
'Version of Blender where the add-on is **no longer** working: ?????'
)
form = ctx['comment_form'] = CommentForm(initial=initial)
# anyone can comment
filtered_activity_types = {ApprovalActivity.ActivityType.COMMENT}
user = self.request.user
if self.object.has_maintainer(user):
ctx['is_maintainer'] = self.object.has_maintainer(self.request.user)
filtered_activity_types.add(ApprovalActivity.ActivityType.AWAITING_REVIEW)
if user.is_moderator or user.is_superuser:
filtered_activity_types.update(
[
ApprovalActivity.ActivityType.APPROVED,
ApprovalActivity.ActivityType.AWAITING_CHANGES,
]
)
choices = list(
filter(
lambda c: c[0] in filtered_activity_types, ApprovalActivity.ActivityType.choices
)
)
form.fields['type'].choices = choices
form.fields['type'].widget.choices = choices
if len(choices) == 1:
form.fields['type'].widget = django.forms.HiddenInput()
ctx['user_is_following'] = is_following(user, self.object, flag=Flag.REVIEWER)
return ctx
class ExtensionsApprovalFormView(LoginRequiredMixin, FormView):
form_class = CommentForm
http_method_names = ['post']
def get_success_url(self):
return reverse('reviewers:approval-detail', kwargs={'slug': self.kwargs['slug']})
def approve_if_allowed(self, form):
if form.cleaned_data['type'] != ApprovalActivity.ActivityType.APPROVED:
return
if not (self.request.user.is_moderator or self.request.user.is_superuser):
log.error('Non-moderator tried to approve extension %s' % form.instance.extension)
return
form.instance.extension.approve()
def form_valid(self, form):
form.instance.user = self.request.user
form.instance.extension = Extension.objects.get(slug=self.kwargs['slug'])
form.save()
self.approve_if_allowed(form)
return super().form_valid(form)
class ExtensionsApprovalFollowView(LoginRequiredMixin, DetailView):
model = Extension
def post(self, request, *args, **kwargs):
extension = self.get_object()
user = request.user
if request.POST.get('follow'):
follow(user, extension, send_action=False, flag=Flag.REVIEWER)
else:
unfollow(user, extension, send_action=False, flag=Flag.REVIEWER)
return redirect(extension.get_review_url() + '#activity')