Added moderated review queue to mkt (bug 767478)

This commit is contained in:
Rob Hudson 2012-07-24 15:04:44 -07:00
Родитель 68239be049
Коммит 96c2b4a075
7 изменённых файлов: 271 добавлений и 90 удалений

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

@ -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()