Landing page for mkt reviewers (bug 741634)
This commit is contained in:
Родитель
6809e5ebb5
Коммит
0ee7d06a20
|
@ -191,22 +191,24 @@ class ActivityLogManager(amo.models.ManagerBase):
|
|||
qs = self._by_type(webapp)
|
||||
return qs.filter(action__in=amo.LOG_REVIEW_QUEUE)
|
||||
|
||||
def total_reviews(self):
|
||||
def total_reviews(self, webapp=False):
|
||||
qs = self._by_type(webapp)
|
||||
"""Return the top users, and their # of reviews."""
|
||||
return (self.values('user', 'user__display_name')
|
||||
.filter(action__in=amo.LOG_REVIEW_QUEUE)
|
||||
.annotate(approval_count=models.Count('id'))
|
||||
.order_by('-approval_count'))
|
||||
return (qs.values('user', 'user__display_name', 'user__username')
|
||||
.filter(action__in=amo.LOG_REVIEW_QUEUE)
|
||||
.annotate(approval_count=models.Count('id'))
|
||||
.order_by('-approval_count'))
|
||||
|
||||
def monthly_reviews(self):
|
||||
def monthly_reviews(self, webapp=False):
|
||||
"""Return the top users for the month, and their # of reviews."""
|
||||
qs = self._by_type(webapp)
|
||||
now = datetime.now()
|
||||
created_date = datetime(now.year, now.month, 1)
|
||||
return (self.values('user', 'user__display_name')
|
||||
.filter(created__gte=created_date,
|
||||
action__in=amo.LOG_REVIEW_QUEUE)
|
||||
.annotate(approval_count=models.Count('id'))
|
||||
.order_by('-approval_count'))
|
||||
return (qs.values('user', 'user__display_name', 'user__username')
|
||||
.filter(created__gte=created_date,
|
||||
action__in=amo.LOG_REVIEW_QUEUE)
|
||||
.annotate(approval_count=models.Count('id'))
|
||||
.order_by('-approval_count'))
|
||||
|
||||
def _by_type(self, webapp=False):
|
||||
qs = super(ActivityLogManager, self).get_query_set()
|
||||
|
|
|
@ -396,10 +396,16 @@ ul.tabnav a:hover {
|
|||
|
||||
.editor-stats-title,
|
||||
.editor-stats-table {
|
||||
width: 33.3333%;
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#editors-stats-charts .editor-stats-title,
|
||||
#editors-stats-charts .editor-stats-table {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.editor-stats-title span,
|
||||
.editor-stats-title a {
|
||||
font-weight: bold;
|
||||
|
|
|
@ -27,13 +27,13 @@ def reviewers_breadcrumbs(context, queue=None, addon_queue=None, items=None):
|
|||
crumbs = [(reverse('reviewers.home'), _('Reviewer Tools'))]
|
||||
|
||||
if addon_queue and addon_queue.type == amo.ADDON_WEBAPP:
|
||||
queue = 'apps'
|
||||
queue = 'pending'
|
||||
|
||||
if queue:
|
||||
queues = {'apps': _('Apps')}
|
||||
queues = {'pending': _('Apps')}
|
||||
|
||||
if items and not queue == 'queue':
|
||||
url = reverse('reviewers.queue_%s' % queue)
|
||||
url = reverse('reviewers.apps.queue_%s' % queue)
|
||||
else:
|
||||
# The Addon is the end of the trail.
|
||||
url = None
|
||||
|
@ -63,4 +63,5 @@ def queue_tabnav(context):
|
|||
Each tuple contains three elements: (tab_code, page_url, tab_text)
|
||||
"""
|
||||
counts = queue_counts()
|
||||
return [('apps', 'queue_apps', _('Apps ({0})').format(counts['apps']))]
|
||||
return [('apps', 'queue_pending',
|
||||
_('Apps ({0})').format(counts['pending']))]
|
||||
|
|
|
@ -27,13 +27,13 @@
|
|||
<a href="#" class="controller">{{ _('Queues') }}</a>
|
||||
{% if queue_counts %}
|
||||
<ul>
|
||||
<li><a href="{{ url('reviewers.queue_apps') }}">
|
||||
{{ _('Apps') }} ({{ queue_counts['apps'] }})</a></li>
|
||||
<li><a href="{{ url('reviewers.apps.queue_pending') }}">
|
||||
{{ _('Apps') }} ({{ queue_counts['pending'] }})</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="slim">
|
||||
<a href="{{ url('reviewers.logs') }}">{{ _('Logs') }}</a>
|
||||
<a href="{{ url('reviewers.apps.logs') }}">{{ _('Logs') }}</a>
|
||||
</li>
|
||||
{# TODO: Implement MOTD for apps (bug 741529). #}
|
||||
<li class="slim">
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
{% extends 'reviewers/base.html' %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ reviewers_breadcrumbs(queue=tab) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="island">
|
||||
<div class="featured" id="editors-stats-charts">
|
||||
<div class="listing-header">
|
||||
<div class="editor-stats-title">
|
||||
<a href="{{ url('reviewers.apps.queue_pending') }}">
|
||||
{{ ngettext('Pending Update ({num})',
|
||||
'Pending Updates ({num})',
|
||||
queue_counts['pending'])|f(num=queue_counts['pending']) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="editor-stats">
|
||||
{% for type in ['pending']: %}
|
||||
<div class="editor-stats-table">
|
||||
<div>
|
||||
{{ ngettext("{c} unreviewed submissions.",
|
||||
"{c} unreviewed submissions.",
|
||||
progress['week'])|f(c=progress['week']) }}
|
||||
</div>
|
||||
<div class="editor-stats-dark">
|
||||
<strong>{{ _('Current waiting times:') }}</strong>
|
||||
<div class="editor-waiting">
|
||||
{% for (d, duration) in durations: %}
|
||||
{% set total = progress[d] %}
|
||||
<div class="waiting_{{ d }} tooltip"
|
||||
data-delay="100"
|
||||
style="width:{{ percentage[d] }}%"
|
||||
title="{{ duration }} ::
|
||||
{{ ngettext('{0} app', '{0} apps', total)|f(total) }}
|
||||
• {{ _('{0}%')|f(percentage[d]|round|int) }}"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="island c">
|
||||
<div class="featured" id="editors-stats">
|
||||
<div class="listing-header">
|
||||
<div class="editor-stats-title"><span>{{ _('Total Reviews') }}</span></div>
|
||||
<div class="editor-stats-title"><span>{{ _('Reviews This Month') }}</span></div>
|
||||
{#<div class="editor-stats-title"><span>{{ _('New Reviewers') }}</span></div>#}
|
||||
</div>
|
||||
<div class="editor-stats">
|
||||
<div class="editor-stats-table">
|
||||
<div>
|
||||
<table>
|
||||
{% for row in reviews_total %}
|
||||
<tr>
|
||||
<td>{{ row['user__display_name']|d(row['user__username'], true) }}</td>
|
||||
<td class="int">{{ row['approval_count']|numberfmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-stats-table">
|
||||
<div>
|
||||
<table>
|
||||
{% for row in reviews_monthly %}
|
||||
<tr>
|
||||
<td>{{ row['user__display_name']|d(row['user__username'], true) }}</td>
|
||||
<td class="int">{{ row['approval_count']|numberfmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{# TODO: Bug 747035
|
||||
<div class="editor-stats-table">
|
||||
<div>
|
||||
<table>
|
||||
{% for editors in new_editors %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url('users.profile', editors['added']) }}">
|
||||
{{ editors['display_name'] }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="date" title="{{ editors['created']|babel_datetime }}">
|
||||
{{ editors['created']|timesince }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# TODO: Bug 746755 -- Moderated user review queue #}
|
||||
|
||||
{% endblock %}
|
|
@ -7,7 +7,7 @@
|
|||
{% block content %}
|
||||
|
||||
<div id="log-filter" class="log-filter-outside">
|
||||
<form action="{{ url('reviewers.logs') }}" method="get">
|
||||
<form action="{{ url('reviewers.apps.logs') }}" method="get">
|
||||
<div class="date_range">
|
||||
{{ form.start.label_tag() }}
|
||||
{{ form.start }}
|
||||
|
@ -44,7 +44,7 @@
|
|||
{{ item.arguments.0|link }}
|
||||
{% if item.arguments|count >= 2 %}
|
||||
{{ item.arguments[1] }}
|
||||
<a href="{{ url('reviewers.app_review', item.arguments[0].app_slug) }}">
|
||||
<a href="{{ url('reviewers.apps.review', item.arguments[0].app_slug) }}">
|
||||
{{ ACTION_DICT.get(item.action).short }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<ul class="tabnav">
|
||||
{% for this, loc, text in queue_tabnav() %}
|
||||
<li class="{% if tab == this %}selected{% endif %}">
|
||||
<a href="{{ url('reviewers.%s' % loc) }}">{{ text }}</a></li>
|
||||
<a href="{{ url('reviewers.apps.%s' % loc) }}">{{ text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import datetime
|
||||
from itertools import cycle
|
||||
import time
|
||||
|
||||
from django.core import mail
|
||||
from django.conf import settings
|
||||
|
||||
import mock
|
||||
from nose.tools import eq_
|
||||
from nose.tools import eq_, ok_
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from addons.models import AddonUser
|
||||
|
@ -35,6 +37,65 @@ class AppReviewerTest(object):
|
|||
eq_(self.client.head(self.url).status_code, 403)
|
||||
|
||||
|
||||
class TestReviewersHome(EditorTest):
|
||||
|
||||
def setUp(self):
|
||||
self.login_as_editor()
|
||||
super(TestReviewersHome, self).setUp()
|
||||
self.login_as_editor()
|
||||
self.apps = [app_factory(name='Antelope',
|
||||
status=amo.WEBAPPS_UNREVIEWED_STATUS),
|
||||
app_factory(name='Bear',
|
||||
status=amo.WEBAPPS_UNREVIEWED_STATUS),
|
||||
app_factory(name='Cougar',
|
||||
status=amo.WEBAPPS_UNREVIEWED_STATUS)]
|
||||
self.url = reverse('reviewers.home')
|
||||
|
||||
def test_stats_waiting(self):
|
||||
now = datetime.datetime.now()
|
||||
days_ago = lambda n: now - datetime.timedelta(days=n)
|
||||
|
||||
self.apps[0].update(created=days_ago(1))
|
||||
self.apps[1].update(created=days_ago(5))
|
||||
self.apps[2].update(created=days_ago(15))
|
||||
|
||||
doc = pq(self.client.get(self.url).content)
|
||||
|
||||
# Total unreviewed apps.
|
||||
eq_(doc('.editor-stats-title a').text(), 'Pending Updates (3)')
|
||||
# Unreviewed submissions in the past week.
|
||||
ok_('2 unreviewed submissions.' in
|
||||
doc('.editor-stats-table > div').text())
|
||||
# Maths.
|
||||
eq_(doc('.waiting_new').attr('title')[-3:], '33%')
|
||||
eq_(doc('.waiting_med').attr('title')[-3:], '33%')
|
||||
eq_(doc('.waiting_old').attr('title')[-3:], '33%')
|
||||
|
||||
def test_reviewer_leaders(self):
|
||||
reviewers = UserProfile.objects.all()[:2]
|
||||
# 1st user reviews 2, 2nd user only 1.
|
||||
users = cycle(reviewers)
|
||||
for app in self.apps:
|
||||
amo.log(amo.LOG.APPROVE_VERSION, app, app.current_version,
|
||||
user=users.next(), details={'comments': 'hawt'})
|
||||
|
||||
doc = pq(self.client.get(self.url).content.decode('utf-8'))
|
||||
|
||||
# Top Reviews.
|
||||
table = doc('#editors-stats .editor-stats-table').eq(0)
|
||||
eq_(table.find('td').eq(0).text(), reviewers[0].name)
|
||||
eq_(table.find('td').eq(1).text(), u'2')
|
||||
eq_(table.find('td').eq(2).text(), reviewers[1].name)
|
||||
eq_(table.find('td').eq(3).text(), u'1')
|
||||
|
||||
# Top Reviews this month.
|
||||
table = doc('#editors-stats .editor-stats-table').eq(1)
|
||||
eq_(table.find('td').eq(0).text(), reviewers[0].name)
|
||||
eq_(table.find('td').eq(1).text(), u'2')
|
||||
eq_(table.find('td').eq(2).text(), reviewers[1].name)
|
||||
eq_(table.find('td').eq(3).text(), u'1')
|
||||
|
||||
|
||||
class TestAppQueue(AppReviewerTest, EditorTest):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -43,10 +104,10 @@ class TestAppQueue(AppReviewerTest, EditorTest):
|
|||
status=amo.WEBAPPS_UNREVIEWED_STATUS),
|
||||
app_factory(name='YYY',
|
||||
status=amo.WEBAPPS_UNREVIEWED_STATUS)]
|
||||
self.url = reverse('reviewers.queue_apps')
|
||||
self.url = reverse('reviewers.apps.queue_pending')
|
||||
|
||||
def review_url(self, app, num):
|
||||
return urlparams(reverse('reviewers.app_review', args=[app.app_slug]),
|
||||
return urlparams(reverse('reviewers.apps.review', args=[app.app_slug]),
|
||||
num=num)
|
||||
|
||||
def test_restricted_results(self):
|
||||
|
@ -89,14 +150,14 @@ class TestReviewApp(AppReviewerTest, EditorTest):
|
|||
self.app = self.get_app()
|
||||
self.app.update(status=amo.STATUS_PENDING)
|
||||
self.version = self.app.current_version
|
||||
self.url = reverse('reviewers.app_review', args=[self.app.app_slug])
|
||||
self.url = reverse('reviewers.apps.review', args=[self.app.app_slug])
|
||||
|
||||
def get_app(self):
|
||||
return Webapp.objects.get(id=337141)
|
||||
|
||||
def post(self, data):
|
||||
r = self.client.post(self.url, data)
|
||||
self.assertRedirects(r, reverse('reviewers.queue_apps'))
|
||||
self.assertRedirects(r, reverse('reviewers.apps.queue_pending'))
|
||||
|
||||
@mock.patch.object(settings, 'DEBUG', False)
|
||||
def test_cannot_review_my_app(self):
|
||||
|
@ -212,7 +273,7 @@ class TestCannedResponses(EditorTest):
|
|||
self.cr_app = CannedResponse.objects.create(
|
||||
name=u'app reason', response=u'app reason body',
|
||||
sort_group=u'public', type=amo.CANNED_RESPONSE_APP)
|
||||
self.url = reverse('reviewers.app_review', args=[self.app.app_slug])
|
||||
self.url = reverse('reviewers.apps.review', args=[self.app.app_slug])
|
||||
|
||||
def test_no_addon(self):
|
||||
r = self.client.get(self.url)
|
||||
|
@ -239,7 +300,7 @@ class TestReviewLog(EditorTest):
|
|||
status=amo.WEBAPPS_UNREVIEWED_STATUS),
|
||||
app_factory(name='YYY',
|
||||
status=amo.WEBAPPS_UNREVIEWED_STATUS)]
|
||||
self.url = reverse('reviewers.logs')
|
||||
self.url = reverse('reviewers.apps.logs')
|
||||
|
||||
def get_user(self):
|
||||
return UserProfile.objects.all()[0]
|
||||
|
|
|
@ -4,11 +4,13 @@ from mkt.urls import APP_SLUG
|
|||
from . import views
|
||||
|
||||
|
||||
# All URLs under /editortools/.
|
||||
# All URLs under /reviewers/.
|
||||
urlpatterns = (
|
||||
url(r'^$', views.home, name='reviewers.home'),
|
||||
url(r'^queue/apps$', views.queue_apps, name='reviewers.queue_apps'),
|
||||
url(r'^apps/queue/$', views.queue_apps,
|
||||
name='reviewers.apps.queue_pending'),
|
||||
url(r'^apps/review/%s$' % APP_SLUG, views.app_review,
|
||||
name='reviewers.app_review'),
|
||||
url(r'^logs$', views.logs, name='reviewers.logs'),
|
||||
name='reviewers.apps.review'),
|
||||
url(r'^apps/logs$', views.logs,
|
||||
name='reviewers.apps.logs'),
|
||||
)
|
||||
|
|
|
@ -74,7 +74,7 @@ class WebappQueueTable(tables.ModelTable, ItemStateTable):
|
|||
|
||||
@classmethod
|
||||
def review_url(cls, row):
|
||||
return reverse('reviewers.app_review', args=[row.app_slug])
|
||||
return reverse('reviewers.apps.review', args=[row.app_slug])
|
||||
|
||||
class Meta:
|
||||
sortable = True
|
||||
|
@ -142,7 +142,7 @@ class ReviewBase:
|
|||
'reviewer': self.request.user.get_profile().name,
|
||||
'detail_url': absolutify(
|
||||
self.addon.get_url_path(add_prefix=False)),
|
||||
'review_url': absolutify(reverse('reviewers.app_review',
|
||||
'review_url': absolutify(reverse('reviewers.apps.review',
|
||||
args=[self.addon.app_slug],
|
||||
add_prefix=False)),
|
||||
'status_url': absolutify(self.addon.get_dev_url('versions')),
|
||||
|
@ -254,7 +254,7 @@ class ReviewHelper:
|
|||
|
||||
def get_review_type(self, request, addon, version):
|
||||
if self.addon.type == amo.ADDON_WEBAPP:
|
||||
self.review_type = 'apps'
|
||||
self.review_type = 'pending'
|
||||
self.handler = ReviewApp(request, addon, version, 'pending')
|
||||
|
||||
def get_actions(self):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import date
|
||||
import datetime
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
|
@ -29,18 +29,63 @@ from .models import AppCannedResponse
|
|||
|
||||
@reviewer_required
|
||||
def home(request):
|
||||
# TODO: Implement landing page for apps (bug 741634).
|
||||
return redirect('reviewers.queue_apps')
|
||||
durations = (('new', _('New Apps (Under 5 days)')),
|
||||
('med', _('Passable (5 to 10 days)')),
|
||||
('old', _('Overdue (Over 10 days)')))
|
||||
|
||||
progress, percentage = _progress()
|
||||
|
||||
data = context(
|
||||
reviews_total=ActivityLog.objects.total_reviews(webapp=True)[:5],
|
||||
reviews_monthly=ActivityLog.objects.monthly_reviews(webapp=True)[:5],
|
||||
#new_editors=EventLog.new_editors(), # Bug 747035
|
||||
#eventlog=ActivityLog.objects.editor_events()[:6], # Bug 746755
|
||||
progress=progress,
|
||||
percentage=percentage,
|
||||
durations=durations
|
||||
)
|
||||
return jingo.render(request, 'reviewers/home.html', data)
|
||||
|
||||
|
||||
def queue_counts(type_=None, **kw):
|
||||
counts = {'apps': Webapp.objects.pending().count}
|
||||
if type_:
|
||||
# Evaluate count for only this type.
|
||||
return counts.get(type_)()
|
||||
else:
|
||||
# Evaluate all counts.
|
||||
return dict((k, v()) for k, v in counts.iteritems())
|
||||
def queue_counts(type=None, **kw):
|
||||
counts = {
|
||||
'pending': 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
|
||||
|
||||
|
||||
def _progress():
|
||||
"""Returns unreviewed apps progress.
|
||||
|
||||
Return the number of apps still unreviewed for a given period of time and
|
||||
the percentage.
|
||||
"""
|
||||
|
||||
days_ago = lambda n: datetime.datetime.now() - datetime.timedelta(days=n)
|
||||
qs = Webapp.objects.pending()
|
||||
progress = {
|
||||
'new': qs.filter(created__gt=days_ago(5)).count(),
|
||||
'med': qs.filter(created__range=(days_ago(10), days_ago(5))).count(),
|
||||
'old': qs.filter(created__lt=days_ago(10)).count(),
|
||||
'week': qs.filter(created__gte=days_ago(7)).count(),
|
||||
}
|
||||
|
||||
# Return the percent of (p)rogress out of (t)otal.
|
||||
pct = lambda p, t: (p / float(t)) * 100 if p > 0 else 0
|
||||
|
||||
percentage = {}
|
||||
total = progress['new'] + progress['med'] + progress['old']
|
||||
percentage = {}
|
||||
for duration in ('new', 'med', 'old'):
|
||||
percentage[duration] = pct(progress[duration], total)
|
||||
|
||||
return (progress, percentage)
|
||||
|
||||
|
||||
def _queue(request, TableObj, tab, qs=None):
|
||||
|
@ -65,7 +110,7 @@ def _queue(request, TableObj, tab, qs=None):
|
|||
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 = 10 # TODO: Change to 100.
|
||||
default = 100
|
||||
per_page = request.GET.get('per_page', default)
|
||||
try:
|
||||
per_page = int(per_page)
|
||||
|
@ -99,7 +144,7 @@ def _review(request, addon):
|
|||
|
||||
queue_type = (form.helper.review_type if form.helper.review_type
|
||||
!= 'preliminary' else 'prelim')
|
||||
redirect_url = reverse('reviewers.queue_%s' % queue_type)
|
||||
redirect_url = reverse('reviewers.apps.queue_%s' % queue_type)
|
||||
|
||||
num = request.GET.get('num')
|
||||
paging = {}
|
||||
|
@ -178,7 +223,7 @@ def app_review(request, addon):
|
|||
@permission_required('Apps', 'Review')
|
||||
def queue_apps(request):
|
||||
qs = Webapp.objects.pending().annotate(Count('abuse_reports'))
|
||||
return _queue(request, utils.WebappQueueTable, 'apps', qs=qs)
|
||||
return _queue(request, utils.WebappQueueTable, 'pending', qs=qs)
|
||||
|
||||
|
||||
@permission_required('Apps', 'Review')
|
||||
|
@ -186,8 +231,8 @@ def logs(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)
|
||||
today = datetime.date.today()
|
||||
data['start'] = datetime.date(today.year, today.month, 1)
|
||||
|
||||
form = forms.ReviewAppLogForm(data)
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче