Skip to content

Commit ea8c3a9

Browse files
authored
Don't inherit due date and/or NeedsHumanReview for ABUSE_ADDON_VIOLATION/CINDER_ESCALATION (#23343)
* Don't inherit due date for ABUSE_ADDON_VIOLATION/CINDER_ESCALATION *Only* for those reasons: as soon as a version has more reasons for needing human review its due date can be inherited from again. * Also don't inherit NHR itself * Implement filtering for reasons inside get_queryset_for_pending_queues() This fixes due date display/sorting in the review queue when there are multiple different due dates set and some would be filtered out
1 parent 5e9e5e7 commit ea8c3a9

File tree

8 files changed

+230
-22
lines changed

8 files changed

+230
-22
lines changed

src/olympia/addons/models.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def get_queryset_for_pending_queues(
307307
theme_review=False,
308308
show_temporarily_delayed=True,
309309
show_only_upcoming=False,
310+
due_date_reasons_choices=None,
310311
):
311312
filters = {
312313
'type__in': amo.GROUP_TYPE_THEME if theme_review else amo.GROUP_TYPE_ADDON,
@@ -359,13 +360,29 @@ def get_queryset_for_pending_queues(
359360
& ~Q(**{listed_delay_flag_field: datetime.max})
360361
)
361362
)
363+
version_subqs = versions_due_qs.all()
364+
if due_date_reasons_choices:
365+
versions_filter = Q(
366+
versions__needshumanreview__reason__in=due_date_reasons_choices.values
367+
)
368+
version_subqs = version_subqs.filter(
369+
needshumanreview__reason__in=due_date_reasons_choices.values
370+
)
371+
else:
372+
versions_filter = None
362373
qs = (
363374
qs.filter(**filters)
364375
.annotate(
365-
first_version_due_date=Min('versions__due_date'),
366-
first_version_id=Subquery(
367-
versions_due_qs.filter(addon=OuterRef('pk')).values('pk')[:1]
376+
# We need both first_version_due_date and first_version_id to
377+
# be set and have the same behavior. The former is used for
378+
# grouping (hence the Min()) and provides a way for callers to
379+
# sort this queryset by due date, the latter to filter add-ons
380+
# matching the reasons we care about and to instantiate the
381+
# right Version in first_pending_version_transformer().
382+
first_version_due_date=Min(
383+
'versions__due_date', filter=versions_filter
368384
),
385+
first_version_id=Subquery(version_subqs.values('pk')[:1]),
369386
**{
370387
name: Exists(versions_due_qs.filter(q))
371388
for name, q in (
@@ -768,6 +785,21 @@ def _set_needs_human_review_on_latest_signed_version(
768785
version.reset_due_date(due_date)
769786
return version
770787

788+
def versions_triggering_needs_human_review_inheritance(self, channel):
789+
"""Return queryset of Versions belonging to this addon in the specified
790+
channel that should be considered for due date and NeedsHumanReview
791+
inheritance."""
792+
from olympia.reviewers.models import NeedsHumanReview
793+
794+
reasons_triggering_inheritance = set(
795+
NeedsHumanReview.REASONS.values.keys()
796+
) - set(NeedsHumanReview.REASONS.NO_DUE_DATE_INHERITANCE.values.keys())
797+
return self.versions(manager='unfiltered_for_relations').filter(
798+
channel=channel,
799+
needshumanreview__is_active=True,
800+
needshumanreview__reason__in=reasons_triggering_inheritance,
801+
)
802+
771803
@property
772804
def is_guid_denied(self):
773805
return DeniedGuid.objects.filter(guid=self.guid).exists()

src/olympia/addons/tests/test_models.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2483,6 +2483,46 @@ def test_set_needs_human_review_on_latest_versions_even_deleted(self):
24832483
== NeedsHumanReview.REASONS.UNKNOWN
24842484
)
24852485

2486+
def test_versions_triggering_needs_human_review_inheritance(self):
2487+
addon = Addon.objects.get(id=3615)
2488+
version = addon.current_version
2489+
version.needshumanreview_set.create(reason=NeedsHumanReview.REASONS.UNKNOWN)
2490+
version2 = version_factory(addon=addon)
2491+
unlisted_version = version_factory(addon=addon, channel=amo.CHANNEL_UNLISTED)
2492+
unlisted_version.needshumanreview_set.create(
2493+
reason=NeedsHumanReview.REASONS.UNKNOWN
2494+
)
2495+
assert set(
2496+
addon.versions_triggering_needs_human_review_inheritance(amo.CHANNEL_LISTED)
2497+
) == {version}
2498+
assert set(
2499+
addon.versions_triggering_needs_human_review_inheritance(
2500+
amo.CHANNEL_UNLISTED
2501+
)
2502+
) == {unlisted_version}
2503+
2504+
# Adding any of those NHR should not matter.
2505+
for reason_value, _ in NeedsHumanReview.REASONS.NO_DUE_DATE_INHERITANCE:
2506+
version2.needshumanreview_set.create(reason=reason_value)
2507+
2508+
assert set(
2509+
addon.versions_triggering_needs_human_review_inheritance(amo.CHANNEL_LISTED)
2510+
) == {version}
2511+
2512+
# Adding any other NHR should.
2513+
nhr = version2.needshumanreview_set.create(
2514+
reason=NeedsHumanReview.REASONS.AUTO_APPROVAL_DISABLED
2515+
)
2516+
assert set(
2517+
addon.versions_triggering_needs_human_review_inheritance(amo.CHANNEL_LISTED)
2518+
) == {version, version2}
2519+
2520+
# An inactive NHR should not trigger inheritance.
2521+
nhr.update(is_active=False)
2522+
assert set(
2523+
addon.versions_triggering_needs_human_review_inheritance(amo.CHANNEL_LISTED)
2524+
) == {version}
2525+
24862526
def test_update_all_due_dates(self):
24872527
addon = Addon.objects.get(id=3615)
24882528
versions_that_should_have_due_date = [
@@ -3865,6 +3905,52 @@ def test_pending_queue_needs_human_scanner_action(self):
38653905
NeedsHumanReview.REASONS.SCANNER_ACTION, 'needs_human_review_scanner_action'
38663906
)
38673907

3908+
def test_get_queryset_for_pending_queues_for_specific_due_date_reasons(self):
3909+
expected_addons = [
3910+
version_factory(
3911+
addon=addon_factory(
3912+
version_kw={
3913+
'needshumanreview_kw': {
3914+
'reason': NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
3915+
},
3916+
'due_date': self.days_ago(48),
3917+
'version': '0.1',
3918+
}
3919+
),
3920+
needshumanreview_kw={
3921+
'reason': NeedsHumanReview.REASONS.AUTO_APPROVAL_DISABLED
3922+
},
3923+
due_date=self.days_ago(15),
3924+
version='0.2',
3925+
).addon,
3926+
addon_factory(
3927+
version_kw={
3928+
'needshumanreview_kw': {
3929+
'reason': NeedsHumanReview.REASONS.SCANNER_ACTION
3930+
},
3931+
'due_date': self.days_ago(16),
3932+
'version': '666.0',
3933+
}
3934+
),
3935+
]
3936+
addon_factory(
3937+
version_kw={
3938+
'needshumanreview_kw': {
3939+
'reason': NeedsHumanReview.REASONS.DEVELOPER_REPLY
3940+
},
3941+
'due_date': self.days_ago(23),
3942+
}
3943+
) # Should not show up
3944+
addons = Addon.objects.get_queryset_for_pending_queues(
3945+
due_date_reasons_choices=NeedsHumanReview.REASONS.extract_subset(
3946+
'AUTO_APPROVAL_DISABLED', 'SCANNER_ACTION'
3947+
)
3948+
)
3949+
expected_version = expected_addons[0].versions.get(version='0.2')
3950+
assert addons[0].first_version_id == expected_version.pk
3951+
assert addons[0].first_pending_version == expected_version
3952+
assert addons[0].first_version_due_date == expected_version.due_date
3953+
38683954
def test_get_pending_rejection_queue(self):
38693955
expected_addons = [
38703956
version_review_flags_factory(

src/olympia/reviewers/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,13 @@ class NeedsHumanReview(ModelBase):
933933
'SECOND_LEVEL_REQUEUE',
934934
),
935935
)
936+
REASONS.add_subset(
937+
'NO_DUE_DATE_INHERITANCE',
938+
(
939+
'ABUSE_ADDON_VIOLATION',
940+
'CINDER_ESCALATION',
941+
),
942+
)
936943

937944
reason = models.SmallIntegerField(
938945
default=0, choices=REASONS.choices, editable=False

src/olympia/reviewers/tests/test_views.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,34 @@ def test_due_date_reason_with_two_filters(self):
17621762
)
17631763
self._test_results()
17641764

1765+
def test_different_due_dates_correct_one_is_shown_when_filtering(self):
1766+
self.url += '?due_date_reasons=needs_human_review_developer_reply'
1767+
self.expected_addons = self.get_expected_addons_by_names(
1768+
['Pending One', 'Public'],
1769+
)
1770+
# Create a NHR that would be filtered out and not inherited on
1771+
# "Pending One" first version with an old due date, then make a new
1772+
# version with a NHR that we are going to filter for, and a different
1773+
# due date. That new version and due date should be the ones shown.
1774+
self.addons['Pending One'].current_version.needshumanreview_set.create(
1775+
reason=NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
1776+
)
1777+
self.addons['Pending One'].current_version.update(due_date=self.days_ago(42))
1778+
self.expected_versions = self.get_expected_versions(self.expected_addons)
1779+
self.expected_versions[self.addons['Pending One']] = version_factory(
1780+
addon=self.addons['Pending One'], version='0.2'
1781+
)
1782+
for version in self.expected_versions.values():
1783+
version.needshumanreview_set.create(
1784+
reason=NeedsHumanReview.REASONS.DEVELOPER_REPLY
1785+
)
1786+
version.update(due_date=self.days_ago(1))
1787+
1788+
self.expected_versions[self.addons['Pending One']]
1789+
doc = self._test_results()
1790+
rows = doc('#addon-queue tr.addon-row')
1791+
assert '1\xa0day ago' in rows[0].text_content()
1792+
17651793

17661794
class TestThemeQueue(QueueTest):
17671795
def setUp(self):

src/olympia/reviewers/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,14 @@ class Meta(AddonQueueTable.Meta):
115115
orderable = True
116116

117117
@classmethod
118-
def get_queryset(cls, request, *, upcoming_due_date_focus=False, **kwargs):
118+
def get_queryset(
119+
cls,
120+
request,
121+
*,
122+
upcoming_due_date_focus=False,
123+
due_date_reasons_choices=None,
124+
**kwargs,
125+
):
119126
if request is None:
120127
admin_reviewer = True
121128
show_temporarily_delayed = True
@@ -132,6 +139,7 @@ def get_queryset(cls, request, *, upcoming_due_date_focus=False, **kwargs):
132139
admin_reviewer=admin_reviewer,
133140
show_temporarily_delayed=show_temporarily_delayed,
134141
show_only_upcoming=show_only_upcoming,
142+
due_date_reasons_choices=due_date_reasons_choices,
135143
)
136144

137145
def get_version(self, record):
@@ -172,10 +180,12 @@ class Meta(PendingManualApprovalQueueTable.Meta):
172180
)
173181

174182
@classmethod
175-
def get_queryset(cls, request, **kwargs):
183+
def get_queryset(cls, request, due_date_reasons_choices=None, **kwargs):
176184
admin_reviewer = is_admin_reviewer(request.user) if request else True
177185
return Addon.objects.get_queryset_for_pending_queues(
178-
admin_reviewer=admin_reviewer, theme_review=True
186+
admin_reviewer=admin_reviewer,
187+
theme_review=True,
188+
due_date_reasons_choices=due_date_reasons_choices,
179189
)
180190

181191

src/olympia/reviewers/views.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import functools
2-
import operator
31
from collections import OrderedDict
42
from datetime import date, datetime
53
from urllib.parse import urljoin
@@ -316,7 +314,6 @@ def queue(request, tab):
316314

317315
@permission_or_tools_listed_view_required(TableObj.permission)
318316
def _queue(request, tab):
319-
qs = TableObj.get_queryset(request=request, upcoming_due_date_focus=True)
320317
params = {}
321318
order_by = request.GET.get('sort')
322319
if order_by is None and hasattr(TableObj, 'default_order_by'):
@@ -326,11 +323,21 @@ def _queue(request, tab):
326323
filter_form = ReviewQueueFilter(
327324
request.GET if 'due_date_reasons' in request.GET else None
328325
)
329-
if filter_form.is_valid() and (
330-
due_date_reasons := filter_form.cleaned_data['due_date_reasons']
331-
):
332-
filters = [Q(**{reason: True}) for reason in due_date_reasons]
333-
qs = qs.filter(functools.reduce(operator.or_, filters))
326+
due_date_reasons_choices = None
327+
if filter_form.is_valid():
328+
# Build a choices subset from the submitted reasons.
329+
due_date_reasons_choices = NeedsHumanReview.REASONS.__class__(
330+
*(
331+
entry
332+
for entry in NeedsHumanReview.REASONS.entries
333+
if entry.annotation in filter_form.cleaned_data['due_date_reasons']
334+
)
335+
)
336+
qs = TableObj.get_queryset(
337+
request=request,
338+
upcoming_due_date_focus=True,
339+
due_date_reasons_choices=due_date_reasons_choices,
340+
)
334341
table = TableObj(data=qs, **params)
335342
per_page = request.GET.get('per_page', REVIEWS_PER_PAGE)
336343
try:

src/olympia/versions/models.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,7 @@ def from_upload(
430430
)
431431

432432
previous_version_had_needs_human_review = (
433-
addon.versions(manager='unfiltered_for_relations')
434-
.filter(channel=channel, needshumanreview__is_active=True)
435-
.exists()
433+
addon.versions_triggering_needs_human_review_inheritance(channel).exists()
436434
)
437435

438436
version = cls.objects.create(
@@ -1006,19 +1004,20 @@ def reset_due_date(self, due_date=None, should_have_due_date=None):
10061004
def generate_due_date(self):
10071005
"""
10081006
(Re)Generate a due date for this version, inheriting from the earliest
1009-
due date possible from any other version in the same channel if one
1010-
exists, but only if the result would be at at earlier date than
1011-
the default/existing one on the instance.
1007+
due date possible from any other version with reasons that can trigger
1008+
inheritance in the same channel if one exists, but only if the result
1009+
would be at at earlier date than the default/existing one on the
1010+
instance.
10121011
"""
10131012
qs = (
1014-
Version.unfiltered.filter(addon=self.addon, channel=self.channel)
1013+
self.addon.versions_triggering_needs_human_review_inheritance(self.channel)
10151014
.exclude(due_date=None)
10161015
.exclude(id=self.pk)
10171016
.values_list('due_date', flat=True)
10181017
.order_by('-due_date')
10191018
)
1020-
standard_or_existing_due_date = self.due_date or get_review_due_date()
10211019
due_date = qs.first()
1020+
standard_or_existing_due_date = self.due_date or get_review_due_date()
10221021
if not due_date or due_date > standard_or_existing_due_date:
10231022
due_date = standard_or_existing_due_date
10241023
return due_date

src/olympia/versions/tests/test_models.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,45 @@ def test_dont_inherit_needs_human_review_from_different_channel(self):
24192419
assert not upload_version.due_date
24202420
assert upload_version.needshumanreview_set.count() == 0
24212421

2422+
def test_dont_inherit_due_date_or_nhr_for_some_specific_reasons(self):
2423+
# Some NeedsHumanReview reasons don't pass their due date through
2424+
# inheritance if they are the only reason a version had a due date.
2425+
old_version = self.addon.current_version
2426+
old_version.needshumanreview_set.create(
2427+
reason=NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
2428+
)
2429+
assert old_version.due_date
2430+
old_version.update(due_date=self.days_ago(1))
2431+
new_version = Version.from_upload(
2432+
self.upload,
2433+
self.addon,
2434+
amo.CHANNEL_LISTED,
2435+
selected_apps=[self.selected_app],
2436+
parsed_data=self.dummy_parsed_data,
2437+
)
2438+
# The version doesn't gain a NHR for INHERITANCE
2439+
assert new_version.needshumanreview_set.count() == 0
2440+
# If it gains a NHR, it doesn't inherit the due date since the old
2441+
# version only needs human review for one of the reasons that does not
2442+
# trigger inheritance.
2443+
new_version.needshumanreview_set.create(reason=NeedsHumanReview.REASONS.UNKNOWN)
2444+
assert new_version.due_date
2445+
assert new_version.due_date > old_version.due_date
2446+
# Forcing re-generation doesn't change anything.
2447+
assert new_version.generate_due_date() > old_version.due_date
2448+
2449+
# The above remains true for CINDER_ESCALATION which is another reason
2450+
# that doesn't trigger due date inheritance.
2451+
old_version.needshumanreview_set.create(
2452+
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION
2453+
)
2454+
assert new_version.generate_due_date() > old_version.due_date
2455+
2456+
# If we add another reason that *does* trigger inheritance to the old
2457+
# version, suddenly we will inherit its due date.
2458+
old_version.needshumanreview_set.create(reason=NeedsHumanReview.REASONS.UNKNOWN)
2459+
assert new_version.generate_due_date() == old_version.due_date
2460+
24222461
def test_set_version_to_customs_scanners_result(self):
24232462
self.create_switch('enable-customs', active=True)
24242463
scanners_result = ScannerResult.objects.create(

0 commit comments

Comments
 (0)