Skip to content

Commit e0befef

Browse files
committed
Allow policies to selected directly for reviewer tools actions
1 parent edae141 commit e0befef

File tree

12 files changed

+682
-154
lines changed

12 files changed

+682
-154
lines changed

src/olympia/reviewers/forms.py

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import html
12
import zipfile
3+
from collections import defaultdict
24
from datetime import datetime, timedelta
35

46
from django import forms
@@ -15,11 +17,13 @@
1517
from django.utils.translation import gettext
1618

1719
import markupsafe
20+
import waffle
1821

1922
import olympia.core.logger
2023
from olympia import amo, ratings
2124
from olympia.abuse.models import CinderJob, CinderPolicy, ContentDecision
2225
from olympia.access import acl
26+
from olympia.addons.models import Addon
2327
from olympia.amo.forms import AMOModelForm
2428
from olympia.constants.abuse import DECISION_ACTIONS
2529
from olympia.constants.reviewers import REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT
@@ -104,22 +108,10 @@ class NumberInput(widgets.Input):
104108
input_type = 'number'
105109

106110

107-
class VersionsChoiceField(ModelMultipleChoiceField):
108-
"""
109-
Widget to use together with VersionsChoiceWidget to display the list of
110-
versions used by review page for some actions.
111-
"""
112-
113-
def label_from_instance(self, obj):
114-
"""Return the object instead of transforming into a label at this stage
115-
so that it's available in the widget."""
116-
return obj
117-
118-
119111
class VersionsChoiceWidget(forms.SelectMultiple):
120112
"""
121-
Widget to use together with VersionsChoiceField to display the list of
122-
versions used by review page for some actions.
113+
Widget to use together with WidgetRenderedModelMultipleChoiceField to display the
114+
list of versions used by review page for some actions.
123115
"""
124116

