addons-server/apps/editors/views.py

662 строки
23 KiB
Python

from collections import defaultdict
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, Count
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 abuse.models import AbuseReport
from access import acl
from addons.decorators import addon_view
from addons.models import Addon, Version
from amo.decorators import json_view, login_required, post_required
from amo.utils import paginate
from amo.urlresolvers import reverse
from devhub.models import ActivityLog, CommentLog
from editors import forms
from editors.models import (EditorSubscription, ViewPendingQueue,
ViewFullReviewQueue, ViewPreliminaryQueue,
EventLog, AddonCannedResponse, PerformanceGraph,
ViewFastTrackQueue)
from editors.helpers import (ViewPendingQueueTable, ViewFullReviewQueueTable,
ViewPreliminaryQueueTable, WebappQueueTable,
ViewFastTrackQueueTable)
from reviews.forms import ReviewFlagFormSet
from reviews.models import Review, ReviewFlag
from users.models import UserProfile
from mkt.webapps.models import Webapp
from zadmin.models import get_config, set_config
def _view_on_get(request):
"""Returns whether the user can access this page.
If the user is in a group with rule 'ReviewerTools:View' and the request is
a GET request, they are allowed to view.
"""
return (request.method == 'GET' and
acl.action_allowed(request, 'ReviewerTools', 'View'))
def reviewer_required(only=None):
"""Requires the user to be logged in as a reviewer or admin, or allows
someone with rule 'ReviewerTools:View' for GET requests.
Reviewer is someone who is in one of the groups with the following
permissions:
Addons:Review
Apps:Review
Personas:Review
If only is provided, it will only check for a certain type of reviewer.
Valid values for only are: addon, app, persona.
"""
def decorator(f):
@login_required
@functools.wraps(f)
def wrapper(request, *args, **kw):
if acl.check_reviewer(request, only) or _view_on_get(request):
return f(request, *args, **kw)
else:
return http.HttpResponseForbidden()
return wrapper
# If decorator has no args, and is "paren-less", it's callable.
if callable(only):
return decorator(only)
else:
return decorator
def context(**kw):
ctx = dict(motd=get_config('editors_review_motd'),
queue_counts=queue_counts())
ctx.update(kw)
return ctx
@reviewer_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)
@reviewer_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)
@reviewer_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)
@reviewer_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
@reviewer_required
def motd(request):
form = None
if acl.action_allowed(request, 'AddonReviewerMOTD', 'Edit'):
form = forms.MOTDForm()
data = context(form=form)
return jingo.render(request, 'editors/motd.html', data)
@reviewer_required
@post_required
def save_motd(request):
if not acl.action_allowed(request, 'AddonReviewerMOTD', 'Edit'):
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=None):
if qs is None:
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 http.HttpResponseRedirect('%s?num=%s' % (
TableObj.review_url(row),
review_num))
except IndexError:
pass
order_by = request.GET.get('sort', TableObj.default_order_by())
order_by = TableObj.translate_sort_cols(order_by)
table = TableObj(data=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),
'fast_track': construct_query(ViewFastTrackQueue, **kw),
'moderated': (
Review.objects.exclude(addon__type=amo.ADDON_WEBAPP)
.filter(reviewflag__isnull=False,
editorreview=1).count),
'apps': Webapp.objects.pending().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
@reviewer_required
def queue(request):
return redirect(reverse('editors.queue_pending'))
@reviewer_required
def queue_nominated(request):
return _queue(request, ViewFullReviewQueueTable, 'nominated')
@reviewer_required
def queue_pending(request):
return _queue(request, ViewPendingQueueTable, 'pending')
@reviewer_required
def queue_prelim(request):
return _queue(request, ViewPreliminaryQueueTable, 'prelim')
@reviewer_required
def queue_fast_track(request):
return _queue(request, ViewFastTrackQueueTable, 'fast_track')
@reviewer_required
def queue_moderated(request):
rf = (Review.objects.exclude(
Q(addon__type=amo.ADDON_WEBAPP) |
Q(addon__isnull=True) |
Q(reviewflag__isnull=True))
.filter(editorreview=1)
.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))
@reviewer_required('app')
def queue_apps(request):
qs = Webapp.objects.pending().annotate(Count('abuse_reports'))
return _queue(request, WebappQueueTable, 'apps', qs=qs)
@reviewer_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)}
@reviewer_required
@addon_view
def review(request, addon):
return _review(request, addon)
@reviewer_required('app')
@addon_view
def app_review(request, addon):
return _review(request, addon)
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)}
is_admin = acl.action_allowed(request, 'Addons', 'Edit')
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)
if form.cleaned_data.get('adminflag') and is_admin:
addon.update(admin_review=False)
amo.messages.success(request, _('Review successfully processed.'))
return redirect(redirect_url)
canned = AddonCannedResponse.objects.all()
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)
.exclude(files__status=amo.STATUS_BETA)
.order_by('-created')
.transform(Version.transformer_activity)
.transform(Version.transformer))
class PseudoVersion(object):
def __init__(self):
self.all_activity = []
all_files = ()
approvalnotes = None
compatible_apps_ordered = ()
releasenotes = None
status = 'Deleted',
@property
def created(self):
return self.all_activity[0].created
@property
def version(self):
return (self.all_activity[0].activity_log
.details.get('version', '[deleted]'))
# Grab review history for deleted versions of this add-on
comments = (CommentLog.objects
.filter(activity_log__action__in=amo.LOG_REVIEW_QUEUE,
activity_log__versionlog=None,
activity_log__addonlog__addon=addon)
.order_by('created')
.select_related('activity_log'))
comment_versions = defaultdict(PseudoVersion)
for c in comments:
c.version = c.activity_log.details.get('version', c.created)
comment_versions[c.version].all_activity.append(c)
all_versions = comment_versions.values()
all_versions.extend(versions)
all_versions.sort(key=lambda v: v.created,
reverse=True)
pager = amo.utils.paginate(request, all_versions, 10)
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
@reviewer_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}
@never_cache
@json_view
@reviewer_required
def queue_viewing(request):
if 'addon_ids' not in request.POST:
return {}
viewing = {}
user_id = request.amo_user.id
for addon_id in request.POST['addon_ids'].split(','):
addon_id = addon_id.strip()
key = '%s:review_viewing:%s' % (settings.CACHE_PREFIX, addon_id)
currently_viewing = cache.get(key)
if currently_viewing and currently_viewing != user_id:
viewing[addon_id] = (UserProfile.objects
.get(id=currently_viewing)
.display_name)
return viewing
@json_view
@reviewer_required
def queue_version_notes(request, addon_id):
addon = get_object_or_404(Addon, pk=addon_id)
version = addon.latest_version
return {'releasenotes': unicode(version.releasenotes),
'approvalnotes': version.approvalnotes}
@reviewer_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)
@reviewer_required
@addon_view
def abuse_reports(request, addon):
reports = AbuseReport.objects.filter(addon=addon).order_by('-created')
total = reports.count()
reports = amo.utils.paginate(request, reports)
return jingo.render(request, 'editors/abuse_reports.html',
dict(addon=addon, reports=reports, total=total))