Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make reviewer pending rejection input a datetime widget and allow changing it through an action #23001

Merged
merged 25 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions src/olympia/abuse/actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import random
from datetime import datetime, timedelta
from datetime import datetime

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -383,10 +383,23 @@ class ContentActionRejectVersionDelayed(ContentActionRejectVersion):

def __init__(self, decision):
super().__init__(decision)
self.days = int(self.decision.metadata.get('delayed_rejection_days', 0))
if 'delayed_rejection_date' in self.decision.metadata:
diox marked this conversation as resolved.
Show resolved Hide resolved
self.delayed_rejection_date = datetime.fromisoformat(
self.decision.metadata.get('delayed_rejection_date')
)
else:
diox marked this conversation as resolved.
Show resolved Hide resolved
# Will fail later if we try to use it to log/process the action,
# but allows us to at least instantiate the class and use other
# methods.
self.delayed_rejection_date = None

def log_action(self, activity_log_action, *extra_args, extra_details=None):
extra_details = {**(extra_details or {}), 'delayed_rejection_days': self.days}
extra_details = {
**(extra_details or {}),
'delayed_rejection_days': (
self.delayed_rejection_date - datetime.now()
).days,
}
return super().log_action(
activity_log_action, *extra_args, extra_details=extra_details
)
Expand All @@ -397,14 +410,13 @@ def process_action(self):
if not self.decision.reviewer_user:
# This action should only be used by reviewer tools, not cinder webhook
raise NotImplementedError
pending_rejection_deadline = datetime.now() + timedelta(days=self.days)

for version in self.decision.target_versions.all():
# (Re)set pending_rejection.
VersionReviewerFlags.objects.update_or_create(
version=version,
defaults={
'pending_rejection': pending_rejection_deadline,
'pending_rejection': self.delayed_rejection_date,
'pending_rejection_by': self.decision.reviewer_user,
'pending_content_rejection': self.content_review,
},
Expand Down Expand Up @@ -447,6 +459,14 @@ def process_action(self):
return self.log_action(amo.LOG.REQUEST_LEGAL)


class ContentActionChangePendingRejectionDate(ContentAction):
description = 'Add-on pending rejection date has changed'
valid_targets = (Addon,)

def get_owners(self):
return self.target.authors.all()


class ContentActionDeleteCollection(ContentAction):
valid_targets = (Collection,)
description = 'Collection has been deleted'
Expand Down
5 changes: 5 additions & 0 deletions src/olympia/abuse/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ContentActionApproveInitialDecision,
ContentActionApproveNoAction,
ContentActionBanUser,
ContentActionChangePendingRejectionDate,
ContentActionDeleteCollection,
ContentActionDeleteRating,
ContentActionDisableAddon,
Expand Down Expand Up @@ -1070,6 +1071,9 @@ def get_action_helper_class(cls, decision_action):
DECISION_ACTIONS.AMO_IGNORE: ContentActionIgnore,
DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: ContentActionAlreadyRemoved,
DECISION_ACTIONS.AMO_LEGAL_FORWARD: ContentActionForwardToLegal,
DECISION_ACTIONS.AMO_CHANGE_PENDING_REJECTION_DATE: (
ContentActionChangePendingRejectionDate
),
}.get(decision_action, ContentActionNotImplemented)

def get_action_helper(self):
Expand Down Expand Up @@ -1314,6 +1318,7 @@ def send_notifications(self):
extra_context = {
'auto_approval': is_auto_approval,
'delayed_rejection_days': details.get('delayed_rejection_days'),
'details': details,
'is_addon_being_blocked': details.get('is_addon_being_blocked'),
'is_addon_disabled': details.get('is_addon_being_disabled')
or getattr(self.target, 'is_disabled', False),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% extends "abuse/emails/base.txt" %}{% block content %}

As you are aware, your {{ type }} {{ name }} was manually reviewed by the Mozilla Add-ons team, at which point we found a violation of one or more Mozilla add-on policies.
KevinMind marked this conversation as resolved.
Show resolved Hide resolved

Our previous correspondence indicated that you would be required to correct the violation(s) by {{ details.old_deadline }}. However, after further assessing the circumstances - including the violation itself, the risks it presents, and the steps required to resolve it - we have determined that an alternative timeline is appropriate. Based on that determination, we have updated the deadline, and will now require you to correct your add-on violations no later than {{ details.new_deadline }}.
{% endblock %}
10 changes: 6 additions & 4 deletions src/olympia/abuse/tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,9 +782,13 @@ def test_reject_version_after_reporter_appeal(self):
self._test_reporter_appeal_takedown_email(subject)

