add basic page to proceed/escalate held actions (#22822)
* add basic page to proceed/escalate held actions * drop match default case * held_action_review -> decision_review
This commit is contained in:
Родитель
9b435466ef
Коммит
b3e55e775a
|
@ -114,7 +114,7 @@ class ContentAction:
|
|||
'reference_id': reference_id,
|
||||
'target': self.target,
|
||||
'target_url': target_url,
|
||||
'type': self.decision.get_target_type(),
|
||||
'type': self.decision.get_target_display(),
|
||||
'SITE_URL': settings.SITE_URL,
|
||||
**(extra_context or {}),
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ class ContentAction:
|
|||
'policy_document_url': POLICY_DOCUMENT_URL,
|
||||
'reference_id': reference_id,
|
||||
'target_url': absolutify(self.target.get_url_path()),
|
||||
'type': self.decision.get_target_type(),
|
||||
'type': self.decision.get_target_display(),
|
||||
'SITE_URL': settings.SITE_URL,
|
||||
}
|
||||
if is_appeal:
|
||||
|
|
|
@ -319,7 +319,7 @@ class CinderJob(ModelBase):
|
|||
uuid__in=policy_ids
|
||||
).without_parents_if_their_children_are_present()
|
||||
cinder_decision.policies.add(*policies)
|
||||
cinder_decision.process_action(overridden_action)
|
||||
cinder_decision.process_action(overridden_action=overridden_action)
|
||||
|
||||
def process_queue_move(self, *, new_queue, notes):
|
||||
CinderQueueMove.objects.create(cinder_job=self, notes=notes, to_queue=new_queue)
|
||||
|
@ -1026,6 +1026,14 @@ class ContentDecision(ModelBase):
|
|||
else:
|
||||
return self.collection
|
||||
|
||||
def get_target_display(self):
|
||||
if self.addon_id:
|
||||
return self.addon.get_type_display()
|
||||
elif self.user_id:
|
||||
return _('User profile')
|
||||
else:
|
||||
return self.target.__class__.__name__
|
||||
|
||||
@property
|
||||
def is_third_party_initiated(self):
|
||||
return hasattr(self, 'cinder_job') and bool(self.cinder_job.all_abuse_reports)
|
||||
|
@ -1279,10 +1287,11 @@ class ContentDecision(ModelBase):
|
|||
},
|
||||
)
|
||||
|
||||
def process_action(self, overridden_action=None):
|
||||
def process_action(self, *, overridden_action=None, release_hold=False):
|
||||
"""currently only called by decisions from cinder.
|
||||
see https://mozilla-hub.atlassian.net/browse/AMOENG-1125
|
||||
"""
|
||||
assert not self.action_date # we should not be attempting to process twice
|
||||
appealed_action = (
|
||||
getattr(self.cinder_job.appealed_decisions.first(), 'action', None)
|
||||
if hasattr(self, 'cinder_job')
|
||||
|
@ -1293,7 +1302,7 @@ class ContentDecision(ModelBase):
|
|||
overridden_action=overridden_action,
|
||||
appealed_action=appealed_action,
|
||||
)
|
||||
if not action_helper.should_hold_action():
|
||||
if release_hold or not action_helper.should_hold_action():
|
||||
self.action_date = datetime.now()
|
||||
log_entry = action_helper.process_action()
|
||||
if cinder_job := getattr(self, 'cinder_job', None):
|
||||
|
@ -1304,29 +1313,12 @@ class ContentDecision(ModelBase):
|
|||
action_helper.hold_action()
|
||||
|
||||
def get_target_review_url(self):
|
||||
return (
|
||||
reverse('reviewers.review', args=(self.target.id,))
|
||||
if isinstance(self.target, Addon)
|
||||
else ''
|
||||
)
|
||||
|
||||
def get_target_type(self):
|
||||
match self.target:
|
||||
case target if isinstance(target, Addon):
|
||||
return target.get_type_display()
|
||||
case target if isinstance(target, UserProfile):
|
||||
return _('User profile')
|
||||
case target if isinstance(target, Collection):
|
||||
return _('Collection')
|
||||
case target if isinstance(target, Rating):
|
||||
return _('Rating')
|
||||
case target:
|
||||
return target.__class__.__name__
|
||||
return reverse('reviewers.decision_review', kwargs={'decision_id': self.id})
|
||||
|
||||
def get_target_name(self):
|
||||
return str(
|
||||
_('"{}" for {}').format(self.target, self.target.addon.name)
|
||||
if isinstance(self.target, Rating)
|
||||
_('"{}" for {}').format(self.rating, self.rating.addon.name)
|
||||
if self.rating
|
||||
else getattr(self.target, 'name', self.target)
|
||||
)
|
||||
|
||||
|
|
|
@ -3132,6 +3132,14 @@ class TestContentDecision(TestCase):
|
|||
not in mail.outbox[0].body
|
||||
)
|
||||
|
||||
def _test_process_action_ban_user_outcome(self, decision):
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
self.assertCloseToNow(decision.user.reload().banned)
|
||||
assert (
|
||||
ActivityLog.objects.filter(action=amo.LOG.ADMIN_USER_BANNED.id).count() == 1
|
||||
)
|
||||
assert 'appeal' in mail.outbox[0].body
|
||||
|
||||
def test_process_action_ban_user_held(self):
|
||||
user = user_factory(email='superstarops@mozilla.com')
|
||||
decision = ContentDecision.objects.create(
|
||||
|
@ -3149,6 +3157,9 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
decision.process_action(release_hold=True)
|
||||
self._test_process_action_ban_user_outcome(decision)
|
||||
|
||||
def test_process_action_ban_user(self):
|
||||
user = user_factory()
|
||||
decision = ContentDecision.objects.create(
|
||||
|
@ -3156,11 +3167,12 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert decision.action_date is None
|
||||
decision.process_action()
|
||||
self._test_process_action_ban_user_outcome(decision)
|
||||
|
||||
def _test_process_action_disable_addon_outcome(self, decision):
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
self.assertCloseToNow(user.reload().banned)
|
||||
assert (
|
||||
ActivityLog.objects.filter(action=amo.LOG.ADMIN_USER_BANNED.id).count() == 1
|
||||
)
|
||||
assert decision.addon.reload().status == amo.STATUS_DISABLED
|
||||
assert ActivityLog.objects.filter(action=amo.LOG.FORCE_DISABLE.id).count() == 1
|
||||
assert 'appeal' in mail.outbox[0].body
|
||||
|
||||
def test_process_action_disable_addon_held(self):
|
||||
|
@ -3181,6 +3193,9 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
decision.process_action(release_hold=True)
|
||||
self._test_process_action_disable_addon_outcome(decision)
|
||||
|
||||
def test_process_action_disable_addon(self):
|
||||
addon = addon_factory(users=[user_factory()])
|
||||
decision = ContentDecision.objects.create(
|
||||
|
@ -3188,9 +3203,15 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert decision.action_date is None
|
||||
decision.process_action()
|
||||
self._test_process_action_disable_addon_outcome(decision)
|
||||
|
||||
def _test_process_action_delete_collection_outcome(self, decision):
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
assert addon.reload().status == amo.STATUS_DISABLED
|
||||
assert ActivityLog.objects.filter(action=amo.LOG.FORCE_DISABLE.id).count() == 1
|
||||
assert decision.collection.reload().deleted
|
||||
assert (
|
||||
ActivityLog.objects.filter(action=amo.LOG.COLLECTION_DELETED.id).count()
|
||||
== 1
|
||||
)
|
||||
assert 'appeal' in mail.outbox[0].body
|
||||
|
||||
def test_process_action_delete_collection_held(self):
|
||||
|
@ -3210,6 +3231,9 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
decision.process_action(release_hold=True)
|
||||
self._test_process_action_delete_collection_outcome(decision)
|
||||
|
||||
def test_process_action_delete_collection(self):
|
||||
collection = collection_factory(author=user_factory())
|
||||
decision = ContentDecision.objects.create(
|
||||
|
@ -3217,12 +3241,12 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert decision.action_date is None
|
||||
decision.process_action()
|
||||
self._test_process_action_delete_collection_outcome(decision)
|
||||
|
||||
def _test_process_action_delete_rating_outcome(self, decision):
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
assert collection.reload().deleted
|
||||
assert (
|
||||
ActivityLog.objects.filter(action=amo.LOG.COLLECTION_DELETED.id).count()
|
||||
== 1
|
||||
)
|
||||
assert decision.rating.reload().deleted
|
||||
assert ActivityLog.objects.filter(action=amo.LOG.DELETE_RATING.id).count() == 1
|
||||
assert 'appeal' in mail.outbox[0].body
|
||||
|
||||
def test_process_action_delete_rating_held(self):
|
||||
|
@ -3254,6 +3278,9 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
decision.process_action(release_hold=True)
|
||||
self._test_process_action_delete_rating_outcome(decision)
|
||||
|
||||
def test_process_action_delete_rating(self):
|
||||
rating = Rating.objects.create(addon=addon_factory(), user=user_factory())
|
||||
decision = ContentDecision.objects.create(
|
||||
|
@ -3261,10 +3288,7 @@ class TestContentDecision(TestCase):
|
|||
)
|
||||
assert decision.action_date is None
|
||||
decision.process_action()
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
assert rating.reload().deleted
|
||||
assert ActivityLog.objects.filter(action=amo.LOG.DELETE_RATING.id).count() == 1
|
||||
assert 'appeal' in mail.outbox[0].body
|
||||
self._test_process_action_delete_rating_outcome(decision)
|
||||
|
||||
def test_get_target_review_url(self):
|
||||
addon = addon_factory()
|
||||
|
@ -3272,29 +3296,26 @@ class TestContentDecision(TestCase):
|
|||
addon=addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON
|
||||
)
|
||||
assert decision.get_target_review_url() == reverse(
|
||||
'reviewers.review', args=(addon.id,)
|
||||
'reviewers.decision_review', args=(decision.id,)
|
||||
)
|
||||
|
||||
decision.update(addon=None, user=user_factory())
|
||||
assert decision.get_target_review_url() == ''
|
||||
|
||||
def test_get_target_type(self):
|
||||
def test_get_target_display(self):
|
||||
decision = ContentDecision.objects.create(
|
||||
addon=addon_factory(), action=DECISION_ACTIONS.AMO_DISABLE_ADDON
|
||||
)
|
||||
assert decision.get_target_type() == 'Extension'
|
||||
assert decision.get_target_display() == 'Extension'
|
||||
|
||||
decision.update(addon=None, user=user_factory())
|
||||
assert decision.get_target_type() == 'User profile'
|
||||
assert decision.get_target_display() == 'User profile'
|
||||
|
||||
decision.update(user=None, collection=collection_factory())
|
||||
assert decision.get_target_type() == 'Collection'
|
||||
assert decision.get_target_display() == 'Collection'
|
||||
|
||||
decision.update(
|
||||
collection=None,
|
||||
rating=Rating.objects.create(addon=addon_factory(), user=user_factory()),
|
||||
)
|
||||
assert decision.get_target_type() == 'Rating'
|
||||
assert decision.get_target_display() == 'Rating'
|
||||
|
||||
def test_get_target_name(self):
|
||||
decision = ContentDecision.objects.create(
|
||||
|
|
|
@ -277,7 +277,6 @@ class CinderJobsWidget(forms.CheckboxSelectMultiple):
|
|||
for text, is_reporter in appeals
|
||||
),
|
||||
]
|
||||
print(subtexts_gen)
|
||||
|
||||
label = format_html(
|
||||
'{}{}{}<details><summary>Show detail on {} reports</summary>'
|
||||
|
@ -658,3 +657,30 @@ class ReviewQueueFilter(forms.Form):
|
|||
self.fields['due_date_reasons'].choices = [
|
||||
(reason, labels.get(reason, reason)) for reason in due_date_reasons
|
||||
]
|
||||
|
||||
|
||||
class HeldDecisionReviewForm(forms.Form):
|
||||
cinder_job = WidgetRenderedModelMultipleChoiceField(
|
||||
label='Resolving Job:',
|
||||
required=False,
|
||||
queryset=CinderJob.objects.none(),
|
||||
widget=CinderJobsWidget(),
|
||||
disabled=True,
|
||||
)
|
||||
choice = forms.ChoiceField(
|
||||
choices=(('yes', 'Proceed with action'), ('no', 'Approve content instead')),
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
jobs_qs = kw.pop('cinder_jobs_qs')
|
||||
super().__init__(*args, **kw)
|
||||
|
||||
if jobs_qs:
|
||||
# Set the queryset for cinder_job
|
||||
self.fields['cinder_job'].queryset = jobs_qs
|
||||
self.fields['cinder_job'].initial = [job.id for job in jobs_qs]
|
||||
if jobs_qs[0].target_addon:
|
||||
self.fields['choice'].choices += (
|
||||
('forward', 'Forward to Reviewer Tools'),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
{% extends "reviewers/base.html" %}
|
||||
|
||||
{% block js %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{{ decision.get_target_display() }}: {{ decision.get_target_name() }} – Add-ons for Firefox
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="primary entity-type-{{ decision.get_target_display() }}"
|
||||
role="main" data-id="{{ decision.target.id }}"
|
||||
>
|
||||
<h2>
|
||||
<span class="app-icon ed-sprite-action-target-{{ decision.get_target_display() }}" title="{{ decision.get_target_display() }}"></span>
|
||||
{{ decision.get_target_display() }} Decision for {{ decision.get_target_name() }}
|
||||
</h2>
|
||||
<table>
|
||||
<tr class="entity-id">
|
||||
<th>Target ID</th>
|
||||
<td>{{ decision.get_target_display() }}: {{ decision.target.id }}</td>
|
||||
</tr>
|
||||
<tr class="entity-name">
|
||||
<th>Target</th>
|
||||
<td><a href="{{ decision.target.get_url_path() }}">{{ decision.get_target_name() }}</a></td>
|
||||
</tr>
|
||||
<tr class="decision-created">
|
||||
<th>Decision</th>
|
||||
<td>
|
||||
<a href="{{ cinder_url }}">{{ decision.cinder_id }}</a>
|
||||
on <time datetime="{{ decision.created|isotime }}">{{
|
||||
decision.created|date }}</time>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="decision-action">
|
||||
<th>Held Decision Action</th>
|
||||
<td>
|
||||
<strong>{{ decision.get_action_display() }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="decision-policies">
|
||||
<th>Policies</th>
|
||||
<td>
|
||||
<ul>
|
||||
{% for policy in decision.policies.all() %}
|
||||
<li>{{ policy }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="decision-notes">
|
||||
<th>Notes from moderator/reviewer</th>
|
||||
<td>
|
||||
{{ decision.notes }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Links</h3>
|
||||
<ul>
|
||||
{% if decision.addon %}
|
||||
<li><a href="{{ url('reviewers.review', 'listed', decision.addon_id) }}">Listed Review page</a></li>
|
||||
<li><a href="{{ url('reviewers.review', 'unlisted', decision.addon_id) }}">Unlisted Review page</a></li>
|
||||
<li><a href="{{ url('admin:addons_addon_change', decision.addon_id) }}">Admin</a></li>
|
||||
{% elif decision.user %}
|
||||
<li><a href="{{ url('admin:users_userprofile_change', decision.user_id) }}">Admin</a></li>
|
||||
{% elif decision.collection %}
|
||||
<li><a href="{{ url('admin:bandwagon_collection_change', decision.collection_id) }}">Admin</a></li>
|
||||
{% elif decision.rating %}
|
||||
<li><a href="{{ url('admin:ratings_rating_change', decision.rating_id) }}">Admin</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<form method="POST" class="review-held-action-form">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input type="submit" value="Process" {{ 'disabled' if decision.action_date else '' }}>
|
||||
</form>
|
||||
</div> {# /#primary #}
|
||||
{% endblock %}
|
|
@ -77,8 +77,8 @@
|
|||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% elif tab == 'held_actions' %}
|
||||
<table id="held-action-queue" class="data-grid">
|
||||
{% elif tab == 'held_decisions' %}
|
||||
<table id="held-decision-queue" class="data-grid">
|
||||
<thead>
|
||||
<tr class="listing-header">
|
||||
<th>Type</th>
|
||||
|
@ -90,7 +90,7 @@
|
|||
<tbody>
|
||||
{% for decision in page.object_list %}
|
||||
<tr id="{{ decision.get_reference_id(short=True) }}" class="held-item">
|
||||
<td><div class="app-icon ed-sprite-action-target-{{ decision.get_target_type() }}" title="{{ decision.get_target_type() }}"></div></td>
|
||||
<td><div class="app-icon ed-sprite-action-target-{{ decision.get_target_display() }}" title="{{ decision.get_target_display() }}"></div></td>
|
||||
<td>{{ decision.get_target_name() }}</td>
|
||||
<td><a href="{{ decision.get_target_review_url() }}">{{ decision.get_action_display() }}</a></td>
|
||||
<td>{{ decision.created|datetime }}</td>
|
||||
|
|
|
@ -26,7 +26,7 @@ def queue_tabnav(context, reviewer_tables_registry):
|
|||
'moderated',
|
||||
'content_review',
|
||||
'pending_rejection',
|
||||
'held_actions',
|
||||
'held_decisions',
|
||||
):
|
||||
if acl.action_allowed_for(
|
||||
request.user, reviewer_tables_registry[queue].permission
|
||||
|
|
|
@ -736,7 +736,7 @@ class TestDashboard(TestCase):
|
|||
'https://wiki.mozilla.org/Add-ons/Reviewers/Guide/Moderation',
|
||||
reverse('reviewers.motd'),
|
||||
reverse('reviewers.queue_pending_rejection'),
|
||||
reverse('reviewers.queue_held_actions'),
|
||||
reverse('reviewers.queue_decisions'),
|
||||
]
|
||||
links = [link.attrib['href'] for link in doc('.dashboard a')]
|
||||
assert links == expected_links
|
||||
|
@ -750,7 +750,9 @@ class TestDashboard(TestCase):
|
|||
assert doc('.dashboard a')[8].text == 'Ratings Awaiting Moderation (1)'
|
||||
# admin tools
|
||||
assert doc('.dashboard a')[12].text == 'Add-ons Pending Rejection (1)'
|
||||
assert doc('.dashboard a')[13].text == 'Held Actions for 2nd Level Approval (1)'
|
||||
assert (
|
||||
doc('.dashboard a')[13].text == 'Held Decisions for 2nd Level Approval (1)'
|
||||
)
|
||||
|
||||
def test_can_see_all_through_reviewer_view_all_permission(self):
|
||||
self.grant_permission(self.user, 'ReviewerTools:View')
|
||||
|
@ -772,7 +774,7 @@ class TestDashboard(TestCase):
|
|||
'https://wiki.mozilla.org/Add-ons/Reviewers/Guide/Moderation',
|
||||
reverse('reviewers.motd'),
|
||||
reverse('reviewers.queue_pending_rejection'),
|
||||
reverse('reviewers.queue_held_actions'),
|
||||
reverse('reviewers.queue_decisions'),
|
||||
]
|
||||
links = [link.attrib['href'] for link in doc('.dashboard a')]
|
||||
assert links == expected_links
|
||||
|
@ -1321,7 +1323,7 @@ class TestQueueBasics(QueueTest):
|
|||
expected.extend(
|
||||
[
|
||||
reverse('reviewers.queue_pending_rejection'),
|
||||
reverse('reviewers.queue_held_actions'),
|
||||
reverse('reviewers.queue_decisions'),
|
||||
]
|
||||
)
|
||||
assert links == expected
|
||||
|
@ -8954,11 +8956,11 @@ class TestReviewVersionRedirect(ReviewerTest):
|
|||
)
|
||||
|
||||
|
||||
class TestHeldActionQueue(ReviewerTest):
|
||||
class TestHeldDecisionQueue(ReviewerTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.url = reverse('reviewers.queue_held_actions')
|
||||
self.url = reverse('reviewers.queue_decisions')
|
||||
|
||||
self.addon_decision = ContentDecision.objects.create(
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon_factory()
|
||||
|
@ -8979,7 +8981,7 @@ class TestHeldActionQueue(ReviewerTest):
|
|||
def test_results(self):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
doc = pq(response.content)('#held-action-queue')
|
||||
doc = pq(response.content)('#held-decision-queue')
|
||||
|
||||
rows = doc('tr.held-item')
|
||||
assert rows.length == 4
|
||||
|
@ -9014,4 +9016,134 @@ class TestHeldActionQueue(ReviewerTest):
|
|||
self.client.force_login(user)
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
assert pq(response.content)('#held-action-queue')
|
||||
assert pq(response.content)('#held-decision-queue')
|
||||
|
||||
|
||||
class TestHeldDecisionReview(ReviewerTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.decision = ContentDecision.objects.create(
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
addon=addon_factory(),
|
||||
cinder_id='1234',
|
||||
)
|
||||
self.decision.policies.add(
|
||||
CinderPolicy.objects.create(uuid='1', name='Bad Things')
|
||||
)
|
||||
CinderPolicy.objects.create(
|
||||
uuid='2', name='Approve', default_cinder_action=DECISION_ACTIONS.AMO_APPROVE
|
||||
)
|
||||
# CinderJob.objects.create(cinder)
|
||||
self.url = reverse('reviewers.decision_review', args=(self.decision.id,))
|
||||
self.login_as_admin()
|
||||
|
||||
def _test_review_page_addon(self):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
doc = pq(response.content)('.entity-type-Extension')
|
||||
|
||||
assert f'Extension Decision for {self.decision.addon.name}' in doc.html()
|
||||
assert f'Extension: {self.decision.addon_id}' in doc.html()
|
||||
assert (
|
||||
doc('tr.decision-created a').attr('href').endswith(self.decision.cinder_id)
|
||||
)
|
||||
assert 'Add-on disable' in doc('tr.decision-action td').html()
|
||||
assert 'Bad Things' in doc('tr.decision-policies td').html()
|
||||
assert 'Proceed with action' == doc('[for="id_choice_0"]').text()
|
||||
assert 'Approve content instead' == doc('[for="id_choice_1"]').text()
|
||||
return doc
|
||||
|
||||
def test_review_page_addon_with_job(self):
|
||||
CinderJob.objects.create(
|
||||
target_addon=self.decision.addon, decision=self.decision
|
||||
)
|
||||
doc = self._test_review_page_addon()
|
||||
assert 'Forward to Reviewer Tools' == doc('[for="id_choice_2"]').text()
|
||||
|
||||
def test_review_page_addon_no_job(self):
|
||||
doc = self._test_review_page_addon()
|
||||
assert 'Forward to Reviewer Tools' not in doc.text()
|
||||
|
||||
def test_review_page_user(self):
|
||||
self.decision.update(
|
||||
addon=None, user=user_factory(), action=DECISION_ACTIONS.AMO_BAN_USER
|
||||
)
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
doc = pq(response.content)('.entity-type-User')
|
||||
|
||||
assert f'User profile Decision for {self.decision.user.name}' in doc.html()
|
||||
assert f'User profile: {self.decision.user_id}' in doc.html()
|
||||
assert (
|
||||
doc('tr.decision-created a').attr('href').endswith(self.decision.cinder_id)
|
||||
)
|
||||
assert 'User ban' in doc('tr.decision-action td').html()
|
||||
assert 'Bad Things' in doc('tr.decision-policies td').html()
|
||||
assert 'Proceed with action' == doc('[for="id_choice_0"]').text()
|
||||
assert 'Approve content instead' == doc('[for="id_choice_1"]').text()
|
||||
assert 'Forward to Reviewer Tools' not in doc.text()
|
||||
|
||||
def test_release_addon_disable_hold(self):
|
||||
addon = self.decision.addon
|
||||
assert addon.status == amo.STATUS_APPROVED
|
||||
|
||||
response = self.client.post(self.url, {'choice': 'yes'})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert addon.reload().status == amo.STATUS_DISABLED
|
||||
self.assertCloseToNow(self.decision.reload().action_date)
|
||||
|
||||
def test_approve_addon_instead(self):
|
||||
addon = self.decision.addon
|
||||
assert addon.status == amo.STATUS_APPROVED
|
||||
|
||||
response = self.client.post(self.url, {'choice': 'no'})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert addon.reload().status == amo.STATUS_APPROVED
|
||||
assert self.decision.reload().action == DECISION_ACTIONS.AMO_APPROVE
|
||||
self.assertCloseToNow(self.decision.action_date)
|
||||
|
||||
def test_escalate_addon_instead(self):
|
||||
addon = self.decision.addon
|
||||
CinderJob.objects.create(target_addon=addon, decision=self.decision)
|
||||
assert addon.status == amo.STATUS_APPROVED
|
||||
responses.add(
|
||||
responses.POST,
|
||||
f'{settings.CINDER_SERVER_URL}create_report',
|
||||
json={'job_id': '5678'},
|
||||
status=201,
|
||||
)
|
||||
|
||||
response = self.client.post(self.url, {'choice': 'forward'})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert addon.reload().status == amo.STATUS_APPROVED
|
||||
new_job = self.decision.cinder_job.reload().forwarded_to_job
|
||||
assert new_job
|
||||
assert new_job.resolvable_in_reviewer_tools
|
||||
self.assertCloseToNow(self.decision.reload().action_date)
|
||||
|
||||
def test_non_admin_cannot_access(self):
|
||||
self.login_as_reviewer()
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_reviewer_viewer_can_access(self):
|
||||
user = user_factory()
|
||||
self.grant_permission(user, 'ReviewerTools:View')
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
assert pq(response.content)('.entity-type-Extension')
|
||||
|
||||
def test_reviewer_viewer_cannot_submit(self):
|
||||
user = user_factory()
|
||||
self.grant_permission(user, 'ReviewerTools:View')
|
||||
self.client.force_login(user)
|
||||
response = self.client.post(self.url, {'choice': 'yes'})
|
||||
|
||||
assert response.status_code == 403
|
||||
assert self.decision.addon.reload().status == amo.STATUS_APPROVED
|
||||
assert self.decision.reload().action_date is None
|
||||
|
|
|
@ -73,6 +73,11 @@ urlpatterns = (
|
|||
views.whiteboard,
|
||||
name='reviewers.whiteboard',
|
||||
),
|
||||
re_path(
|
||||
r'^decision-review/(?P<decision_id>[^/<>]+)$',
|
||||
views.decision_review,
|
||||
name='reviewers.decision_review',
|
||||
),
|
||||
re_path(r'^eula/%s$' % ADDON_ID, views.eula, name='reviewers.eula'),
|
||||
re_path(r'^privacy/%s$' % ADDON_ID, views.privacy, name='reviewers.privacy'),
|
||||
re_path(r'^motd$', views.motd, name='reviewers.motd'),
|
||||
|
|
|
@ -299,13 +299,13 @@ class ModerationQueueTable:
|
|||
view_name = 'queue_moderated'
|
||||
|
||||
|
||||
class HeldActionQueueTable:
|
||||
title = 'Held Actions for 2nd Level Approval'
|
||||
urlname = 'queue_held_actions'
|
||||
url = r'^held_actions$'
|
||||
class HeldDecisionQueueTable:
|
||||
title = 'Held Decisions for 2nd Level Approval'
|
||||
urlname = 'queue_decisions'
|
||||
url = r'^held_decisions$'
|
||||
permission = amo.permissions.REVIEWS_ADMIN
|
||||
show_count_in_dashboard = True
|
||||
view_name = 'queue_held_actions'
|
||||
view_name = 'queue_decisions'
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, request, **kw):
|
||||
|
|
|
@ -2,7 +2,7 @@ import functools
|
|||
import operator
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, urljoin
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
|
@ -35,7 +35,8 @@ from rest_framework.viewsets import GenericViewSet
|
|||
|
||||
import olympia.core.logger
|
||||
from olympia import amo
|
||||
from olympia.abuse.models import AbuseReport
|
||||
from olympia.abuse.models import AbuseReport, CinderJob, CinderPolicy, ContentDecision
|
||||
from olympia.abuse.tasks import handle_escalate_action
|
||||
from olympia.access import acl
|
||||
from olympia.activity.models import ActivityLog, CommentLog, DraftComment
|
||||
from olympia.addons.models import (
|
||||
|
@ -63,6 +64,7 @@ from olympia.api.permissions import (
|
|||
AnyOf,
|
||||
GroupPermission,
|
||||
)
|
||||
from olympia.constants.abuse import DECISION_ACTIONS
|
||||
from olympia.constants.reviewers import (
|
||||
REVIEWS_PER_PAGE,
|
||||
REVIEWS_PER_PAGE_MAX,
|
||||
|
@ -71,47 +73,6 @@ from olympia.constants.reviewers import (
|
|||
from olympia.devhub import tasks as devhub_tasks
|
||||
from olympia.files.models import File
|
||||
from olympia.ratings.models import Rating, RatingFlag
|
||||
from olympia.reviewers.forms import (
|
||||
MOTDForm,
|
||||
PublicWhiteboardForm,
|
||||
RatingFlagFormSet,
|
||||
RatingModerationLogForm,
|
||||
ReviewForm,
|
||||
ReviewLogForm,
|
||||
ReviewQueueFilter,
|
||||
WhiteboardForm,
|
||||
)
|
||||
from olympia.reviewers.models import (
|
||||
AutoApprovalSummary,
|
||||
NeedsHumanReview,
|
||||
ReviewerSubscription,
|
||||
Whiteboard,
|
||||
clear_reviewing_cache,
|
||||
get_flags,
|
||||
get_reviewing_cache,
|
||||
get_reviewing_cache_key,
|
||||
set_reviewing_cache,
|
||||
)
|
||||
from olympia.reviewers.serializers import (
|
||||
AddonBrowseVersionSerializer,
|
||||
AddonBrowseVersionSerializerFileOnly,
|
||||
AddonCompareVersionSerializer,
|
||||
AddonCompareVersionSerializerFileOnly,
|
||||
AddonReviewerFlagsSerializer,
|
||||
DiffableVersionSerializer,
|
||||
DraftCommentSerializer,
|
||||
FileInfoSerializer,
|
||||
)
|
||||
from olympia.reviewers.utils import (
|
||||
ContentReviewTable,
|
||||
HeldActionQueueTable,
|
||||
MadReviewTable,
|
||||
ModerationQueueTable,
|
||||
PendingManualApprovalQueueTable,
|
||||
PendingRejectionTable,
|
||||
ReviewHelper,
|
||||
ThemesQueueTable,
|
||||
)
|
||||
from olympia.scanners.admin import formatted_matched_rules_with_files_and_data
|
||||
from olympia.stats.decorators import bigquery_api_view
|
||||
from olympia.stats.utils import (
|
||||
|
@ -128,7 +89,49 @@ from .decorators import (
|
|||
permission_or_tools_listed_view_required,
|
||||
reviewer_addon_view_factory,
|
||||
)
|
||||
from .forms import (
|
||||
HeldDecisionReviewForm,
|
||||
MOTDForm,
|
||||
PublicWhiteboardForm,
|
||||
RatingFlagFormSet,
|
||||
RatingModerationLogForm,
|
||||
ReviewForm,
|
||||
ReviewLogForm,
|
||||
ReviewQueueFilter,
|
||||
WhiteboardForm,
|
||||
)
|
||||
from .models import (
|
||||
AutoApprovalSummary,
|
||||
NeedsHumanReview,
|
||||
ReviewerSubscription,
|
||||
Whiteboard,
|
||||
clear_reviewing_cache,
|
||||
get_flags,
|
||||
get_reviewing_cache,
|
||||
get_reviewing_cache_key,
|
||||
set_reviewing_cache,
|
||||
)
|
||||
from .serializers import (
|
||||
AddonBrowseVersionSerializer,
|
||||
AddonBrowseVersionSerializerFileOnly,
|
||||
AddonCompareVersionSerializer,
|
||||
AddonCompareVersionSerializerFileOnly,
|
||||
AddonReviewerFlagsSerializer,
|
||||
DiffableVersionSerializer,
|
||||
DraftCommentSerializer,
|
||||
FileInfoSerializer,
|
||||
)
|
||||
from .templatetags.jinja_helpers import to_dom_id
|
||||
from .utils import (
|
||||
ContentReviewTable,
|
||||
HeldDecisionQueueTable,
|
||||
MadReviewTable,
|
||||
ModerationQueueTable,
|
||||
PendingManualApprovalQueueTable,
|
||||
PendingRejectionTable,
|
||||
ReviewHelper,
|
||||
ThemesQueueTable,
|
||||
)
|
||||
|
||||
|
||||
def context(**kw):
|
||||
|
@ -283,10 +286,10 @@ def dashboard(request):
|
|||
reverse('reviewers.queue_pending_rejection'),
|
||||
),
|
||||
(
|
||||
'Held Actions for 2nd Level Approval ({0})'.format(
|
||||
queue_counts['held_actions']
|
||||
'Held Decisions for 2nd Level Approval ({0})'.format(
|
||||
queue_counts['held_decisions']
|
||||
),
|
||||
reverse('reviewers.queue_held_actions'),
|
||||
reverse('reviewers.queue_decisions'),
|
||||
),
|
||||
]
|
||||
return TemplateResponse(
|
||||
|
@ -438,7 +441,7 @@ reviewer_tables_registry = {
|
|||
'mad': MadReviewTable,
|
||||
'pending_rejection': PendingRejectionTable,
|
||||
'moderated': ModerationQueueTable,
|
||||
'held_actions': HeldActionQueueTable,
|
||||
'held_decisions': HeldDecisionQueueTable,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1589,7 +1592,7 @@ def review_version_redirect(request, addon, version):
|
|||
|
||||
|
||||
@permission_or_tools_listed_view_required(amo.permissions.REVIEWS_ADMIN)
|
||||
def queue_held_actions(request, tab):
|
||||
def queue_decisions(request, tab):
|
||||
TableObj = reviewer_tables_registry[tab]
|
||||
qs = TableObj.get_queryset(request)
|
||||
page = paginate(request, qs, per_page=20)
|
||||
|
@ -1604,3 +1607,42 @@ def queue_held_actions(request, tab):
|
|||
title=TableObj.title,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@permission_or_tools_listed_view_required(amo.permissions.REVIEWS_ADMIN)
|
||||
def decision_review(request, decision_id):
|
||||
decision = get_object_or_404(ContentDecision, pk=decision_id)
|
||||
cinder_jobs_qs = CinderJob.objects.filter(decision_id=decision.id)
|
||||
form = HeldDecisionReviewForm(
|
||||
request.POST if request.method == 'POST' else None,
|
||||
cinder_jobs_qs=cinder_jobs_qs,
|
||||
)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
match data.get('choice'):
|
||||
case 'yes':
|
||||
decision.process_action(release_hold=True)
|
||||
case 'no':
|
||||
decision.update(action=DECISION_ACTIONS.AMO_APPROVE, notes='')
|
||||
decision.policies.set(
|
||||
CinderPolicy.objects.filter(
|
||||
default_cinder_action=DECISION_ACTIONS.AMO_APPROVE
|
||||
)
|
||||
)
|
||||
decision.process_action(release_hold=True)
|
||||
case 'forward':
|
||||
decision.update(action_date=datetime.now())
|
||||
for job in cinder_jobs_qs:
|
||||
handle_escalate_action.delay(job_pk=job.id)
|
||||
return redirect('reviewers.queue_decisions')
|
||||
return TemplateResponse(
|
||||
request,
|
||||
'reviewers/decision_review.html',
|
||||
context=context(
|
||||
cinder_url=urljoin(
|
||||
settings.CINDER_SERVER_URL, f'/decision/{decision.cinder_id}'
|
||||
),
|
||||
decision=decision,
|
||||
form=form,
|
||||
),
|
||||
)
|
||||
|
|
Загрузка…
Ссылка в новой задаче