diff --git a/media/css/mkt/reviewers.less b/media/css/mkt/reviewers.less
index 2c758c9cee..3e039ca952 100644
--- a/media/css/mkt/reviewers.less
+++ b/media/css/mkt/reviewers.less
@@ -571,73 +571,74 @@ div.editor-stats-table > div.editor-stats-dark {
/* Moderated reviews css */
-#reviews-flagged .review-flagged {
- border-top: 1px dotted @medium-gray;
- padding: 1em;
- margin: 0 0 1em 1em;
+#reviews-flagged {
+ .review-flagged {
+ border-top: 1px dotted @medium-gray;
+ line-height: 20px;
+ margin: 0 0 1em 1em;
+ padding: 1em;
+ ul {
+ margin-bottom: 0;
+ }
+ }
+ &:first-child {
+ border-top: 0;
+ }
+ .reviews-flagged-reasons {
+ background-color: lighten(@red, 50%);
+ .border-radius(10px);
+ clear: both;
+ margin: 0;
+ padding: 0 10px;
+ li {
+ border-top: 1px dotted @light-gray;
+ padding: 10px 0;
+ &:first-child {
+ border-top: 0;
+ }
+ }
+ }
+ h3 {
+ margin: 0;
+ }
+ p {
+ margin-bottom: 1.5em;
+ }
+ .review-flagged-actions {
+ border-left: 1px dashed @medium-gray;
+ float: right;
+ margin-left: 10px;
+ padding-left: 10px;
+ width: 200px;
+ label {
+ font-weight: normal;
+ }
+ }
+ form {
+ margin: 0;
+ }
+ div.review-saved {
+ margin-bottom: 0;
+ padding: 10px;
+ text-align: right;
+ }
+ label[for$=action_0] {
+ color: @green;
+ }
+ label[for$=action_2] {
+ color: @maroon;
+ }
+ .stars {
+ float: left;
+ }
+ span.light {
+ color: @note-gray;
+ }
}
-
-#reviews-flagged .review-flagged ul {
- margin-bottom: 0;
-}
-
-#reviews-flagged .review-flagged:first-child {
- border-top: 0;
-}
-
-#reviews-flagged .reviews-flagged-reasons {
- background-color: #FFE4E4;
- .border-radius(10px);
- margin: 0;
- padding: 0 10px;
- clear: both;
-}
-
-#reviews-flagged .reviews-flagged-reasons li {
- padding: 10px 0;
- border-top: 1px dotted #FFB1B1;
-}
-
-#reviews-flagged .reviews-flagged-reasons li:first-child {
- border-top: 0;
-}
-
-#reviews-flagged h3 {
- margin: 0;
-}
-
-#reviews-flagged .review-flagged-actions {
- float: right;
- padding-left: 10px;
- margin-left: 10px;
- border-left: 1px dashed @medium-gray;
- width: 200px;
-}
-
-#reviews-flagged .review-flagged-actions label {
- font-weight: normal;
-}
-
-#reviews-flagged form {
- margin: 0;
-}
-
-#reviews-flagged div.review-saved {
- padding: 10px;
- text-align: right;
- margin-bottom: 0;
-}
-
-#reviews-flagged label[for$=action_0] {
- color: #429341;
-}
-
-#reviews-flagged label[for$=action_1] {
- color: #4D80C6;
-}
-
-#reviews-flagged label[for$=action_2] {
- color: #9A2E2E;
+.html-rtl #reviews-flagged {
+ .stars {
+ float: right;
+ }
}
/* data grid search form */
diff --git a/mkt/asset_bundles.py b/mkt/asset_bundles.py
index bf57bad572..30f739ec4a 100644
--- a/mkt/asset_bundles.py
+++ b/mkt/asset_bundles.py
@@ -68,6 +68,7 @@ CSS = {
),
'mkt/reviewers': (
'css/mkt/buttons.less',
+ 'css/mkt/ratings.less',
'css/mkt/reviewers.less',
),
'mkt/consumer': (
diff --git a/mkt/reviewers/helpers.py b/mkt/reviewers/helpers.py
index 17c289cbd7..fd8aa7ca4b 100644
--- a/mkt/reviewers/helpers.py
+++ b/mkt/reviewers/helpers.py
@@ -26,7 +26,8 @@ def reviewers_breadcrumbs(context, queue=None, items=None):
if queue:
queues = {'pending': _('Apps'),
'rereview': _('Re-reviews'),
- 'escalated': _('Escalations')}
+ 'escalated': _('Escalations'),
+ 'moderated': _('Moderated Reviews')}
if items:
url = reverse('reviewers.apps.queue_%s' % queue)
@@ -67,4 +68,7 @@ def queue_tabnav(context):
('escalated', 'queue_escalated',
_('Escalations ({0})',
counts['escalated']).format(counts['escalated'])),
+ ('moderated', 'queue_moderated',
+ _('Moderated Reviews ({0})',
+ counts['moderated']).format(counts['moderated'])),
]
diff --git a/mkt/reviewers/templates/reviewers/queue.html b/mkt/reviewers/templates/reviewers/queue.html
index ee6ea3089c..04d55043d3 100644
--- a/mkt/reviewers/templates/reviewers/queue.html
+++ b/mkt/reviewers/templates/reviewers/queue.html
@@ -20,6 +20,62 @@
@@ -58,7 +114,7 @@
- {% if pager.paginator.count == 0 %}
+ {% if queue_counts[tab] == 0 %}
diff --git a/mkt/reviewers/tests/test_views.py b/mkt/reviewers/tests/test_views.py index f53ef58630..4c057e1bf3 100644 --- a/mkt/reviewers/tests/test_views.py +++ b/mkt/reviewers/tests/test_views.py @@ -13,19 +13,20 @@ from nose.tools import eq_, ok_ from pyquery import PyQuery as pq import amo +import reviews from abuse.models import AbuseReport from access.models import Group, GroupUser from addons.models import AddonDeviceType, AddonUser, DeviceType -from amo.tests import app_factory, check_links +from amo.tests import app_factory, check_links, formset, initial from amo.urlresolvers import reverse from amo.utils import urlparams -from devhub.models import AppLog +from devhub.models import ActivityLog, AppLog from editors.models import CannedResponse, ReviewerScore -from users.models import UserProfile -from zadmin.models import get_config, set_config - from mkt.reviewers.models import EscalationQueue, RereviewQueue from mkt.webapps.models import Webapp +from reviews.models import Review, ReviewFlag +from users.models import UserProfile +from zadmin.models import get_config, set_config class AppReviewerTest(amo.tests.TestCase): @@ -243,6 +244,7 @@ class TestAppQueue(AppReviewerTest, AccessMixin): eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (2)') eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (1)') eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (0)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (0)') def test_escalated_not_in_queue(self): EscalationQueue.objects.create(addon=self.apps[0]) @@ -254,6 +256,7 @@ class TestAppQueue(AppReviewerTest, AccessMixin): eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (1)') eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (1)') eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (1)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (0)') # TODO(robhudson): Add sorting back in. #def test_sort(self): @@ -340,6 +343,7 @@ class TestRereviewQueue(AppReviewerTest, AccessMixin): eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (0)') eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (3)') eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (0)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (0)') def test_escalated_not_in_queue(self): EscalationQueue.objects.create(addon=self.apps[0]) @@ -350,6 +354,7 @@ class TestRereviewQueue(AppReviewerTest, AccessMixin): eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (0)') eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (2)') eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (1)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (0)') class TestEscalationQueue(AppReviewerTest, AccessMixin): @@ -443,6 +448,7 @@ class TestEscalationQueue(AppReviewerTest, AccessMixin): eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (0)') eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (0)') eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (3)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (0)') class TestReviewApp(AppReviewerTest, AccessMixin): @@ -1055,3 +1061,84 @@ class TestAbuseReports(amo.tests.TestCase): reports = r.context['reports'] eq_(len(reports), 2) eq_(sorted([r.message for r in reports]), [u'eff', u'yeah']) + + +class TestModeratedQueue(amo.tests.TestCase, AccessMixin): + fixtures = ['base/users'] + + def setUp(self): + self.app = app_factory() + + self.reviewer = UserProfile.objects.get(email='editor@mozilla.com') + self.users = list(UserProfile.objects.exclude(pk=self.reviewer.id)) + + self.url = reverse('reviewers.apps.queue_moderated') + + self.review1 = Review.objects.create(addon=self.app, body='body', + user=self.users[0], rating=3, + editorreview=True) + ReviewFlag.objects.create(review=self.review1, flag=ReviewFlag.SPAM, + user=self.users[0]) + self.review2 = Review.objects.create(addon=self.app, body='body', + user=self.users[1], rating=4, + editorreview=True) + ReviewFlag.objects.create(review=self.review2, flag=ReviewFlag.SUPPORT, + user=self.users[1]) + + self.client.login(username=self.reviewer.email, password='password') + + def _post(self, action): + ctx = self.client.get(self.url).context + data_formset = formset(initial(ctx['reviews_formset'].forms[0])) + data_formset['form-0-action'] = action + + res = self.client.post(self.url, data_formset) + self.assert3xx(res, self.url) + + def _get_logs(self, action): + return ActivityLog.objects.filter(action=action.id) + + def test_setup(self): + eq_(Review.objects.filter(editorreview=True).count(), 2) + eq_(ReviewFlag.objects.filter(flag=ReviewFlag.SPAM).count(), 1) + + res = self.client.get(self.url) + doc = pq(res.content)('#reviews-flagged') + + # Test the default action is "skip". + eq_(doc('#id_form-0-action_1:checked').length, 1) + + def test_skip(self): + # Skip the first review, which still leaves two. + self._post(reviews.REVIEW_MODERATE_SKIP) + res = self.client.get(self.url) + eq_(len(res.context['page'].object_list), 2) + + def test_delete(self): + # Delete the first review, which leaves one. + self._post(reviews.REVIEW_MODERATE_DELETE) + res = self.client.get(self.url) + eq_(len(res.context['page'].object_list), 1) + eq_(self._get_logs(amo.LOG.DELETE_REVIEW).count(), 1) + + def test_keep(self): + # Keep the first review, which leaves one. + self._post(reviews.REVIEW_MODERATE_KEEP) + res = self.client.get(self.url) + eq_(len(res.context['page'].object_list), 1) + eq_(self._get_logs(amo.LOG.APPROVE_REVIEW).count(), 1) + + def test_no_reviews(self): + Review.objects.all().delete() + res = self.client.get(self.url) + eq_(res.status_code, 200) + eq_(pq(res.content)('#reviews-flagged .no-results').length, 1) + + def test_queue_count(self): + r = self.client.get(self.url) + eq_(r.status_code, 200) + doc = pq(r.content) + eq_(doc('.tabnav li a:eq(0)').text(), u'Apps (0)') + eq_(doc('.tabnav li a:eq(1)').text(), u'Re-reviews (0)') + eq_(doc('.tabnav li a:eq(2)').text(), u'Escalations (0)') + eq_(doc('.tabnav li a:eq(3)').text(), u'Moderated Reviews (2)') diff --git a/mkt/reviewers/urls.py b/mkt/reviewers/urls.py index 9854cc6d7a..fcf3551319 100644 --- a/mkt/reviewers/urls.py +++ b/mkt/reviewers/urls.py @@ -15,6 +15,8 @@ urlpatterns = ( name='reviewers.apps.queue_rereview'), url(r'^apps/queue/escalated/$', views.queue_escalated, name='reviewers.apps.queue_escalated'), + url(r'^apps/queue/reviews$', views.queue_moderated, + name='reviewers.apps.queue_moderated'), url(r'^apps/review/%s$' % amo.APP_SLUG, views.app_review, name='reviewers.apps.review'), url(r'^apps/review/%s/manifest$' % amo.APP_SLUG, views.app_view_manifest, diff --git a/mkt/reviewers/views.py b/mkt/reviewers/views.py index 04ad8d1e45..3478ebc643 100644 --- a/mkt/reviewers/views.py +++ b/mkt/reviewers/views.py @@ -24,7 +24,8 @@ from editors.models import EditorSubscription from editors.views import reviewer_required from mkt.developers.models import ActivityLog from mkt.webapps.models import Webapp -from reviews.models import Review +from reviews.forms import ReviewFlagFormSet +from reviews.models import Review, ReviewFlag from zadmin.models import get_config, set_config from . import forms from .models import AppCannedResponse, EscalationQueue, RereviewQueue @@ -54,20 +55,24 @@ def home(request): def queue_counts(type=None, **kw): - excluded_ids = EscalationQueue.objects.values_list('addon', flat=True) + excluded_ids = EscalationQueue.uncached.values_list('addon', flat=True) counts = { - 'pending': Webapp.objects.pending() - .exclude(id__in=excluded_ids) - .filter(disabled_by_user=False) - .count(), - 'rereview': RereviewQueue.objects + 'pending': Webapp.uncached.exclude(id__in=excluded_ids) + .filter(status=amo.WEBAPPS_UNREVIEWED_STATUS, + disabled_by_user=False) + .count(), + 'rereview': RereviewQueue.uncached .exclude(addon__in=excluded_ids) .filter(addon__disabled_by_user=False) .count(), - 'escalated': EscalationQueue.objects + 'escalated': EscalationQueue.uncached .filter(addon__disabled_by_user=False) .count(), + 'moderated': Review.uncached.filter(reviewflag__isnull=False, + editorreview=True, + addon__type=amo.ADDON_WEBAPP) + .count(), } rv = {} if isinstance(type, basestring): @@ -86,7 +91,7 @@ def _progress(): """ days_ago = lambda n: datetime.datetime.now() - datetime.timedelta(days=n) - qs = Webapp.objects.pending() + qs = Webapp.uncached.filter(status=amo.WEBAPPS_UNREVIEWED_STATUS) progress = { 'new': qs.filter(created__gt=days_ago(5)).count(), 'med': qs.filter(created__range=(days_ago(10), days_ago(5))).count(), @@ -205,18 +210,18 @@ def _queue(request, qs, tab, pager_processor=None): @permission_required('Apps', 'Review') def queue_apps(request): - excluded_ids = EscalationQueue.objects.values_list('addon', flat=True) - qs = (Webapp.objects.pending() - .exclude(id__in=excluded_ids) - .filter(disabled_by_user=False) - .order_by('created')) + excluded_ids = EscalationQueue.uncached.values_list('addon', flat=True) + qs = (Webapp.uncached.filter(status=amo.WEBAPPS_UNREVIEWED_STATUS) + .exclude(id__in=excluded_ids) + .filter(disabled_by_user=False) + .order_by('created')) return _queue(request, qs, 'pending') @permission_required('Apps', 'Review') def queue_rereview(request): - excluded_ids = EscalationQueue.objects.values_list('addon', flat=True) - qs = (RereviewQueue.objects + excluded_ids = EscalationQueue.uncached.values_list('addon', flat=True) + qs = (RereviewQueue.uncached .exclude(addon__in=excluded_ids) .filter(addon__disabled_by_user=False) .order_by('created')) @@ -226,12 +231,35 @@ def queue_rereview(request): @permission_required('Apps', 'Review') def queue_escalated(request): - qs = (EscalationQueue.objects.filter(addon__disabled_by_user=False) + qs = (EscalationQueue.uncached.filter(addon__disabled_by_user=False) .order_by('created')) return _queue(request, qs, 'escalated', lambda p: [r.addon for r in p.object_list]) +@permission_required('Apps', 'Review') +def queue_moderated(request): + rf = (Review.uncached.exclude( + Q(addon__isnull=True) | + Q(reviewflag__isnull=True)) + .filter(addon__type=amo.ADDON_WEBAPP, + editorreview=True) + .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('reviewers.apps.queue_moderated')) + + return jingo.render(request, 'reviewers/queue.html', + context(reviews_formset=reviews_formset, + tab='moderated', page=page, flags=flags)) + + @permission_required('Apps', 'Review') def logs(request): data = request.GET.copy()