def _test_reject_version_delayed(self, *, content_review):
in_the_future = datetime.now() + timedelta(days=14, hours=1)
self.decision.update(
action=DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON,
metadata={'delayed_rejection_days': 14, 'content_review': content_review},
metadata={
'delayed_rejection_date': in_the_future.isoformat(),
'content_review': content_review,
},
)
action = ContentActionRejectVersionDelayed(self.decision)
# process_action is only available for reviewer tools decisions.
Expand All @@ -804,9 +808,7 @@ def _test_reject_version_delayed(self, *, content_review):
assert self.addon.reload().status == amo.STATUS_APPROVED
assert self.version.file.status == amo.STATUS_APPROVED
version_flags = VersionReviewerFlags.objects.filter(version=self.version).get()
self.assertCloseToNow(
version_flags.pending_rejection, now=datetime.now() + timedelta(14)
)
self.assertCloseToNow(version_flags.pending_rejection, now=in_the_future)
assert version_flags.pending_rejection_by == reviewer
assert version_flags.pending_content_rejection == content_review
assert ActivityLog.objects.count() == 1
Expand Down
79 changes: 61 additions & 18 deletions src/olympia/abuse/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import uuid
from datetime import datetime
from datetime import datetime, timedelta
from unittest import mock

from django.conf import settings
Expand Down Expand Up @@ -1170,11 +1170,10 @@ def test_process_decision(self):
policy_a = CinderPolicy.objects.create(uuid='123-45', name='aaa', text='AAA')
policy_b = CinderPolicy.objects.create(uuid='678-90', name='bbb', text='BBB')

with mock.patch.object(
ContentActionBanUser, 'process_action'
) as action_mock, mock.patch.object(
ContentActionBanUser, 'notify_owners'
) as notify_mock:
with (
KevinMind marked this conversation as resolved.
Show resolved Hide resolved
mock.patch.object(ContentActionBanUser, 'process_action') as action_mock,
mock.patch.object(ContentActionBanUser, 'notify_owners') as notify_mock,
):
action_mock.return_value = None
cinder_job.process_decision(
decision_cinder_id='12345',
Expand All @@ -1201,11 +1200,10 @@ def test_process_decision_with_duplicate_parent(self):
uuid='123-45', name='aaa', text='AAA', parent=parent_policy
)

with mock.patch.object(
ContentActionBanUser, 'process_action'
) as action_mock, mock.patch.object(
ContentActionBanUser, 'notify_owners'
) as notify_mock:
with (
mock.patch.object(ContentActionBanUser, 'process_action') as action_mock,
mock.patch.object(ContentActionBanUser, 'notify_owners') as notify_mock,
):
action_mock.return_value = None
cinder_job.process_decision(
decision_cinder_id='12345',
Expand Down Expand Up @@ -2837,7 +2835,11 @@ def test_execute_action_reject_version_delayed(self):
action=DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON,
notes='some review text',
reviewer_user=self.reviewer_user,
metadata={'delayed_rejection_days': 14},
metadata={
'delayed_rejection_date': (
datetime.now() + timedelta(days=14, minutes=1)
).isoformat()
},
)
decision.target_versions.set([addon.current_version])
NeedsHumanReview.objects.create(
Expand Down Expand Up @@ -2872,7 +2874,11 @@ def test_execute_action_reject_version_delayed_held(self):
action=DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON,
notes='some review text',
reviewer_user=self.reviewer_user,
metadata={'delayed_rejection_days': 14},
metadata={
'delayed_rejection_date': (
datetime.now() + timedelta(days=14, minutes=1)
).isoformat()
},
)
decision.target_versions.set([version])
assert decision.action_date is None
Expand All @@ -2889,6 +2895,42 @@ def test_execute_action_reject_version_delayed_held(self):
decision.execute_action(release_hold=True)
self._test_execute_action_reject_version_delayed_outcome(decision)

