522 строки
18 KiB
Python
522 строки
18 KiB
Python
from datetime import date, datetime, timedelta
|
|
import functools
|
|
import json
|
|
import time
|
|
|
|
from django import http
|
|
from django.conf import settings
|
|
from django.core.cache import cache
|
|
from django.db.models import Q
|
|
from django.shortcuts import redirect, get_object_or_404
|
|
from django.utils.datastructures import SortedDict
|
|
from django.views.decorators.cache import never_cache
|
|
|
|
import jingo
|
|
from tower import ugettext as _
|
|
|
|
import amo
|
|
from access import acl
|
|
from addons.decorators import addon_view
|
|
from addons.models import Version
|
|
from amo.decorators import login_required, json_view, post_required
|
|
from amo.utils import paginate
|
|
from amo.urlresolvers import reverse
|
|
from devhub.models import ActivityLog
|
|
from editors import forms
|
|
from editors.models import (EditorSubscription, ViewPendingQueue,
|
|
ViewFullReviewQueue, ViewPreliminaryQueue,
|
|
EventLog, CannedResponse, PerformanceGraph)
|
|
from editors.helpers import (ViewPendingQueueTable, ViewFullReviewQueueTable,
|
|
ViewPreliminaryQueueTable)
|
|
from files.models import Approval, File
|
|
from reviews.forms import ReviewFlagFormSet
|
|
from reviews.models import Review, ReviewFlag
|
|
from users.models import UserProfile
|
|
from zadmin.models import get_config, set_config
|
|
|
|
|
|
def editor_required(func):
|
|
"""Requires the user to be logged in as an editor or admin."""
|
|
@functools.wraps(func)
|
|
@login_required
|
|
def wrapper(request, *args, **kw):
|
|
if acl.action_allowed(request, 'Editors', '%'):
|
|
return func(request, *args, **kw)
|
|
else:
|
|
return http.HttpResponseForbidden()
|
|
return wrapper
|
|
|
|
|
|
def context(**kw):
|
|
ctx = dict(motd=get_config('editors_review_motd'),
|
|
queue_counts=_queue_counts())
|
|
ctx.update(kw)
|
|
return ctx
|
|
|
|
|
|
@editor_required
|
|
def eventlog(request):
|
|
form = forms.EventLogForm(request.GET)
|
|
eventlog = ActivityLog.objects.editor_events()
|
|
|
|
if form.is_valid():
|
|
if form.cleaned_data['start']:
|
|
eventlog = eventlog.filter(created__gte=form.cleaned_data['start'])
|
|
if form.cleaned_data['end']:
|
|
eventlog = eventlog.filter(created__lt=form.cleaned_data['end'])
|
|
if form.cleaned_data['filter']:
|
|
eventlog = eventlog.filter(action=form.cleaned_data['filter'].id)
|
|
|
|
pager = amo.utils.paginate(request, eventlog, 50)
|
|
|
|
data = context(form=form, pager=pager)
|
|
return jingo.render(request, 'editors/eventlog.html', data)
|
|
|
|
|
|
@editor_required
|
|
def eventlog_detail(request, id):
|
|
log = get_object_or_404(ActivityLog.objects.editor_events(), pk=id)
|
|
data = context(log=log)
|
|
return jingo.render(request, 'editors/eventlog_detail.html', data)
|
|
|
|
|
|
@editor_required
|
|
def home(request):
|
|
durations = (('new', _('New Add-ons (Under 5 days)')),
|
|
('med', _('Passable (5 to 10 days)')),
|
|
('old', _('Overdue (Over 10 days)')))
|
|
|
|
progress, percentage = _editor_progress()
|
|
|
|
data = context(reviews_total=ActivityLog.objects.total_reviews()[:5],
|
|
reviews_monthly=ActivityLog.objects.monthly_reviews()[:5],
|
|
new_editors=EventLog.new_editors(),
|
|
eventlog=ActivityLog.objects.editor_events()[:6],
|
|
progress=progress, percentage=percentage,
|
|
durations=durations)
|
|
|
|
return jingo.render(request, 'editors/home.html', data)
|
|
|
|
|
|
def _editor_progress():
|
|
"""Return the progress (number of add-ons still unreviewed for a given
|
|
period of time) and the percentage (out of all add-ons of that type)."""
|
|
|
|
types = ['nominated', 'prelim', 'pending']
|
|
progress = {'new': _queue_counts(types, days_max=4),
|
|
'med': _queue_counts(types, days_min=5, days_max=10),
|
|
'old': _queue_counts(types, days_min=11),
|
|
'week': _queue_counts(types, days_max=7)}
|
|
|
|
# Return the percent of (p)rogress out of (t)otal.
|
|
pct = lambda p, t: (p / float(t)) * 100 if p > 0 else 0
|
|
|
|
percentage = {}
|
|
for t in types:
|
|
total = progress['new'][t] + progress['med'][t] + progress['old'][t]
|
|
percentage[t] = {}
|
|
for duration in ('new', 'med', 'old'):
|
|
percentage[t][duration] = pct(progress[duration][t], total)
|
|
|
|
return (progress, percentage)
|
|
|
|
|
|
@editor_required
|
|
def performance(request, user_id=False):
|
|
user = request.amo_user
|
|
editors = _recent_editors()
|
|
|
|
is_admin = acl.action_allowed(request, 'Admin', '%')
|
|
|
|
if is_admin and user_id:
|
|
user_new = UserProfile.objects.filter(pk=user_id)
|
|
if user_new.exists():
|
|
user = user_new.all()[0]
|
|
|
|
monthly_data = _performance_by_month(user.id)
|
|
performance_total = _performance_total(monthly_data)
|
|
|
|
data = context(monthly_data=json.dumps(monthly_data),
|
|
performance_month=performance_total['month'],
|
|
performance_year=performance_total['year'],
|
|
editors=editors, current_user=user, is_admin=is_admin,
|
|
is_user=(request.amo_user.id == user.id))
|
|
|
|
return jingo.render(request, 'editors/performance.html', data)
|
|
|
|
|
|
def _recent_editors(days=90):
|
|
since_date = datetime.now() - timedelta(days=days)
|
|
editors = (UserProfile.objects
|
|
.filter(activitylog__action__in=amo.LOG_REVIEW_QUEUE,
|
|
activitylog__created__gt=since_date)
|
|
.order_by('display_name')
|
|
.distinct())
|
|
return editors
|
|
|
|
|
|
def _performance_total(data):
|
|
# TODO(gkoberger): Fix this so it's the past X, rather than this X to date.
|
|
# (ex: March 15-April 15, not April 1 - April 15)
|
|
total_yr = dict(usercount=0, teamamt=0, teamcount=0, teamavg=0)
|
|
total_month = dict(usercount=0, teamamt=0, teamcount=0, teamavg=0)
|
|
current_year = datetime.now().year
|
|
|
|
for k, val in data.items():
|
|
if k.startswith(str(current_year)):
|
|
total_yr['usercount'] = total_yr['usercount'] + val['usercount']
|
|
total_yr['teamamt'] = total_yr['teamamt'] + val['teamamt']
|
|
total_yr['teamcount'] = total_yr['teamcount'] + val['teamcount']
|
|
|
|
current_label_month = datetime.now().isoformat()[:7]
|
|
if current_label_month in data:
|
|
total_month = data[current_label_month]
|
|
|
|
return dict(month=total_month, year=total_yr)
|
|
|
|
|
|
def _performance_by_month(user_id, months=12, end_month=None, end_year=None):
|
|
monthly_data = SortedDict()
|
|
|
|
now = datetime.now()
|
|
if not end_month:
|
|
end_month = now.month
|
|
if not end_year:
|
|
end_year = now.year
|
|
|
|
end_time = time.mktime((end_year, end_month + 1, 1, 0, 0, 0, 0, 0, -1))
|
|
start_time = time.mktime((end_year, end_month + 1 - months,
|
|
1, 0, 0, 0, 0, 0, -1))
|
|
|
|
sql = (PerformanceGraph.objects
|
|
.filter_raw('log_activity.created >=',
|
|
date.fromtimestamp(start_time).isoformat())
|
|
.filter_raw('log_activity.created <',
|
|
date.fromtimestamp(end_time).isoformat())
|
|
)
|
|
|
|
for row in sql.all():
|
|
label = row.approval_created.isoformat()[:7]
|
|
|
|
if not label in monthly_data:
|
|
xaxis = row.approval_created.strftime('%b %Y')
|
|
monthly_data[label] = dict(teamcount=0, usercount=0,
|
|
teamamt=0, label=xaxis)
|
|
|
|
monthly_data[label]['teamamt'] = monthly_data[label]['teamamt'] + 1
|
|
monthly_data_count = monthly_data[label]['teamcount']
|
|
monthly_data[label]['teamcount'] = monthly_data_count + row.total
|
|
|
|
if row.user_id == user_id:
|
|
user_count = monthly_data[label]['usercount']
|
|
monthly_data[label]['usercount'] = user_count + row.total
|
|
|
|
# Calculate averages
|
|
for i, vals in monthly_data.items():
|
|
average = round(vals['teamcount'] / float(vals['teamamt']), 1)
|
|
monthly_data[i]['teamavg'] = str(average) # floats aren't valid json
|
|
|
|
return monthly_data;
|
|
|
|
|
|
@editor_required
|
|
def motd(request):
|
|
form = None
|
|
if acl.action_allowed(request, 'Admin', 'EditorsMOTD'):
|
|
form = forms.MOTDForm()
|
|
data = context(form=form)
|
|
return jingo.render(request, 'editors/motd.html', data)
|
|
|
|
|
|
@editor_required
|
|
@post_required
|
|
def save_motd(request):
|
|
if not acl.action_allowed(request, 'Admin', 'EditorsMOTD'):
|
|
return http.HttpResponseForbidden()
|
|
form = forms.MOTDForm(request.POST)
|
|
if form.is_valid():
|
|
set_config('editors_review_motd', form.cleaned_data['motd'])
|
|
return redirect(reverse('editors.motd'))
|
|
data = context(form=form)
|
|
return jingo.render(request, 'editors/motd.html', data)
|
|
|
|
|
|
def _queue(request, TableObj, tab):
|
|
qs = TableObj.Meta.model.objects.all()
|
|
if request.GET:
|
|
search_form = forms.QueueSearchForm(request.GET)
|
|
if search_form.is_valid():
|
|
qs = search_form.filter_qs(qs)
|
|
else:
|
|
search_form = forms.QueueSearchForm()
|
|
review_num = request.GET.get('num', None)
|
|
if review_num:
|
|
try:
|
|
review_num = int(review_num)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
try:
|
|
# Force a limit query for efficiency:
|
|
start = review_num - 1
|
|
row = qs[start : start + 1][0]
|
|
return redirect('%s?num=%s' % (
|
|
reverse('editors.review',
|
|
args=[row.addon_slug]),
|
|
review_num))
|
|
except IndexError:
|
|
pass
|
|
order_by = request.GET.get('sort', '-waiting_time_min')
|
|
legacy_sorts = {
|
|
'name': 'addon_name',
|
|
'age': 'waiting_time_min',
|
|
'type': 'addon_type_id',
|
|
}
|
|
order_by = legacy_sorts.get(order_by, order_by)
|
|
table = TableObj(qs, order_by=order_by)
|
|
default = 100
|
|
per_page = request.GET.get('per_page', default)
|
|
try:
|
|
per_page = int(per_page)
|
|
except ValueError:
|
|
per_page = default
|
|
if per_page <= 0 or per_page > 200:
|
|
per_page = default
|
|
page = paginate(request, table.rows, per_page=per_page)
|
|
table.set_page(page)
|
|
return jingo.render(request, 'editors/queue.html',
|
|
context(table=table, page=page, tab=tab,
|
|
search_form=search_form))
|
|
|
|
|
|
def _queue_counts(type=None, **kw):
|
|
def construct_query(query_type, days_min=None, days_max=None):
|
|
def apply_query(query, *args):
|
|
query = query.having(*args)
|
|
return query
|
|
|
|
query = query_type.objects
|
|
|
|
if days_min:
|
|
query = apply_query(query, 'waiting_time_days >=', days_min)
|
|
if days_max:
|
|
query = apply_query(query, 'waiting_time_days <=', days_max)
|
|
|
|
return query.count
|
|
|
|
counts = {'pending': construct_query(ViewPendingQueue, **kw),
|
|
'nominated': construct_query(ViewFullReviewQueue, **kw),
|
|
'prelim': construct_query(ViewPreliminaryQueue, **kw),
|
|
'moderated': Review.objects.filter(reviewflag__isnull=False,
|
|
editorreview=1).count}
|
|
rv = {}
|
|
if isinstance(type, basestring):
|
|
return counts[type]()
|
|
for k, v in counts.items():
|
|
if not isinstance(type, list) or k in type:
|
|
rv[k] = v()
|
|
return rv
|
|
|
|
|
|
@editor_required
|
|
def queue(request):
|
|
return redirect(reverse('editors.queue_pending'))
|
|
|
|
|
|
@editor_required
|
|
def queue_nominated(request):
|
|
return _queue(request, ViewFullReviewQueueTable, 'nominated')
|
|
|
|
|
|
@editor_required
|
|
def queue_pending(request):
|
|
return _queue(request, ViewPendingQueueTable, 'pending')
|
|
|
|
|
|
@editor_required
|
|
def queue_prelim(request):
|
|
return _queue(request, ViewPreliminaryQueueTable, 'prelim')
|
|
|
|
|
|
@editor_required
|
|
def queue_moderated(request):
|
|
rf = (Review.objects.filter(editorreview=1, reviewflag__isnull=False,
|
|
addon__isnull=False)
|
|
.order_by('reviewflag__created'))
|
|
|
|
page = paginate(request, rf, per_page=20)
|
|
|
|
flags = dict(ReviewFlag.FLAGS)
|
|
|
|
reviews_formset = ReviewFlagFormSet(request.POST or None,
|
|
queryset=page.object_list)
|
|
|
|
if reviews_formset.is_valid():
|
|
reviews_formset.save()
|
|
return redirect(reverse('editors.queue_moderated'))
|
|
|
|
return jingo.render(request, 'editors/queue.html',
|
|
context(reviews_formset=reviews_formset,
|
|
tab='moderated', page=page, flags=flags,
|
|
search_form=None))
|
|
|
|
|
|
@editor_required
|
|
@post_required
|
|
@json_view
|
|
def application_versions_json(request):
|
|
app_id = request.POST['application_id']
|
|
f = forms.QueueSearchForm()
|
|
return {'choices': f.version_choices_for_app_id(app_id)}
|
|
|
|
|
|
@editor_required
|
|
@addon_view
|
|
def review(request, addon):
|
|
version = addon.latest_version
|
|
|
|
if (not settings.DEBUG and
|
|
addon.authors.filter(user=request.user).exists()):
|
|
amo.messages.warning(request, _('Self-reviews are not allowed.'))
|
|
return redirect(reverse('editors.queue'))
|
|
|
|
form = forms.get_review_form(request.POST or None, request=request,
|
|
addon=addon, version=version)
|
|
|
|
queue_type = (form.helper.review_type if form.helper.review_type
|
|
!= 'preliminary' else 'prelim')
|
|
redirect_url = reverse('editors.queue_%s' % queue_type)
|
|
|
|
num = request.GET.get('num')
|
|
paging = {}
|
|
if num:
|
|
try:
|
|
num = int(num)
|
|
except (ValueError, TypeError):
|
|
raise http.Http404
|
|
total = _queue_counts(queue_type)
|
|
paging = {'current': num, 'total': total,
|
|
'prev': num > 1, 'next': num < total,
|
|
'prev_url': '%s?num=%s' % (redirect_url, num - 1),
|
|
'next_url': '%s?num=%s' % (redirect_url, num + 1)}
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
form.helper.process()
|
|
if form.cleaned_data.get('notify'):
|
|
EditorSubscription.objects.get_or_create(user=request.amo_user,
|
|
addon=addon)
|
|
amo.messages.success(request, _('Review successfully processed.'))
|
|
return redirect(redirect_url)
|
|
|
|
canned = CannedResponse.objects.all()
|
|
is_admin = acl.action_allowed(request, 'Admin', 'EditAnyAddon')
|
|
actions = form.helper.actions.items()
|
|
|
|
statuses = [amo.STATUS_PUBLIC, amo.STATUS_LITE,
|
|
amo.STATUS_LITE_AND_NOMINATED]
|
|
|
|
try:
|
|
show_diff = (addon.versions.exclude(id=version.id)
|
|
.filter(files__isnull=False,
|
|
created__lt=version.created,
|
|
files__status__in=statuses)
|
|
.latest())
|
|
except Version.DoesNotExist:
|
|
show_diff = None
|
|
|
|
# The actions we should show a minimal form from.
|
|
actions_minimal = [k for (k, a) in actions if not a.get('minimal')]
|
|
|
|
# We only allow the user to check/uncheck files for "pending"
|
|
allow_unchecking_files = form.helper.review_type == "pending"
|
|
|
|
versions = (Version.objects.filter(addon=addon).order_by('-created')
|
|
.transform(Version.transformer_activity)
|
|
.transform(Version.transformer))
|
|
pager = amo.utils.paginate(request, versions, 5)
|
|
|
|
num_pages = pager.paginator.num_pages
|
|
count = pager.paginator.count
|
|
|
|
ctx = context(version=version, addon=addon,
|
|
pager=pager, num_pages=num_pages, count=count,
|
|
flags=Review.objects.filter(addon=addon, flag=True),
|
|
form=form, paging=paging, canned=canned, is_admin=is_admin,
|
|
status_types=amo.STATUS_CHOICES, show_diff=show_diff,
|
|
allow_unchecking_files=allow_unchecking_files,
|
|
actions=actions, actions_minimal=actions_minimal)
|
|
|
|
return jingo.render(request, 'editors/review.html', ctx)
|
|
|
|
|
|
@never_cache
|
|
@json_view
|
|
@editor_required
|
|
def review_viewing(request):
|
|
if 'addon_id' not in request.POST:
|
|
return {}
|
|
|
|
addon_id = request.POST['addon_id']
|
|
user_id = request.amo_user.id
|
|
current_name = ''
|
|
is_user = 0
|
|
key = '%s:review_viewing:%s' % (settings.CACHE_PREFIX, addon_id)
|
|
interval = amo.EDITOR_VIEWING_INTERVAL
|
|
|
|
# Check who is viewing.
|
|
currently_viewing = cache.get(key)
|
|
|
|
# If nobody is viewing or current user is, set current user as viewing
|
|
if not currently_viewing or currently_viewing == user_id:
|
|
# We want to save it for twice as long as the ping interval,
|
|
# just to account for latency and the like.
|
|
cache.set(key, user_id, interval * 2)
|
|
currently_viewing = user_id
|
|
current_name = request.amo_user.name
|
|
is_user = 1
|
|
else:
|
|
current_name = UserProfile.objects.get(pk=currently_viewing).name
|
|
|
|
return {'current': currently_viewing, 'current_name': current_name,
|
|
'is_user': is_user, 'interval_seconds': interval}
|
|
|
|
|
|
@editor_required
|
|
def reviewlog(request):
|
|
data = request.GET.copy()
|
|
|
|
if not data.get('start') and not data.get('end'):
|
|
today = date.today()
|
|
data['start'] = date(today.year, today.month, 1)
|
|
|
|
form = forms.ReviewLogForm(data)
|
|
|
|
approvals = ActivityLog.objects.review_queue()
|
|
|
|
if form.is_valid():
|
|
data = form.cleaned_data
|
|
if data['start']:
|
|
approvals = approvals.filter(created__gte=data['start'])
|
|
if data['end']:
|
|
approvals = approvals.filter(created__lt=data['end'])
|
|
if data['search']:
|
|
term = data['search']
|
|
approvals = approvals.filter(
|
|
Q(commentlog__comments__contains=term) |
|
|
Q(addonlog__addon__name__localized_string__contains=term) |
|
|
Q(user__display_name__contains=term) |
|
|
Q(user__username__contains=term)).distinct()
|
|
|
|
pager = amo.utils.paginate(request, approvals, 50)
|
|
ad = {
|
|
amo.LOG.APPROVE_VERSION.id: _('was approved'),
|
|
amo.LOG.PRELIMINARY_VERSION.id: _('given preliminary review'),
|
|
amo.LOG.REJECT_VERSION.id: _('rejected'),
|
|
amo.LOG.ESCALATE_VERSION.id: _('escalated',
|
|
'editors_review_history_nominated_adminreview'),
|
|
amo.LOG.REQUEST_INFORMATION.id: _('needs more information'),
|
|
amo.LOG.REQUEST_SUPER_REVIEW.id: _('needs super review'),
|
|
}
|
|
data = context(form=form, pager=pager, ACTION_DICT=ad)
|
|
return jingo.render(request, 'editors/reviewlog.html', data)
|