125117
actions_filters = {
@@ -308,6 +300,37 @@ def create_option(
308300
)
309301

310302

303+
class CinderPolicyWidget(forms.CheckboxSelectMultiple):
304+
"""
305+
Widget to use together with a WidgetRenderedModelMultipleChoiceField to display
306+
select elements with additional attribute to allow toggling.
307+
"""
308+
309+
def create_option(
310+
self, name, value, label, selected, index, subindex=None, attrs=None
311+
):
312+
obj = label
313+
label = str(obj)
314+
attrs = attrs or {}
315+
actions_on_policy = {
316+
DECISION_ACTIONS.for_api_value(action).value
317+
for action in obj.enforcement_actions
318+
if DECISION_ACTIONS.has_api_value(action)
319+
}
320+
actions = (
321+
reviewer_action
322+
for reviewer_action, defn in self.helper_actions.items()
323+
# show this policy for this action if there any common enforcement actions
324+
if (ha_ea := defn.get('enforcement_actions', ()))
325+
and actions_on_policy.intersection(ha_ea)
326+
)
327+
attrs['class'] = 'data-toggle'
328+
attrs['data-value'] = ' '.join(actions)
329+
return super().create_option(
330+
name, value, label, selected, index, subindex, attrs
331+
)
332+
333+
311334
class ActionChoiceWidget(forms.RadioSelect):
312335
"""
313336
Widget to add boilerplate_text to action options.
@@ -383,7 +406,7 @@ class ReviewForm(forms.Form):
383406
required=True, widget=forms.Textarea(), label='Comments:'
384407
)
385408
action = forms.ChoiceField(required=True, widget=ActionChoiceWidget)
386-
versions = VersionsChoiceField(
409+
versions = WidgetRenderedModelMultipleChoiceField(
387410
# The <select> is displayed/hidden dynamically depending on the action
388411
# so it needs the data-toggle class (data-value attribute is set later
389412
# during __init__). VersionsChoiceWidget takes care of adding that to
@@ -444,13 +467,13 @@ class ReviewForm(forms.Form):
444467
queryset=CinderJob.objects.none(),
445468
widget=CinderJobsWidget(attrs={'class': 'data-toggle-hide'}),
446469
)
447-
448-
cinder_policies = forms.ModelMultipleChoiceField(
470+
# queryset and widget are set later in __init__
471+
cinder_policies = WidgetRenderedModelMultipleChoiceField(
449472
# queryset is set later in __init__
450473
queryset=CinderPolicy.objects.none(),
451474
required=True,
452475
label='Choose one or more policies:',
453-
widget=widgets.CheckboxSelectMultiple,
476+
widget=CinderPolicyWidget(),
454477
)
455478
appeal_action = forms.MultipleChoiceField(
456479
required=False,
@@ -470,8 +493,11 @@ def is_valid(self):
470493
self.fields['versions'].required = True
471494
if not action.get('requires_reasons', False):
472495
self.fields['reasons'].required = False
473-
if not action.get('requires_policies'):
496+
if not action.get('enforcement_actions'):
474497
self.fields['cinder_policies'].required = False
498+
else:
499+
# we no longer strictly require comments with cinder policies
500+
self.fields['comments'].required = False
475501
if self.data.get('cinder_jobs_to_resolve'):
476502
# if a cinder job is being resolved we need a review reason
477503
if action.get('requires_reasons_for_cinder_jobs'):
@@ -508,8 +534,9 @@ def clean(self):
508534
if self.cleaned_data.get('cinder_jobs_to_resolve') and self.cleaned_data.get(
509535
'cinder_policies'
510536
):
511-
actions = self.helper.handler.get_decision_actions_from_policies(
512-
self.cleaned_data.get('cinder_policies')
537+
actions = CinderPolicy.get_decision_actions_from_policies(
538+
self.cleaned_data.get('cinder_policies'),
539+
for_entity=Addon,
513540
)
514541
if len(actions) == 0:
515542
self.add_error(
@@ -561,6 +588,23 @@ def clean(self):
561588
),
562589
)
563590

591+
# Add in any dynamic placeholder values for the cinder policies
592+
# see jinja_helpers.render_text_with_input_fields for name format
593+
if policies := {
594+
str(p.id): p for p in self.cleaned_data.get('cinder_policies', [])
595+
}:
596+
policy_values = defaultdict(dict)
597+
for key, value in self.data.items():
598+
if (
599+
key.startswith('policy-value-')
600+
and (split_1 := key.split('policy-value-', 1))
601+
and (id_and_placeholder := html.unescape(split_1[1]))
602+
and len(split_2 := id_and_placeholder.split('-', 1)) == 2
603+
and (policy := policies.get(split_2[0]))
604+
):
605+
policy_values[policy.uuid][split_2[1]] = value
606+
self.cleaned_data['policy_values'] = policy_values
607+
564608
return self.cleaned_data
565609

566610
def clean_delayed_rejection_date(self):
@@ -579,6 +623,9 @@ def clean_version_pk(self):
579623
def __init__(self, *args, **kw):
580624
self.helper = kw.pop('helper')
581625
super().__init__(*args, **kw)
626+
if waffle.switch_is_active('policy_selection_rather_than_reasons'):
627+
# When we're using policies reviewers shouldn't need to write as much
628+
self.fields['comments'].widget = forms.Textarea(attrs={'rows': 2})
582629
if any(action.get('delayable') for action in self.helper.actions.values()):
583630
# Default delayed rejection date should be
584631
# REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT days in the
@@ -646,15 +693,19 @@ def __init__(self, *args, **kw):
646693
]
647694

648695
# Set the queryset for reasons based on the add-on type.
649-
self.fields['reasons'].queryset = ReviewActionReason.objects.filter(
650-
is_active=True,
651-
addon_type__in=[
652-
amo.ADDON_ANY,
653-
amo.ADDON_STATICTHEME
654-
if self.helper.addon.type == amo.ADDON_STATICTHEME
655-
else amo.ADDON_EXTENSION,
656-
],
657-
).exclude(canned_response='')
696+
self.fields['reasons'].queryset = (
697+
ReviewActionReason.objects.filter(
698+
is_active=True,
699+
addon_type__in=[
700+
amo.ADDON_ANY,
701+
amo.ADDON_STATICTHEME
702+
if self.helper.addon.type == amo.ADDON_STATICTHEME
703+
else amo.ADDON_EXTENSION,
704+
],
705+
)
706+
.exclude(canned_response='')
707+
.select_related('cinder_policy__parent')
708+
)
658709

659710
# Add actions from the helper into the action widget so we can access
660711
# them in create_option.
@@ -668,7 +719,10 @@ def __init__(self, *args, **kw):
668719
# Set the queryset for policies to show as options
669720
self.fields['cinder_policies'].queryset = CinderPolicy.objects.filter(
670721
expose_in_reviewer_tools=True
671-
)
722+
).select_related('parent')
723+
724+
# Pass on the reviewer tools actions so we can set the show/hide on policies
725+
self.fields['cinder_policies'].widget.helper_actions = self.helper.actions
672726

673727
@property
674728
def unreviewed_files(self):
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 4.2.20 on 2025-05-08 17:31
2+
3+
from django.db import migrations
4+
5+
from olympia.core.db.migrations import CreateWaffleSwitch
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('reviewers', '0042_alter_needshumanreview_reason'),
12+
]
13+
14+
operations = [
15+
CreateWaffleSwitch('policy_selection_rather_than_reasons')
16+
]

src/olympia/reviewers/templates/reviewers/review.html

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,20 @@ <h3 id="history">
192192
</div>
193193

194194
<div class="review-actions-comments-reasons">
195-
<div class="review-actions-comments data-toggle review-comments" data-value="{{ actions_comments|join(' ') }}">
196-
<label for="id_comments">{{ form.comments.label }}</label>
197-
{{ form.comments }}
198-
{{ form.comments.errors }}
195+
196+
<div class="review-actions-comments">
197+
<div class="policy-texts data-toggle" data-value="{{ actions_policies|join(' ') }}">
198+
{% for policy in cinder_policies %}
199+
<div id="policy-text-{{ policy.id }}" hidden>{{ policy|render_text_with_input_fields }}</div>
200+
{% endfor %}
201+
</div>
202+
<div class="review-comments data-toggle" data-value="{{ actions_comments|join(' ') }}">
203+
<details{% if not waffle.switch('policy_selection_rather_than_reasons') %} open{% endif %}>
204+
<summary><label for="id_comments">{{ form.comments.label }}</label></summary>
205+
{{ form.comments }}
206+
</details>
207+
{{ form.comments.errors }}
208+
</div>
199209
</div>
200210
<div class="data-toggle review-actions-reasons"
201211
data-value="{{ actions_reasons|join(' ') }}">

src/olympia/reviewers/templatetags/jinja_helpers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import datetime
22

3+
from django.utils.html import escape
4+
from django.utils.safestring import mark_safe
5+
36
import jinja2
47
from django_jinja import library
58

@@ -70,3 +73,14 @@ def format_score(value):
7073
@library.filter
7174
def to_dom_id(string):
7275
return string.replace('.', '_')
76+
77+
78+
@library.filter
79+
def render_text_with_input_fields(policy):
80+
class WrapKey(dict):
81+
def __missing__(self, key):
82+
# django.utils.html.escape doesn't escape quotes
83+
key = key.replace('"', '&quot;')
84+
return f'<input placeholder="{key}" name="policy-value-{policy.id}-{key}">'
85+
86+
return mark_safe(escape(policy.text).format_map(WrapKey({})))

0 commit comments

Comments
 (0)