def test_send_notifications_change_pending_rejection_date(self):
addon = addon_factory(users=[user_factory(email='[email protected]')])
old_pending_rejection = self.days_ago(1)
new_pending_rejection = datetime.now() + timedelta(days=1)
version_review_flags_factory(
version=addon.current_version,
pending_rejection=old_pending_rejection,
pending_rejection_by=user_factory(),
pending_content_rejection=False,
)
decision = ContentDecision.objects.create(
addon=addon,
action=DECISION_ACTIONS.AMO_CHANGE_PENDING_REJECTION_DATE,
action_date=datetime.now(),
)
ActivityLog.objects.create(
amo.LOG.CHANGE_PENDING_REJECTION,
addon,
addon.current_version,
decision,
details={
'old_deadline': str(old_pending_rejection),
'new_deadline': str(new_pending_rejection),
},
user=user_factory(),
)
decision.send_notifications()
assert (
'previous correspondence indicated that you would be required '
f'to correct the violation(s) by {old_pending_rejection}'
) in mail.outbox[0].body
assert (
'now require you to correct your add-on violations no later '
f'than {new_pending_rejection}'
) in mail.outbox[0].body

def test_resolve_job_forwarded(self):
addon_developer = user_factory()
addon = addon_factory(users=[addon_developer])
Expand Down Expand Up @@ -3105,11 +3147,12 @@ def test_execute_action_with_action_date_already(self):
user=user_factory(),
)

with mock.patch.object(
ContentActionDisableAddon, 'process_action'
) as process_mock, mock.patch.object(
ContentActionDisableAddon, 'hold_action'
) as hold_mock:
with (
mock.patch.object(
ContentActionDisableAddon, 'process_action'
) as process_mock,
mock.patch.object(ContentActionDisableAddon, 'hold_action') as hold_mock,
):
decision.execute_action()
process_mock.assert_not_called()
hold_mock.assert_not_called()
Expand Down
15 changes: 12 additions & 3 deletions src/olympia/constants/abuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
('AMO_IGNORE', 11, 'Invalid report, so ignored'),
('AMO_CLOSED_NO_ACTION', 12, 'Content already removed (no action)'),
('AMO_LEGAL_FORWARD', 13, 'Forward add-on to legal'),
# Changing pending rejection date is not an available action for moderators
# in cinder - it is only performed by AMO Reviewers.
('AMO_CHANGE_PENDING_REJECTION_DATE', 14, 'Pending rejection date changed'),
)
DECISION_ACTIONS.add_subset(
'APPEALABLE_BY_AUTHOR',
Expand Down Expand Up @@ -51,7 +54,13 @@
'NON_OFFENDING', ('AMO_APPROVE', 'AMO_APPROVE_VERSION', 'AMO_IGNORE')
)
DECISION_ACTIONS.add_subset(
'SKIP_DECISION', ('AMO_APPROVE', 'AMO_APPROVE_VERSION', 'AMO_LEGAL_FORWARD')
'SKIP_DECISION',
(
'AMO_APPROVE',
'AMO_APPROVE_VERSION',
'AMO_LEGAL_FORWARD',
'AMO_CHANGE_PENDING_REJECTION_DATE',
),
)

# Illegal categories, only used when the reason is `illegal`. The constants
Expand Down Expand Up @@ -115,12 +124,12 @@
(
'HIDDEN_ADVERTISEMENT',
4,
'Hidden advertisement or commercial communication, including by ' 'influencers',
'Hidden advertisement or commercial communication, including by influencers',
),
(
'MISLEADING_INFO_GOODS_SERVICES',
5,
'Misleading information about the characteristics of the goods and ' 'services',
'Misleading information about the characteristics of the goods and services',
),
(
'MISLEADING_INFO_CONSUMER_RIGHTS',
Expand Down
11 changes: 11 additions & 0 deletions src/olympia/constants/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,17 @@ class HELD_ACTION_REJECT_CONTENT_DELAYED(_LOG):
admin_event = True


class CHANGE_PENDING_REJECTION(_LOG):
id = 203
format = _('{addon} {version} pending rejection changed.')
short = _('Pending rejection changed')
keep = True
review_queue = True
reviewer_review_action = True
cinder_action = DECISION_ACTIONS.AMO_CHANGE_PENDING_REJECTION_DATE
# Not hidden to developers.


LOGS = [x for x in vars().values() if isclass(x) and issubclass(x, _LOG) and x != _LOG]
# Make sure there's no duplicate IDs.
assert len(LOGS) == len({log.id for log in LOGS})
Expand Down
Loading
Loading