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:
Andrew Williamson 2024-11-20 13:16:30 +00:00 коммит произвёл GitHub
Родитель 9b435466ef
Коммит b3e55e775a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 415 добавлений и 115 удалений

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

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