diff --git a/src/olympia/abuse/migrations/0041_alter_decision_date.py b/src/olympia/abuse/migrations/0041_alter_decision_date.py new file mode 100644 index 0000000000..d1314003e6 --- /dev/null +++ b/src/olympia/abuse/migrations/0041_alter_decision_date.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2024-10-14 13:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abuse', '0040_alter_cinderpolicy_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='cinderdecision', + name='date', + field=models.DateTimeField(db_column='date', null=True), + ), + migrations.RenameField(model_name='cinderdecision', old_name='date', new_name='action_date'), + ] diff --git a/src/olympia/abuse/models.py b/src/olympia/abuse/models.py index 1b1355e7e6..5281e76f7a 100644 --- a/src/olympia/abuse/models.py +++ b/src/olympia/abuse/models.py @@ -5,7 +5,6 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models import Exists, OuterRef, Q from django.db.transaction import atomic -from django.utils import timezone from django.utils.functional import cached_property from olympia import amo @@ -278,14 +277,13 @@ class CinderJob(ModelBase): self, *, decision_cinder_id, - decision_date, decision_action, decision_notes, policy_ids, ): """This is called for cinder originated decisions. See resolve_job for reviewer tools originated decisions.""" - overriden_action = getattr(self.decision, 'action', None) + overridden_action = getattr(self.decision, 'action', None) # We need either an AbuseReport or CinderDecision for the target props abuse_report_or_decision = ( self.appealed_decisions.first() or self.abusereport_set.first() @@ -301,7 +299,6 @@ class CinderJob(ModelBase): 'rating': abuse_report_or_decision.rating, 'collection': abuse_report_or_decision.collection, 'user': abuse_report_or_decision.user, - 'date': decision_date, 'cinder_id': decision_cinder_id, 'action': decision_action, 'notes': decision_notes[ @@ -313,14 +310,8 @@ class CinderJob(ModelBase): policies = CinderPolicy.objects.filter( uuid__in=policy_ids ).without_parents_if_their_children_are_present() - self.decision.policies.add(*policies) - action_helper = self.decision.get_action_helper( - overriden_action=overriden_action, - appealed_action=getattr(self.appealed_decisions.first(), 'action', None), - ) - log_entry = action_helper.process_action() - self.notify_reporters(action_helper) - action_helper.notify_owners(log_entry_id=getattr(log_entry, 'id', None)) + cinder_decision.policies.add(*policies) + cinder_decision.process_action(overridden_action) def resolve_job(self, *, log_entry): """This is called for reviewer tools originated decisions. @@ -349,7 +340,7 @@ class CinderJob(ModelBase): appealed_action=getattr(self.appealed_decisions.first(), 'action', None), ) self.update(decision=cinder_decision) - if self.decision.is_delayed: + if cinder_decision.is_delayed: version_list = log_entry.versionlog_set.values_list('version', flat=True) self.pending_rejections.add( *VersionReviewerFlags.objects.filter(version__in=version_list) @@ -920,7 +911,7 @@ class CinderPolicy(ModelBase): class CinderDecision(ModelBase): action = models.PositiveSmallIntegerField(choices=DECISION_ACTIONS.choices) cinder_id = models.CharField(max_length=36, default=None, null=True, unique=True) - date = models.DateTimeField(default=timezone.now) + action_date = models.DateTimeField(null=True, db_column='date') notes = models.TextField(max_length=1000, blank=True) policies = models.ManyToManyField(to='abuse.CinderPolicy') appeal_job = models.ForeignKey( @@ -1022,7 +1013,7 @@ class CinderDecision(ModelBase): DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: CinderActionAlreadyRemoved, }.get(decision_action, CinderActionNotImplemented) - def get_action_helper(self, *, overriden_action=None, appealed_action=None): + def get_action_helper(self, *, overridden_action=None, appealed_action=None): # Base case when it's a new decision, that wasn't an appeal CinderActionClass = self.get_action_helper_class(self.action) skip_reporter_notify = False @@ -1038,11 +1029,11 @@ class CinderDecision(ModelBase): CinderActionClass = CinderActionTargetAppealRemovalAffirmation # (a reporter appeal doesn't need any alternate CinderAction class) - elif overriden_action in DECISION_ACTIONS.REMOVING: + elif overridden_action in DECISION_ACTIONS.REMOVING: # override on a decision that was a takedown before, and wasn't an appeal if self.action in DECISION_ACTIONS.APPROVING: CinderActionClass = CinderActionOverrideApprove - if self.action == overriden_action: + if self.action == overridden_action: # For an override that is still a takedown we can send the same emails # to the target; but we don't want to notify the reporter again. skip_reporter_notify = True @@ -1059,8 +1050,8 @@ class CinderDecision(ModelBase): """ now = datetime.now() base_criteria = ( - self.date - and self.date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS) + self.action_date + and self.action_date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS) # Can never appeal an original decision that has been appealed and # for which we already have a new decision. In some cases the # appealed decision (new decision id) can be appealed by the author @@ -1187,7 +1178,7 @@ class CinderDecision(ModelBase): 'Missing or invalid cinder_action in activity log details passed to ' 'notify_reviewer_decision' ) - overriden_action = self.action + overridden_action = self.action self.action = DECISION_ACTIONS.for_constant( log_entry.details['cinder_action'] ).value @@ -1201,7 +1192,7 @@ class CinderDecision(ModelBase): 'reasoning': self.notes, 'policy_uuids': [policy.uuid for policy in policies], } - if not overriden_action and ( + if not overridden_action and ( cinder_job := getattr(self, 'cinder_job', None) ): decision_cinder_id = entity_helper.create_job_decision( @@ -1211,11 +1202,12 @@ class CinderDecision(ModelBase): decision_cinder_id = entity_helper.create_decision(**create_decision_kw) with atomic(): self.cinder_id = decision_cinder_id + self.action_date = datetime.now() self.save() self.policies.set(policies) action_helper = self.get_action_helper( - overriden_action=overriden_action, appealed_action=appealed_action + overridden_action=overridden_action, appealed_action=appealed_action ) if cinder_job := getattr(self, 'cinder_job', None): cinder_job.notify_reporters(action_helper) @@ -1249,6 +1241,29 @@ class CinderDecision(ModelBase): }, ) + def process_action(self, overridden_action=None): + """currently only called by decisions from cinder. + see https://mozilla-hub.atlassian.net/browse/AMOENG-1125 + """ + appealed_action = ( + getattr(self.cinder_job.appealed_decisions.first(), 'action', None) + if hasattr(self, 'cinder_job') + else None + ) + + action_helper = self.get_action_helper( + overridden_action=overridden_action, + appealed_action=appealed_action, + ) + if not action_helper.should_hold_action(): + log_entry = action_helper.process_action() + if cinder_job := getattr(self, 'cinder_job', None): + cinder_job.notify_reporters(action_helper) + action_helper.notify_owners(log_entry_id=getattr(log_entry, 'id', None)) + self.update(action_date=datetime.now()) + else: + action_helper.hold_action() + class CinderAppeal(ModelBase): text = models.TextField(blank=False, help_text='The content of the appeal.') diff --git a/src/olympia/abuse/tests/test_models.py b/src/olympia/abuse/tests/test_models.py index 49ae7179ae..6f72d9b271 100644 --- a/src/olympia/abuse/tests/test_models.py +++ b/src/olympia/abuse/tests/test_models.py @@ -31,6 +31,8 @@ from olympia.constants.abuse import ( ILLEGAL_CATEGORIES, ILLEGAL_SUBCATEGORIES, ) +from olympia.constants.promoted import RECOMMENDED +from olympia.core import set_user from olympia.ratings.models import Rating from olympia.reviewers.models import NeedsHumanReview from olympia.versions.models import Version, VersionReviewerFlags @@ -1172,7 +1174,6 @@ class TestCinderJob(TestCase): cinder_job = CinderJob.objects.create(job_id='1234') target = user_factory() AbuseReport.objects.create(user=target, cinder_job=cinder_job) - new_date = datetime(2023, 1, 1) policy_a = CinderPolicy.objects.create(uuid='123-45', name='aaa', text='AAA') policy_b = CinderPolicy.objects.create(uuid='678-90', name='bbb', text='BBB') @@ -1184,13 +1185,11 @@ class TestCinderJob(TestCase): action_mock.return_value = (True, mock.Mock(id=999)) cinder_job.process_decision( decision_cinder_id='12345', - decision_date=new_date, decision_action=DECISION_ACTIONS.AMO_BAN_USER.value, decision_notes='teh notes', policy_ids=['123-45', '678-90'], ) assert cinder_job.decision.cinder_id == '12345' - assert cinder_job.decision.date == new_date assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER assert cinder_job.decision.notes == 'teh notes' assert cinder_job.decision.user == target @@ -1202,7 +1201,6 @@ class TestCinderJob(TestCase): cinder_job = CinderJob.objects.create(job_id='1234') target = user_factory() AbuseReport.objects.create(user=target, cinder_job=cinder_job) - new_date = datetime(2023, 1, 1) parent_policy = CinderPolicy.objects.create( uuid='678-90', name='bbb', text='BBB' ) @@ -1218,13 +1216,11 @@ class TestCinderJob(TestCase): action_mock.return_value = (True, None) cinder_job.process_decision( decision_cinder_id='12345', - decision_date=new_date, decision_action=DECISION_ACTIONS.AMO_BAN_USER.value, decision_notes='teh notes', policy_ids=['123-45', '678-90'], ) assert cinder_job.decision.cinder_id == '12345' - assert cinder_job.decision.date == new_date assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER assert cinder_job.decision.notes == 'teh notes' assert cinder_job.decision.user == target @@ -1237,7 +1233,6 @@ class TestCinderJob(TestCase): cinder_job = CinderJob.objects.create(job_id='1234', target_addon=addon) report = AbuseReport.objects.create(guid=addon.guid, cinder_job=cinder_job) assert not cinder_job.resolvable_in_reviewer_tools - new_date = datetime(2024, 1, 1) responses.add( responses.POST, f'{settings.CINDER_SERVER_URL}create_report', @@ -1247,7 +1242,6 @@ class TestCinderJob(TestCase): cinder_job.process_decision( decision_cinder_id='12345', - decision_date=new_date, decision_action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, decision_notes='blah', policy_ids=[], @@ -1325,7 +1319,7 @@ class TestCinderJob(TestCase): assert 'entity' not in request_body cinder_job.reload() assert cinder_job.decision.action == cinder_action - self.assertCloseToNow(cinder_job.decision.date) + self.assertCloseToNow(cinder_job.decision.action_date) assert list(cinder_job.decision.policies.all()) == policies assert len(mail.outbox) == (2 if expect_target_email else 1) assert mail.outbox[0].to == [abuse_report.reporter.email] @@ -1399,7 +1393,7 @@ class TestCinderJob(TestCase): assert cinder_job.decision.action == ( DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON ) - self.assertCloseToNow(cinder_job.decision.date) + self.assertCloseToNow(cinder_job.decision.action_date) assert list(cinder_job.decision.policies.all()) == policies assert set(cinder_job.pending_rejections.all()) == set( VersionReviewerFlags.objects.filter( @@ -1470,7 +1464,7 @@ class TestCinderJob(TestCase): assert 'entity' not in request_body appeal_job.reload() assert appeal_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON - self.assertCloseToNow(appeal_job.decision.date) + self.assertCloseToNow(appeal_job.decision.action_date) assert list(appeal_job.decision.policies.all()) == policies assert len(mail.outbox) == 1 @@ -1602,7 +1596,7 @@ class TestCinderJob(TestCase): assert request_body['reasoning'] == 'some review text' cinder_job.reload() assert cinder_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON - self.assertCloseToNow(cinder_job.decision.date) + self.assertCloseToNow(cinder_job.decision.action_date) assert list(cinder_job.decision.policies.all()) == policies assert len(mail.outbox) == 2 assert mail.outbox[0].to == [abuse_report.reporter.email] @@ -1797,6 +1791,7 @@ class TestCinderDecisionCanBeAppealed(TestCase): cinder_id='fake_decision_id', action=DECISION_ACTIONS.AMO_APPROVE, addon=self.addon, + action_date=datetime.now(), ) def test_appealed_decision_already_made(self): @@ -1937,6 +1932,7 @@ class TestCinderDecisionCanBeAppealed(TestCase): cinder_id='fake_appeal_decision_id', action=DECISION_ACTIONS.AMO_APPROVE, addon=self.addon, + action_date=datetime.now(), ), ) report = AbuseReport.objects.create( @@ -1970,7 +1966,22 @@ class TestCinderDecisionCanBeAppealed(TestCase): assert self.decision.can_be_appealed( is_reporter=True, abuse_report=initial_report ) - self.decision.update(date=self.days_ago(APPEAL_EXPIRATION_DAYS + 1)) + self.decision.update(action_date=self.days_ago(APPEAL_EXPIRATION_DAYS + 1)) + assert not self.decision.can_be_appealed( + is_reporter=True, abuse_report=initial_report + ) + + def test_reporter_cant_appeal_when_no_action_date(self): + initial_report = AbuseReport.objects.create( + guid=self.addon.guid, + cinder_job=CinderJob.objects.create(decision=self.decision), + reporter=self.reporter, + reason=AbuseReport.REASONS.ILLEGAL, + ) + assert self.decision.can_be_appealed( + is_reporter=True, abuse_report=initial_report + ) + self.decision.update(action_date=None) assert not self.decision.can_be_appealed( is_reporter=True, abuse_report=initial_report ) @@ -2021,6 +2032,7 @@ class TestCinderDecisionCanBeAppealed(TestCase): cinder_id='fake_appeal_decision_id', action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=self.addon, + action_date=datetime.now(), ), ) self.decision.update(appeal_job=appeal_job) @@ -2148,6 +2160,12 @@ class TestCinderPolicy(TestCase): @override_switch('dsa-abuse-reports-review', active=True) @override_switch('dsa-appeals-review', active=True) class TestCinderDecision(TestCase): + def setUp(self): + # It's the webhook's responsibility to do this before calling the + # action. We need it for the ActivityLog creation to work. + self.task_user = user_factory(pk=settings.TASK_USER_ID) + set_user(self.task_user) + def test_get_reference_id(self): decision = CinderDecision() assert decision.get_reference_id() == 'NoClass#None' @@ -2292,7 +2310,7 @@ class TestCinderDecision(TestCase): } ) helper = decision.get_action_helper( - appealed_action=appealed_action, overriden_action=overridden_action + appealed_action=appealed_action, overridden_action=overridden_action ) assert helper.__class__ == ActionClass assert helper.decision == decision @@ -2321,7 +2339,7 @@ class TestCinderDecision(TestCase): } ) helper = decision.get_action_helper( - appealed_action=None, overriden_action=overridden_action + appealed_action=None, overridden_action=overridden_action ) assert helper.reporter_template_path is None assert helper.reporter_appeal_template_path is None @@ -2329,7 +2347,6 @@ class TestCinderDecision(TestCase): assert ActionClass.reporter_appeal_template_path is not None def _test_appeal_as_target(self, *, resolvable_in_reviewer_tools): - user_factory(id=settings.TASK_USER_ID) addon = addon_factory( status=amo.STATUS_DISABLED, file_kw={'is_signed': True, 'status': amo.STATUS_DISABLED}, @@ -2343,7 +2360,7 @@ class TestCinderDecision(TestCase): resolvable_in_reviewer_tools=resolvable_in_reviewer_tools, decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon, ), @@ -2405,7 +2422,7 @@ class TestCinderDecision(TestCase): cinder_job=CinderJob.objects.create( decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon, ), @@ -2443,7 +2460,7 @@ class TestCinderDecision(TestCase): cinder_job=CinderJob.objects.create( decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), # This (target is an add-on, decision is a user ban) shouldn't # be possible but we want to make sure this is handled # explicitly. @@ -2487,7 +2504,7 @@ class TestCinderDecision(TestCase): cinder_job=CinderJob.objects.create( decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_BAN_USER, user=target, ) @@ -2528,7 +2545,7 @@ class TestCinderDecision(TestCase): target_addon=addon, decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, ), @@ -2572,7 +2589,7 @@ class TestCinderDecision(TestCase): target_addon=addon, decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, ), @@ -2615,7 +2632,7 @@ class TestCinderDecision(TestCase): cinder_job = CinderJob.objects.create( decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_APPROVE, addon=addon_factory(), ) @@ -2638,7 +2655,7 @@ class TestCinderDecision(TestCase): cinder_job = CinderJob.objects.create( decision=CinderDecision.objects.create( cinder_id='4815162342-lost', - date=self.days_ago(179), + action_date=self.days_ago(179), action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, ) @@ -2715,7 +2732,7 @@ class TestCinderDecision(TestCase): assert request_body['enforcement_actions_slugs'] == [ cinder_action.api_value ] - self.assertCloseToNow(decision.date) + self.assertCloseToNow(decision.action_date) assert list(decision.policies.all()) == policies assert CinderDecision.objects.count() == 1 assert decision.id @@ -2730,7 +2747,7 @@ class TestCinderDecision(TestCase): assert request_body['enforcement_actions_slugs'] == [ cinder_action.api_value ] - self.assertCloseToNow(decision.date) + self.assertCloseToNow(decision.action_date) assert list(decision.policies.all()) == policies assert CinderDecision.objects.count() == 1 assert decision.id @@ -2995,6 +3012,130 @@ class TestCinderDecision(TestCase): not in mail.outbox[0].body ) + def test_process_action_ban_user_held(self): + user = user_factory(email='superstarops@mozilla.com') + decision = CinderDecision.objects.create( + user=user, action=DECISION_ACTIONS.AMO_BAN_USER + ) + assert decision.action_date is None + decision.process_action() + assert decision.action_date is None + assert not user.reload().banned + assert ( + ActivityLog.objects.filter( + action=amo.LOG.HELD_ACTION_ADMIN_USER_BANNED.id + ).count() + == 1 + ) + + def test_process_action_ban_user(self): + user = user_factory() + decision = CinderDecision.objects.create( + user=user, action=DECISION_ACTIONS.AMO_BAN_USER + ) + assert decision.action_date is None + decision.process_action() + self.assertCloseToNow(decision.action_date) + self.assertCloseToNow(user.reload().banned) + assert ( + ActivityLog.objects.filter(action=amo.LOG.ADMIN_USER_BANNED.id).count() == 1 + ) + + def test_process_action_disable_addon_held(self): + addon = addon_factory() + self.make_addon_promoted(addon, RECOMMENDED, approve_version=True) + decision = CinderDecision.objects.create( + addon=addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON + ) + assert decision.action_date is None + decision.process_action() + assert decision.action_date is None + assert addon.reload().status == amo.STATUS_APPROVED + assert ( + ActivityLog.objects.filter( + action=amo.LOG.HELD_ACTION_FORCE_DISABLE.id + ).count() + == 1 + ) + + def test_process_action_disable_addon(self): + addon = addon_factory() + decision = CinderDecision.objects.create( + addon=addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON + ) + assert decision.action_date is None + decision.process_action() + self.assertCloseToNow(decision.action_date) + assert addon.reload().status == amo.STATUS_DISABLED + assert ActivityLog.objects.filter(action=amo.LOG.FORCE_DISABLE.id).count() == 1 + + def test_process_action_delete_collection_held(self): + collection = collection_factory(author=self.task_user) + decision = CinderDecision.objects.create( + collection=collection, action=DECISION_ACTIONS.AMO_DELETE_COLLECTION + ) + assert decision.action_date is None + decision.process_action() + assert decision.action_date is None + assert not collection.reload().deleted + assert ( + ActivityLog.objects.filter( + action=amo.LOG.HELD_ACTION_COLLECTION_DELETED.id + ).count() + == 1 + ) + + def test_process_action_delete_collection(self): + collection = collection_factory(author=user_factory()) + decision = CinderDecision.objects.create( + collection=collection, action=DECISION_ACTIONS.AMO_DELETE_COLLECTION + ) + assert decision.action_date is None + decision.process_action() + self.assertCloseToNow(decision.action_date) + assert collection.reload().deleted + assert ( + ActivityLog.objects.filter(action=amo.LOG.COLLECTION_DELETED.id).count() + == 1 + ) + + def test_process_action_delete_rating_held(self): + user = user_factory() + addon = addon_factory(users=[user]) + rating = Rating.objects.create( + addon=addon, + user=user, + body='reply', + reply_to=Rating.objects.create( + addon=addon, user=user_factory(), body='sdsd' + ), + ) + decision = CinderDecision.objects.create( + rating=rating, action=DECISION_ACTIONS.AMO_DELETE_RATING + ) + self.make_addon_promoted(rating.addon, RECOMMENDED, approve_version=True) + assert decision.action_date is None + decision.process_action() + assert decision.action_date is None + assert not rating.reload().deleted + assert ( + ActivityLog.objects.filter( + action=amo.LOG.HELD_ACTION_DELETE_RATING.id + ).count() + == 1 + ) + + def test_process_action_delete_rating(self): + rating = Rating.objects.create(addon=addon_factory(), user=user_factory()) + decision = CinderDecision.objects.create( + rating=rating, action=DECISION_ACTIONS.AMO_DELETE_RATING + ) + 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 + @pytest.mark.django_db @pytest.mark.parametrize( diff --git a/src/olympia/abuse/tests/test_tasks.py b/src/olympia/abuse/tests/test_tasks.py index 4369d2b487..754aebeec2 100644 --- a/src/olympia/abuse/tests/test_tasks.py +++ b/src/olympia/abuse/tests/test_tasks.py @@ -434,6 +434,7 @@ def test_addon_appeal_to_cinder_reporter(statsd_incr_mock): cinder_id='4815162342-abc', action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, + action_date=datetime.now(), ) ) abuse_report = AbuseReport.objects.create( @@ -495,6 +496,7 @@ def test_addon_appeal_to_cinder_reporter_exception(statsd_incr_mock): cinder_id='4815162342-abc', action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, + action_date=datetime.now(), ) ) abuse_report = AbuseReport.objects.create( @@ -536,6 +538,7 @@ def test_addon_appeal_to_cinder_authenticated_reporter(): cinder_id='4815162342-abc', action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, + action_date=datetime.now(), ) ) abuse_report = AbuseReport.objects.create( @@ -593,6 +596,7 @@ def test_addon_appeal_to_cinder_authenticated_author(): cinder_id='4815162342-abc', action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon, + action_date=datetime.now(), ) responses.add( responses.POST, diff --git a/src/olympia/abuse/tests/test_utils.py b/src/olympia/abuse/tests/test_utils.py index 99e68a9c89..d8aff81cdb 100644 --- a/src/olympia/abuse/tests/test_utils.py +++ b/src/olympia/abuse/tests/test_utils.py @@ -1,3 +1,5 @@ +from datetime import datetime + from django.conf import settings from django.core import mail from django.urls import reverse @@ -6,9 +8,10 @@ from waffle.testutils import override_switch from olympia import amo from olympia.activity.models import ActivityLog, ActivityLogToken -from olympia.addons.models import Addon +from olympia.addons.models import Addon, AddonUser from olympia.amo.tests import TestCase, addon_factory, collection_factory, user_factory from olympia.constants.abuse import DECISION_ACTIONS +from olympia.constants.promoted import RECOMMENDED from olympia.core import set_user from olympia.ratings.models import Rating @@ -37,22 +40,22 @@ class BaseTestCinderAction: action=DECISION_ACTIONS.AMO_APPROVE, notes="extra note's", addon=addon, + action_date=datetime.now(), ) self.cinder_job = CinderJob.objects.create( job_id='1234', decision=self.decision ) - self.decision.policies.add( - CinderPolicy.objects.create( - uuid='1234', - name='Bad policy', - text='This is bad thing', - parent=CinderPolicy.objects.create( - uuid='p4r3nt', - name='Parent Policy', - text='Parent policy text', - ), - ) + self.policy = CinderPolicy.objects.create( + uuid='1234', + name='Bad policy', + text='This is bad thing', + parent=CinderPolicy.objects.create( + uuid='p4r3nt', + name='Parent Policy', + text='Parent policy text', + ), ) + self.decision.policies.add(self.policy) self.abuse_report_no_auth = AbuseReport.objects.create( reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE, guid=addon.guid, @@ -332,14 +335,18 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase): def _test_ban_user(self): self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER) action = self.ActionClass(self.decision) - assert action.process_action() is None + activity = action.process_action() + assert activity.log == amo.LOG.ADMIN_USER_BANNED + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.user, self.policy] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_BAN_USER, + } self.user.reload() self.assertCloseToNow(self.user.banned) - assert ActivityLog.objects.count() == 1 - activity = ActivityLog.objects.get(action=amo.LOG.ADMIN_USER_BANNED.id) - assert activity.arguments == [self.user] - assert activity.user == self.task_user assert len(mail.outbox) == 0 self.cinder_job.notify_reporters(action) @@ -420,6 +427,43 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase): action.notify_owners() self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.user.name}') + def test_should_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER) + action = self.ActionClass(self.decision) + assert action.should_hold_action() is False + + self.user.update(email='superstarops@mozilla.com') + assert action.should_hold_action() is True + + self.user.update(email='foo@baa') + assert action.should_hold_action() is False + del self.user.groups_list + self.grant_permission(self.user, 'this:thing') + assert action.should_hold_action() is True + + self.user.groups_list = [] + assert action.should_hold_action() is False + addon = addon_factory(users=[self.user]) + assert action.should_hold_action() is False + self.make_addon_promoted(addon, RECOMMENDED, approve_version=True) + assert action.should_hold_action() is True + + self.user.banned = datetime.now() + assert action.should_hold_action() is False + + def test_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER) + action = self.ActionClass(self.decision) + activity = action.hold_action() + assert activity.log == amo.LOG.HELD_ACTION_ADMIN_USER_BANNED + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.user, self.policy] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_BAN_USER, + } + @override_switch('dsa-cinder-forwarded-review', active=True) @override_switch('dsa-appeals-review', active=True) @@ -442,7 +486,7 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase): assert activity.log == amo.LOG.FORCE_DISABLE assert self.addon.reload().status == amo.STATUS_DISABLED assert ActivityLog.objects.count() == 1 - assert activity.arguments == [self.addon] + assert activity.arguments == [self.addon, self.policy] assert activity.user == self.task_user assert len(mail.outbox) == 0 @@ -762,6 +806,30 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase): ) assert 'right to appeal' not in mail_item.body + def test_should_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DISABLE_ADDON) + action = self.ActionClass(self.decision) + assert action.should_hold_action() is False + + self.make_addon_promoted(self.addon, RECOMMENDED, approve_version=True) + assert action.should_hold_action() is True + + self.addon.status = amo.STATUS_DISABLED + assert action.should_hold_action() is False + + def test_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DISABLE_ADDON) + action = self.ActionClass(self.decision) + activity = action.hold_action() + assert activity.log == amo.LOG.HELD_ACTION_FORCE_DISABLE + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.addon, self.policy] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_DISABLE_ADDON, + } + class TestCinderActionCollection(BaseTestCinderAction, TestCase): ActionClass = CinderActionDeleteCollection @@ -788,7 +856,7 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase): assert ActivityLog.objects.count() == 1 activity = ActivityLog.objects.get(action=amo.LOG.COLLECTION_DELETED.id) assert activity == log_entry - assert activity.arguments == [self.collection] + assert activity.arguments == [self.collection, self.policy] assert activity.user == self.task_user assert len(mail.outbox) == 0 @@ -870,6 +938,30 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase): action.notify_owners() self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.collection.name}') + def test_should_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_COLLECTION) + action = self.ActionClass(self.decision) + assert action.should_hold_action() is False + + self.collection.update(author=self.task_user) + assert action.should_hold_action() is True + + self.collection.deleted = True + assert action.should_hold_action() is False + + def test_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_COLLECTION) + action = self.ActionClass(self.decision) + activity = action.hold_action() + assert activity.log == amo.LOG.HELD_ACTION_COLLECTION_DELETED + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.collection, self.policy] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_DELETE_COLLECTION, + } + class TestCinderActionRating(BaseTestCinderAction, TestCase): ActionClass = CinderActionDeleteRating @@ -887,13 +979,21 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase): def _test_delete_rating(self): self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING) action = self.ActionClass(self.decision) - assert action.process_action() is None + activity = action.process_action() + assert activity.log == amo.LOG.DELETE_RATING + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.rating, self.policy, self.rating.addon] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_DELETE_RATING, + 'addon_id': self.rating.addon_id, + 'addon_title': str(self.rating.addon.name), + 'body': self.rating.body, + 'is_flagged': False, + } assert self.rating.reload().deleted - assert ActivityLog.objects.count() == 1 - activity = ActivityLog.objects.get(action=amo.LOG.DELETE_RATING.id) - assert activity.arguments == [self.rating.addon, self.rating] - assert activity.user == self.task_user assert len(mail.outbox) == 0 self.cinder_job.notify_reporters(action) @@ -977,3 +1077,35 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase): self._test_owner_affirmation_email( f'Mozilla Add-ons: "Saying ..." for {self.rating.addon.name}' ) + + def test_should_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING) + action = self.ActionClass(self.decision) + assert action.should_hold_action() is False + + AddonUser.objects.create(addon=self.rating.addon, user=self.rating.user) + assert action.should_hold_action() is False + self.make_addon_promoted(self.rating.addon, RECOMMENDED, approve_version=True) + assert action.should_hold_action() is False + self.rating.update( + reply_to=Rating.objects.create( + addon=self.rating.addon, user=user_factory(), body='original' + ) + ) + assert action.should_hold_action() is True + + self.rating.update(deleted=self.rating.id) + assert action.should_hold_action() is False + + def test_hold_action(self): + self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING) + action = self.ActionClass(self.decision) + activity = action.hold_action() + assert activity.log == amo.LOG.HELD_ACTION_DELETE_RATING + assert ActivityLog.objects.count() == 1 + assert activity.arguments == [self.rating, self.policy, self.rating.addon] + assert activity.user == self.task_user + assert activity.details == { + 'comments': self.decision.notes, + 'cinder_action': DECISION_ACTIONS.AMO_DELETE_RATING, + } diff --git a/src/olympia/abuse/tests/test_views.py b/src/olympia/abuse/tests/test_views.py index 8583f76008..a1bdccd08b 100644 --- a/src/olympia/abuse/tests/test_views.py +++ b/src/olympia/abuse/tests/test_views.py @@ -1270,7 +1270,6 @@ class TestCinderWebhook(TestCase): process_mock.assert_called() process_mock.assert_called_with( decision_cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', - decision_date=datetime(2023, 10, 12, 9, 8, 37, 4789), decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value, decision_notes='some notes', policy_ids=['f73ad527-54ed-430c-86ff-80e15e2a352b'], @@ -1287,7 +1286,6 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_APPROVE, appeal_job=CinderJob.objects.create( @@ -1302,7 +1300,6 @@ class TestCinderWebhook(TestCase): assert process_mock.call_count == 1 process_mock.assert_called_with( decision_cinder_id='76e0006d-1a42-4ec7-9475-148bab1970f1', - decision_date=datetime(2024, 4, 24, 17, 45, 32, 8810), decision_action=DECISION_ACTIONS.AMO_APPROVE.value, decision_notes='still no!', policy_ids=['1c5d711a-78b7-4fc2-bdef-9a33024f5e8b'], @@ -1325,7 +1322,7 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), + action_date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_APPROVE, appeal_job=CinderJob.objects.create( @@ -1340,7 +1337,6 @@ class TestCinderWebhook(TestCase): assert process_mock.call_count == 1 process_mock.assert_called_with( decision_cinder_id='4f18b22c-6078-4934-b395-6a2e01cadf63', - decision_date=datetime(2024, 4, 24, 18, 19, 30, 274623), decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value, decision_notes="fine I'll disable it", policy_ids=[ @@ -1359,7 +1355,7 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), + action_date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_DISABLE_ADDON, appeal_job=CinderJob.objects.create( @@ -1386,7 +1382,7 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), + action_date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_DISABLE_ADDON, appeal_job=CinderJob.objects.create( @@ -1413,7 +1409,7 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), + action_date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_APPROVE, appeal_job=CinderJob.objects.create( @@ -1448,7 +1444,7 @@ class TestCinderWebhook(TestCase): original_cinder_job = CinderJob.objects.get() original_cinder_job.update( decision=CinderDecision.objects.create( - date=datetime(2023, 10, 12, 9, 8, 37, 4789), + action_date=datetime(2023, 10, 12, 9, 8, 37, 4789), cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', action=DECISION_ACTIONS.AMO_APPROVE, appeal_job=CinderJob.objects.create( @@ -2472,7 +2468,7 @@ class TestAppeal(TestCase): decision=CinderDecision.objects.create( cinder_id='my-decision-id', action=DECISION_ACTIONS.AMO_APPROVE, - date=self.days_ago(1), + action_date=self.days_ago(1), addon=self.addon, ), created=self.days_ago(2), @@ -2599,7 +2595,7 @@ class TestAppeal(TestCase): } def test_appeal_approval_anonymous_report_with_email_post_cant_be_appealed(self): - self.cinder_job.decision.update(date=self.days_ago(200)) + self.cinder_job.decision.update(action_date=self.days_ago(200)) self.abuse_report.update(reporter_email='me@example.com') response = self.client.get(self.reporter_appeal_url) assert response.status_code == 200 @@ -2654,7 +2650,7 @@ class TestAppeal(TestCase): assert self.appeal_mock.call_count == 0 def test_appeal_approval_logged_in_report_cant_be_appealed(self): - self.cinder_job.decision.update(date=self.days_ago(200)) + self.cinder_job.decision.update(action_date=self.days_ago(200)) self.user = user_factory() self.abuse_report.update(reporter=self.user) self.client.force_login(self.user) @@ -2720,6 +2716,7 @@ class TestAppeal(TestCase): addon=self.addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON, cinder_id='some-decision-id', + action_date=datetime.now(), ) author_appeal_url = reverse( 'abuse.appeal_author', kwargs={'decision_cinder_id': decision.cinder_id} diff --git a/src/olympia/abuse/utils.py b/src/olympia/abuse/utils.py index 11273e96c5..fa9c51c152 100644 --- a/src/olympia/abuse/utils.py +++ b/src/olympia/abuse/utils.py @@ -51,11 +51,34 @@ class CinderAction: f'{self.valid_targets}' ) + def log_action(self, activity_log_action, *extra_args, extra_details=None): + return log_create( + activity_log_action, + self.target, + *(self.decision.policies.all()), + *extra_args, + details={ + 'comments': self.decision.notes, + 'cinder_action': self.decision.action, + **(extra_details or {}), + }, + ) + + def should_hold_action(self): + """This should return false if the action should be processed immediately, + without further checks, and true if it should be held for further review.""" + return False + def process_action(self): """This method should return an activity log instance for the action, if available.""" raise NotImplementedError + def hold_action(self): + """This method should take no action, but create an activity log instance with + appropriate details.""" + pass + def get_owners(self): """No owner emails will be sent. Override to send owner emails""" return () @@ -239,12 +262,30 @@ class CinderActionBanUser(CinderAction): reporter_template_path = 'abuse/emails/reporter_takedown_user.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' + def should_hold_action(self): + return bool( + not self.target.banned + and ( + self.target.is_staff # mozilla.com + or self.target.groups_list # has any permissions + # owns a high profile add-on + or any( + addon.promoted_group().high_profile + for addon in self.target.addons.all() + ) + ) + ) + def process_action(self): if not self.target.banned: UserProfile.objects.filter( pk=self.target.pk - ).ban_and_disable_related_content() - return None + ).ban_and_disable_related_content(skip_activity_log=True) + return self.log_action(amo.LOG.ADMIN_USER_BANNED) + + def hold_action(self): + if not self.target.banned: + return self.log_action(amo.LOG.HELD_ACTION_ADMIN_USER_BANNED) def get_owners(self): return [self.target] @@ -256,10 +297,22 @@ class CinderActionDisableAddon(CinderAction): reporter_template_path = 'abuse/emails/reporter_takedown_addon.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' + def should_hold_action(self): + return bool( + self.target.status != amo.STATUS_DISABLED + # is a high profile add-on + and self.target.promoted_group().high_profile + ) + def process_action(self): if self.target.status != amo.STATUS_DISABLED: self.target.force_disable(skip_activity_log=True) - return log_create(amo.LOG.FORCE_DISABLE, self.target) + return self.log_action(amo.LOG.FORCE_DISABLE) + return None + + def hold_action(self): + if self.target.status != amo.STATUS_DISABLED: + return self.log_action(amo.LOG.HELD_ACTION_FORCE_DISABLE) return None def get_owners(self): @@ -269,10 +322,19 @@ class CinderActionDisableAddon(CinderAction): class CinderActionRejectVersion(CinderActionDisableAddon): description = 'Add-on version(s) have been rejected' + def should_hold_action(self): + # This action should only be used by reviewer tools, not cinder webhook + # eventually, if add-on becomes non-public do as disable + raise NotImplementedError + def process_action(self): # This action should only be used by reviewer tools, not cinder webhook raise NotImplementedError + def hold_action(self): + # This action should only be used by reviewer tools, not cinder webhook + raise NotImplementedError + class CinderActionRejectVersionDelayed(CinderActionRejectVersion): description = 'Add-on version(s) will be rejected' @@ -295,10 +357,21 @@ class CinderActionDeleteCollection(CinderAction): reporter_template_path = 'abuse/emails/reporter_takedown_collection.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' + def should_hold_action(self): + return ( + # Mozilla-owned collection + not self.target.deleted and self.target.author_id == settings.TASK_USER_ID + ) + def process_action(self): if not self.target.deleted: self.target.delete(clear_slug=False) - return log_create(amo.LOG.COLLECTION_DELETED, self.target) + return self.log_action(amo.LOG.COLLECTION_DELETED) + return None + + def hold_action(self): + if not self.target.deleted: + return self.log_action(amo.LOG.HELD_ACTION_COLLECTION_DELETED) return None def get_owners(self): @@ -311,9 +384,32 @@ class CinderActionDeleteRating(CinderAction): reporter_template_path = 'abuse/emails/reporter_takedown_rating.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' + def should_hold_action(self): + # Developer reply in recommended or partner extensions + return bool( + not self.target.deleted + and self.target.reply_to + and self.target.addon.promoted_group().high_profile_rating + ) + def process_action(self): if not self.target.deleted: - self.target.delete(clear_flags=False) + self.target.delete(skip_activity_log=True, clear_flags=False) + return self.log_action( + amo.LOG.DELETE_RATING, + self.target.addon, + extra_details={ + 'body': str(self.target.body), + 'addon_id': self.target.addon.pk, + 'addon_title': str(self.target.addon.name), + 'is_flagged': self.target.ratingflag_set.exists(), + }, + ) + return None + + def hold_action(self): + if not self.target.deleted: + return self.log_action(amo.LOG.HELD_ACTION_DELETE_RATING, self.target.addon) return None def get_owners(self): diff --git a/src/olympia/abuse/views.py b/src/olympia/abuse/views.py index 3b78363dbe..d04538153b 100644 --- a/src/olympia/abuse/views.py +++ b/src/olympia/abuse/views.py @@ -1,6 +1,5 @@ import hashlib import hmac -from datetime import datetime, timezone from django import forms from django.conf import settings @@ -169,18 +168,6 @@ class CinderInboundPermission: return hmac.compare_digest(header, digest) -def process_datestamp(date_string): - try: - return ( - datetime.fromisoformat(date_string.replace(' ', '')) - .astimezone(timezone.utc) - .replace(tzinfo=None) - ) - except ValueError: - log.warn('Invalid timestamp from cinder webhook %s', date_string) - return datetime.now() - - def filter_enforcement_actions(enforcement_actions, cinder_job): target = cinder_job.target if not target: @@ -253,7 +240,6 @@ def cinder_webhook(request): cinder_job.process_decision( decision_cinder_id=source.get('decision', {}).get('id'), - decision_date=process_datestamp(payload.get('timestamp')), decision_action=enforcement_actions[0], decision_notes=payload.get('notes') or '', policy_ids=policy_ids, diff --git a/src/olympia/constants/activity.py b/src/olympia/constants/activity.py index 2963426fdd..4305428929 100644 --- a/src/olympia/constants/activity.py +++ b/src/olympia/constants/activity.py @@ -1059,6 +1059,39 @@ class DENY_APPEAL_JOB(_LOG): reviewer_review_action = True +class HELD_ACTION_ADMIN_USER_BANNED(_LOG): + id = 193 + format = _('User {user} ban action held for further review.') + short = 'Held user ban' + admin_event = True + + +class HELD_ACTION_DELETE_RATING(_LOG): + """Requires rating.id and add-on objects.""" + + id = 194 + action_class = 'review' + format = _('Review {rating} for {addon} delete held for further review.') + reviewer_format = 'Held {user_responsible}s delete {rating} for {addon}' + admin_event = True + + +class HELD_ACTION_COLLECTION_DELETED(_LOG): + id = 195 + format = _('Collection {collection} deletion held for further review') + admin_event = True + + +class HELD_ACTION_FORCE_DISABLE(_LOG): + id = 196 + reviewer_review_action = True + format = _('{addon} force-disable held for further review') + reviewer_format = 'Held {addon} force-disable by {user_responsible}.' + admin_format = reviewer_format + short = 'Held force disable' + admin_event = True + + 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}) diff --git a/src/olympia/constants/promoted.py b/src/olympia/constants/promoted.py index d376a052e3..f9d90e8703 100644 --- a/src/olympia/constants/promoted.py +++ b/src/olympia/constants/promoted.py @@ -18,10 +18,12 @@ _PromotedSuperClass = namedtuple( 'admin_review', 'badged', # See BADGE_CATEGORIES in frontend too: both need changing 'autograph_signing_states', - 'can_primary_hero', - 'immediate_approval', - 'flag_for_human_review', + 'can_primary_hero', # can be added to a primary hero shelf + 'immediate_approval', # will addon be auto-approved once added + 'flag_for_human_review', # will be add-on be flagged for another review 'can_be_compatible_with_all_fenix_versions', # If addon is promoted for Android + 'high_profile', # the add-on is considered high-profile for review purposes + 'high_profile_rating', # developer replies are considered high-profile ], defaults=( # "Since fields with a default value must come after any fields without @@ -33,10 +35,12 @@ _PromotedSuperClass = namedtuple( False, # admin_review False, # badged {}, # autograph_signing_states - should be a dict of App.short: state - False, # can_primary_hero - can be added to a primary hero shelf - False, # immediate_approval - will addon be auto-approved once added - False, # flag_for_human_review - will be add-on be flagged for another review + False, # can_primary_hero + False, # immediate_approval + False, # flag_for_human_review False, # can_be_compatible_with_all_fenix_versions + False, # high_profile + False, # high_profile_rating ), ) @@ -67,6 +71,8 @@ RECOMMENDED = PromotedClass( }, can_primary_hero=True, can_be_compatible_with_all_fenix_versions=True, + high_profile=True, + high_profile_rating=True, ) # Obsolete, never used in production, only there to prevent us from re-using @@ -89,6 +95,8 @@ LINE = PromotedClass( }, can_primary_hero=True, can_be_compatible_with_all_fenix_versions=True, + high_profile=True, + high_profile_rating=True, ) SPOTLIGHT = PromotedClass( @@ -99,6 +107,7 @@ SPOTLIGHT = PromotedClass( admin_review=True, can_primary_hero=True, immediate_approval=True, + high_profile=True, ) STRATEGIC = PromotedClass( @@ -115,6 +124,7 @@ NOTABLE = PromotedClass( listed_pre_review=True, unlisted_pre_review=True, flag_for_human_review=True, + high_profile=True, ) diff --git a/src/olympia/users/models.py b/src/olympia/users/models.py index dd93be7b82..e08a78f8b7 100644 --- a/src/olympia/users/models.py +++ b/src/olympia/users/models.py @@ -111,7 +111,7 @@ class UserEmailBoundField(forms.boundfield.BoundField): class UserQuerySet(BaseQuerySet): - def ban_and_disable_related_content(self): + def ban_and_disable_related_content(self, *, skip_activity_log=False): """Admin method to ban multiple users and disable the content they produced. @@ -222,7 +222,8 @@ class UserQuerySet(BaseQuerySet): ratings_qs.delete() # And then ban the users. for user in users: - activity.log_create(amo.LOG.ADMIN_USER_BANNED, user) + if not skip_activity_log: + activity.log_create(amo.LOG.ADMIN_USER_BANNED, user) log.info( 'User (%s: <%s>) is being banned.', user,