Hold actions on the back of decisions, rather than process them, if content is high profile (#22766)

* seperate decision from action enforcement

* implement hold actions for BAN_USER

* implement hold actions for DELETE_RATING

* implement hold action for DELETE_COLLECTION

* migration number clash

* implement old actions for DISABLE_ADDON
This commit is contained in:
Andrew Williamson 2024-10-18 11:24:26 +01:00 коммит произвёл GitHub
Родитель 3a7d24badb
Коммит 8b100c94ae
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 545 добавлений и 111 удалений

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

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

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

@ -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.')

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

@ -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(

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

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

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

@ -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,12 +40,12 @@ 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(
self.policy = CinderPolicy.objects.create(
uuid='1234',
name='Bad policy',
text='This is bad thing',
@ -52,7 +55,7 @@ class BaseTestCinderAction:
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,
}

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

@ -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}

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

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

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

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

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

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

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

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

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

@ -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,6 +222,7 @@ class UserQuerySet(BaseQuerySet):
ratings_qs.delete()
# And then ban the users.
for user in users:
if not skip_activity_log:
activity.log_create(amo.LOG.ADMIN_USER_BANNED, user)
log.info(
'User (%s: <%s>) is being banned.',