Landing page for mkt reviewers (bug 741634)

This commit is contained in:
Rob Hudson 2012-04-17 17:09:51 -07:00
Родитель 6809e5ebb5
Коммит 0ee7d06a20
11 изменённых файлов: 274 добавлений и 52 удалений

Просмотреть файл

@ -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) }}
&bull; {{ _('{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)