Converted apps review queue to use ES (bug 746764)

This commit is contained in:
Rob Hudson 2012-04-24 14:29:20 -07:00
Родитель 6cf43197e9
Коммит f802e8c358
8 изменённых файлов: 284 добавлений и 194 удалений

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

@ -0,0 +1,68 @@
[
{
"pk": 1,
"model": "addons.devicetype",
"fields": {
"name": 2425886,
"class_name": "desktop",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
},
{
"pk": 2527136,
"model": "translations.translation",
"fields": {
"id": 2425886,
"localized_string": "Desktop",
"localized_string_clean": null,
"locale": "en-US",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
},
{
"pk": 2,
"model": "addons.devicetype",
"fields": {
"name": 2425887,
"class_name": "mobile",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
},
{
"pk": 2527137,
"model": "translations.translation",
"fields": {
"id": 2425887,
"localized_string": "Mobile",
"localized_string_clean": null,
"locale": "en-US",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
},
{
"pk": 3,
"model": "addons.devicetype",
"fields": {
"name": 2425888,
"class_name": "tablet",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
},
{
"pk": 2527138,
"model": "translations.translation",
"fields": {
"id": 2425888,
"localized_string": "Tablet",
"localized_string_clean": null,
"locale": "en-US",
"created": "2012-05-16 22:36:00",
"modified": "2012-05-16 22:36:00"
}
}
]

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

@ -342,66 +342,6 @@ h1 .num {
}
}
.device-list {
margin-top: 10px;
h4, ul {
display: inline-block;
line-height: 20px;
vertical-align: top;
}
ul {
color: @medium-gray;
margin: 0;
padding: 0;
}
li {
display: inline-block;
height: 23px;
position: relative;
text-indent: -99999px;
width: 26px;
background: url(../../img/mkt/icons/device_icons.png) no-repeat 0 0;
&.mobile.unavailable {
background-position: 0 -23px;
}
&.tablet {
background-position: 0 -46px;
&.unavailable {
background-position: 0 -69px;
}
}
&.desktop {
background-position: 0 -92px;
&.unavailable {
background-position: 0 -115px;
}
}
}
}
.listing .device-list {
text-align: center;
h4, ul {
display: block;
}
h4 {
color: @gray;
font-size: 11px;
font-weight: normal;
}
ul {
color: @medium-gray;
font-size: 11px;
margin: 0;
padding: 0;
}
}
.html-rtl .device-list {
li {
margin: 0 5px 0 0;
}
}
.support ul {
margin-top: 47px;
list-style-type: none;

61
media/css/mkt/device.less Normal file
Просмотреть файл

@ -0,0 +1,61 @@
@import 'lib';
.device-list {
margin-top: 10px;
h4, ul {
display: inline-block;
line-height: 20px;
vertical-align: top;
}
ul {
color: @medium-gray;
margin: 0;
padding: 0;
}
li {
display: inline-block;
height: 23px;
position: relative;
text-indent: -99999px;
width: 26px;
background: url(../../img/mkt/icons/device_icons.png) no-repeat 0 0;
&.mobile.unavailable {
background-position: 0 -23px;
}
&.tablet {
background-position: 0 -46px;
&.unavailable {
background-position: 0 -69px;
}
}
&.desktop {
background-position: 0 -92px;
&.unavailable {
background-position: 0 -115px;
}
}
}
}
.listing .device-list {
text-align: center;
h4, ul {
display: block;
}
h4 {
color: @gray;
font-size: 11px;
font-weight: normal;
}
ul {
color: @medium-gray;
font-size: 11px;
margin: 0;
padding: 0;
}
}
.html-rtl .device-list {
li {
margin: 0 5px 0 0;
}
}

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

@ -24,6 +24,7 @@ CSS = {
# Popups, Modals, Tooltips.
'css/devreg/devhub-popups.less',
'css/mkt/device.less',
'css/devreg/tooltips.less',
# L10n menu ("Localize for ...").
@ -81,6 +82,7 @@ CSS = {
'css/mkt/buttons.less',
'css/mkt/detail.less',
'css/mkt/ratings.less',
'css/mkt/device.less',
'css/mkt/slider.less',
'css/mkt/promo-grid.less',
'css/mkt/overlay.less',

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

@ -18,45 +18,47 @@
<thead>
<tr class="listing-header">
<th>&nbsp;</th>
{% for column in table.columns %}
{% if column.is_ordered_reverse %}
{% set cls, sprite = 'ordered', 'desc' %}
{% elif column.is_ordered_straight %}
{% set cls, sprite = 'ordered', 'asc' %}
{% else %}
{% set cls, sprite = '', 'both' %}
{% endif %}
<th class="{{ cls }}">
{% if column.sortable %}
<a href="{{ request.get_full_path()|urlparams(sort=column.name_toggled) }}"
class="sort-icon ed-sprite-sort-{{ sprite }}">
{{ column }}
</a>
{% else %}
{{ column }}
{% endif %}
</th>
{% endfor %}
<th>{{ _('App') }}</th>
<th>{{ _('Flags') }}</th>
<th>{{ _('Waiting Time') }}</th>
<th>{{ _('Devices') }}</th>
<th>{{ _('Payments') }}</th>
<th>{{ _('Abuse Reports') }}</th>
</tr>
</thead>
<tbody>
{% for row in page.object_list %}
<tr data-addon="{{ row.data.id }}" class="addon-row"
id="addon-{{ row.data.id }}">
{% for addon in pager.object_list %}
<tr data-addon="{{ addon.id }}" class="addon-row" id="addon-{{ addon.id }}">
<td><div class="addon-locked"></div></td>
{% for value in row %}
<td>{{ value|xssafe }}</td>
{% endfor %}
<td><a href="{{ url('reviewers.apps.review', addon.app_slug)|urlparams(num=loop.index) }}">{{ addon.name|xssafe }}</a></td>
<td>{# Flags #}
{% if addon.admin_review %}
<div class="app-icon ed-sprite-admin-review" title="{{ _('Admin Review') }}"></div>
{% endif %}
{% if addon.current_version.has_info_request %}
<div class="app-icon ed-sprite-info" title="{{ _('More Information Requested') }}"></div>
{% endif %}
{% if addon.current_version.has_editor_comment %}
<div class="app-icon ed-sprite-editor" title="{{ _('Contains Editor Comment') }}"></div>
{% endif %}
</td>
<td>{{ addon.created|timesince }}</td>
<td>{{ device_list(addon) }}</td>
<td>{{ amo.ADDON_PREMIUM_TYPES[addon.premium_type] }}</td>
<td>{{ addon.abuse_reports.count() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if page.paginator.count == 0 %}
{% if pager.paginator.count == 0 %}
<div class="no-results">
{{ _('There are currently no items of this type to review.') }}
</div>
{% else %}
{{ pager|impala_paginator }}
{% endif %}
{{ page|impala_paginator }}
</section>
<p id="helpfulLinks">

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

@ -5,18 +5,19 @@ import time
from django.core import mail
from django.conf import settings
from elasticutils import S
import mock
from nose.tools import eq_, ok_
from pyquery import PyQuery as pq
import amo
from abuse.models import AbuseReport
from access.models import Group, GroupUser
from addons.models import AddonUser
from addons.models import AddonDeviceType, AddonUser, DeviceType
from amo.tests import app_factory, check_links
from amo.urlresolvers import reverse
from amo.utils import urlparams
from devhub.models import AppLog
from editors.forms import MOTDForm
from editors.models import CannedResponse
from editors.tests.test_views import EditorTest
from users.models import UserProfile
@ -30,12 +31,12 @@ class AppReviewerTest(object):
def setUp(self):
self.login_as_editor()
def test_403_for_non_editor(self):
def test_403_for_non_editor(self, *args, **kwargs):
assert self.client.login(username='regular@mozilla.com',
password='password')
eq_(self.client.head(self.url).status_code, 403)
def test_403_for_anonymous(self):
def test_403_for_anonymous(self, *args, **kwargs):
self.client.logout()
eq_(self.client.head(self.url).status_code, 403)
@ -99,20 +100,34 @@ class TestReviewersHome(EditorTest):
class TestAppQueue(AppReviewerTest, EditorTest):
fixtures = ['base/devicetypes', 'base/users']
def setUp(self):
self.login_as_editor()
now = datetime.datetime.now()
days_ago = lambda n: now - datetime.timedelta(days=n)
self.apps = [app_factory(name='XXX',
status=amo.WEBAPPS_UNREVIEWED_STATUS),
app_factory(name='YYY',
status=amo.WEBAPPS_UNREVIEWED_STATUS)]
self.apps[0].update(created=days_ago(2))
self.apps[1].update(created=days_ago(1))
self.login_as_editor()
self.url = reverse('reviewers.apps.queue_pending')
# Mock S(...).values() so we don't touch ES.
patcher = mock.patch.object(S, 'values')
self.s_mock = patcher.start()
self.s_mock.return_value = [a.id for a in self.apps]
self.addCleanup(patcher.stop)
def review_url(self, app, num):
return urlparams(reverse('reviewers.apps.review', args=[app.app_slug]),
num=num)
def test_restricted_results(self):
def test_template_links(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
links = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(2) a')
@ -123,21 +138,72 @@ class TestAppQueue(AppReviewerTest, EditorTest):
]
check_links(expected, links, verify=False)
def test_flag_super_review(self):
self.apps[0].update(admin_review=True)
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(3)')
flags = tds('div.ed-sprite-admin-review')
eq_(flags.length, 1)
def test_flag_info(self):
self.apps[0].current_version.update(has_info_request=True)
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(3)')
flags = tds('div.ed-sprite-info')
eq_(flags.length, 1)
def test_flag_comment(self):
self.apps[0].current_version.update(has_editor_comment=True)
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(3)')
flags = tds('div.ed-sprite-editor')
eq_(flags.length, 1)
def test_devices(self):
AddonDeviceType.objects.create(
addon=self.apps[0], device_type=DeviceType.objects.get(pk=1))
AddonDeviceType.objects.create(
addon=self.apps[0], device_type=DeviceType.objects.get(pk=2))
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(5)')
eq_(tds('ul li:not(.unavailable)').length, 2)
def test_payments(self):
self.apps[0].update(premium_type=amo.ADDON_PREMIUM)
self.apps[1].update(premium_type=amo.ADDON_FREE_INAPP)
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(6)')
eq_(tds.eq(0).text(), amo.ADDON_PREMIUM_TYPES[amo.ADDON_PREMIUM])
eq_(tds.eq(1).text(), amo.ADDON_PREMIUM_TYPES[amo.ADDON_FREE_INAPP])
def test_abuse(self):
AbuseReport.objects.create(addon=self.apps[0], message='!@#$')
r = self.client.get(self.url)
eq_(r.status_code, 200)
tds = pq(r.content)('#addon-queue tbody')('tr td:nth-of-type(7)')
eq_(tds.eq(0).text(), '1')
def test_invalid_page(self):
r = self.client.get(self.url, {'page': 999})
eq_(r.status_code, 200)
eq_(r.context['page'].number, 1)
eq_(r.context['pager'].number, 1)
def test_queue_count(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
eq_(pq(r.content)('.tabnav li a:eq(0)').text(), u'Apps (2)')
def test_sort(self):
r = self.client.get(self.url, {'sort': '-name'})
eq_(r.status_code, 200)
eq_(pq(r.content)('#addon-queue tbody tr').eq(0).attr('data-addon'),
str(self.apps[1].id))
# TODO(robhudson): Add sorting back in.
#def test_sort(self):
# r = self.client.get(self.url, {'sort': '-name'})
# eq_(r.status_code, 200)
# eq_(pq(r.content)('#addon-queue tbody tr').eq(0).attr('data-addon'),
# str(self.apps[1].id))
def test_redirect_to_review(self):
r = self.client.get(self.url, {'num': 2})
@ -159,7 +225,9 @@ class TestReviewApp(AppReviewerTest, EditorTest):
def post(self, data):
r = self.client.post(self.url, data)
self.assertRedirects(r, reverse('reviewers.apps.queue_pending'))
# Purposefully not using assertRedirects to avoid having to mock ES.
eq_(r.status_code, 302)
ok_(reverse('reviewers.apps.queue_pending') in r['Location'])
@mock.patch.object(settings, 'DEBUG', False)
def test_cannot_review_my_app(self):

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

@ -4,17 +4,12 @@ from django.conf import settings
from django.utils.datastructures import SortedDict
import commonware.log
import django_tables as tables
import jinja2
from tower import ugettext as _, ugettext_lazy as _lazy
from tower import ugettext_lazy as _lazy
import amo
from amo.helpers import absolutify, timesince
from amo.helpers import absolutify
from amo.urlresolvers import reverse
from amo.utils import send_mail_jinja
from editors.helpers import ItemStateTable
from mkt.webapps.models import Webapp
log = commonware.log.getLogger('z.mailer')
@ -29,52 +24,6 @@ def send_mail(subject, template, context, emails, perm_setting=None):
headers={'Reply-To': settings.MKT_REVIEWERS_EMAIL})
class WebappQueueTable(tables.ModelTable, ItemStateTable):
name = tables.Column(verbose_name=_lazy(u'App'))
flags = tables.Column(verbose_name=_lazy(u'Flags'), sortable=False)
created = tables.Column(verbose_name=_lazy(u'Waiting Time'))
abuse_reports__count = tables.Column(verbose_name=_lazy(u'Abuse Reports'))
def render_name(self, row):
url = '%s?num=%s' % (self.review_url(row), self.item_number)
self.increment_item()
return u'<a href="%s">%s</a>' % (url, jinja2.escape(row.name))
def render_flags(self, row):
o = []
if row.admin_review:
o.append(u'<div class="app-icon ed-sprite-admin-review" '
u'title="%s"></div>' % _('Admin Review'))
return ''.join(o)
def render_created(self, row):
return timesince(row.created)
def render_abuse_reports__count(self, row):
url = reverse('editors.abuse_reports', args=[row.slug])
return u'<a href="%s">%s</a>' % (jinja2.escape(url),
row.abuse_reports__count)
@classmethod
def translate_sort_cols(cls, colname):
return colname
@classmethod
def default_order_by(cls):
return 'created'
@classmethod
def review_url(cls, row):
return reverse('reviewers.apps.review', args=[row.app_slug])
class Meta:
sortable = True
model = Webapp
columns = ['name', 'flags', 'created', 'abuse_reports__count']
class ReviewBase:
def __init__(self, request, addon, version, review_type):
@ -227,7 +176,7 @@ class ReviewApp(ReviewBase):
self.log_action(amo.LOG.COMMENT_VERSION)
class ReviewHelper:
class ReviewHelper(object):
"""
A class that builds enough to render the form back to the user and
process off to the correct handler.

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

@ -2,20 +2,23 @@ import datetime
from django import http
from django.conf import settings
from django.db.models import Count, Q
from django.db.models import Q
from django.shortcuts import redirect
from elasticutils import S
import jingo
from tower import ugettext as _
from access import acl
import amo
from amo import messages
from amo.utils import urlparams
from addons.decorators import addon_view
from addons.models import Version
from amo.decorators import permission_required
from amo.urlresolvers import reverse
from amo.utils import paginate
from apps.search.views import name_query
from editors.forms import MOTDForm
from editors.models import EditorSubscription
from editors.views import reviewer_required
@ -24,10 +27,14 @@ from zadmin.models import get_config, set_config
from mkt.developers.models import ActivityLog
from mkt.webapps.models import Webapp
from . import forms, utils
from . import forms
from .models import AppCannedResponse
QUEUE_PER_PAGE = 100
@reviewer_required
def home(request):
durations = (('new', _('New Apps (Under 5 days)')),
@ -89,42 +96,6 @@ def _progress():
return (progress, percentage)
def _queue(request, TableObj, tab, qs=None):
if qs is None:
qs = TableObj.Meta.model.objects.all()
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' % (
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, 'reviewers/queue.html',
context(table=table, page=page, tab=tab))
def context(**kw):
ctx = dict(motd=get_config('mkt_reviewers_motd'),
queue_counts=queue_counts())
@ -223,8 +194,37 @@ 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, 'pending', qs=qs)
sqs = S(Webapp).filter(type=amo.ADDON_WEBAPP, status=amo.STATUS_PENDING,
is_disabled=False)
q = request.GET.get('q', None)
if q:
sqs = sqs.query(or_=name_query(q))
ids = sqs.values()
qs = Webapp.objects.filter(id__in=ids).order_by('created')
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(
urlparams(reverse('reviewers.apps.review',
args=[row.app_slug]),
num=review_num))
except IndexError:
pass
per_page = request.GET.get('per_page', QUEUE_PER_PAGE)
pager = paginate(request, qs, per_page)
return jingo.render(request, 'reviewers/queue.html', {'pager': pager})
@permission_required('Apps', 'Review')