Browse files

Landing page for mkt reviewers (bug 741634)

  • Loading branch information...
1 parent 6809e5e commit 0ee7d06a203a2e5d6f9c5e7392fddad4b145626d @robhudson robhudson committed Apr 17, 2012
View
24 apps/devhub/models.py
@@ -191,22 +191,24 @@ def review_queue(self, webapp=False):
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()
View
8 media/css/mkt/reviewers.less
@@ -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;
View
9 mkt/reviewers/helpers.py
@@ -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']))]
View
6 mkt/reviewers/templates/reviewers/base.html
@@ -27,13 +27,13 @@ <h1 id="masthead" class="site-title prominent">
<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">
View
105 mkt/reviewers/templates/reviewers/home.html
@@ -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 %}
View
4 mkt/reviewers/templates/reviewers/logs.html
@@ -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 %}
View
2 mkt/reviewers/templates/reviewers/queue.html
@@ -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>
View
75 mkt/reviewers/tests/test_views.py
@@ -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 @@ def test_403_for_anonymous(self):
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 @@ def setUp(self):
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 @@ def setUp(self):
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 @@ def setUp(self):
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 @@ def setUp(self):
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]
View
10 mkt/reviewers/urls.py
@@ -4,11 +4,13 @@
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'),
)
View
6 mkt/reviewers/utils.py
@@ -74,7 +74,7 @@ def default_order_by(cls):
@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 @@ def get_context_data(self):
'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 @@ def set_data(self, data):
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):
View
81 mkt/reviewers/views.py
@@ -1,4 +1,4 @@
-from datetime import date
+import datetime
from django import http
from django.conf import settings
@@ -29,18 +29,63 @@
@reviewer_required
def home(request):
- # TODO: Implement landing page for apps (bug 741634).
- return redirect('reviewers.queue_apps')
-
-
-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())
+ 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 = {
+ '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,16 +223,16 @@ 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')
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)

0 comments on commit 0ee7d06

Please sign in to comment.