Added moderated review queue to mkt (bug 767478)
This commit is contained in:
Родитель
68239be049
Коммит
96c2b4a075
|
@ -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 */
|
||||
|
|
|
@ -68,6 +68,7 @@ CSS = {
|
|||
),
|
||||
'mkt/reviewers': (
|
||||
'css/mkt/buttons.less',
|
||||
'css/mkt/ratings.less',
|
||||
'css/mkt/reviewers.less',
|
||||
),
|
||||
'mkt/consumer': (
|
||||
|
|
|
@ -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'])),
|
||||
]
|
||||
|
|
|
@ -20,6 +20,62 @@
|
|||
</ul>
|
||||
|
||||
<section class="island">
|
||||
{% if tab == 'moderated' %}
|
||||
<div id="reviews-flagged">
|
||||
<form method="post" class="item">
|
||||
{% if queue_counts[tab] != 0 %}
|
||||
<div class="review-saved">
|
||||
<button type="submit">{{ _('Process Reviews') }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ csrf() }}
|
||||
{{ reviews_formset.management_form }}
|
||||
{% for review in reviews_formset.forms %}
|
||||
<div class="review-flagged">
|
||||
<div class="review-flagged-actions">
|
||||
{{ review.errors }}
|
||||
<strong>{{ _('Moderation actions:') }}</strong>
|
||||
{{ review.id }}
|
||||
{{ review.action }}
|
||||
</div>
|
||||
<h3>
|
||||
<a href="{{ review.instance.addon.get_url_path() }}">{{ review.instance.addon.name }}</a>
|
||||
{%- if review.instance.title %}: {{ review.instance.title }}{% endif %}
|
||||
</h3>
|
||||
<p>
|
||||
{% trans user=review.instance.user|user_link, date=review.instance.created|datetime,
|
||||
stars=review.instance.rating|stars, locale=review.instance.title.locale %}
|
||||
by {{ user }} on {{ date }}
|
||||
{{ stars }} ({{ locale }})
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p class="description">{{ review.instance.body|nl2br }}</p>
|
||||
<ul class="reviews-flagged-reasons">
|
||||
{% for reason in review.instance.reviewflag_set.all() %}
|
||||
<li>
|
||||
<div>
|
||||
{% trans user=reason.user|user_link, date=reason.modified|babel_datetime,
|
||||
reason=flags[reason.flag] %}
|
||||
<strong>{{ reason }}</strong>
|
||||
<span class="light">Flagged by {{ user }} on {{ date }}</span>
|
||||
{% endtrans %}
|
||||
</div>
|
||||
{{ reason.note }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if queue_counts[tab] == 0 %}
|
||||
<div class="no-results">{{ _('All reviews have been moderated. Good work!') }}</div>
|
||||
{% else %}
|
||||
<div class="review-saved review-flagged">
|
||||
<button type="submit">{{ _('Process Reviews') }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<table id="addon-queue" class="data-grid items"
|
||||
data-url="{{ url('editors.queue_viewing') }}">
|
||||
<thead>
|
||||
|
@ -58,7 +114,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if pager.paginator.count == 0 %}
|
||||
{% if queue_counts[tab] == 0 %}
|
||||
<div class="no-results">
|
||||
{{ _('There are currently no items of this type to review.') }}
|
||||
</div>
|
||||
|
@ -66,6 +122,8 @@
|
|||
{{ pager|impala_paginator }}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
<p id="helpfulLinks">
|
||||
|
|
|
@ -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)')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче