Re-implement cinder forwards (escalations) as a new job in the content infringement queue (#22582)

* Re-implement cinder forwards (escalations) as a property of the job

* create report in reviewer queue for escalations

* Rename escalations waffle switch to forwarded

* limit notes migrated to first 255 characters

* Retain existing CinderJob instance; create new, as cinder does

* add handle_escalate_action to CELERY_TASK_ROUTES

* add backfill_cinder_escalations command

* extract clearing NHR from CinderJob.resolve_job to test
This commit is contained in:
Andrew Williamson 2024-08-30 15:36:21 +01:00 коммит произвёл GitHub
Родитель 82ed35e1d5
Коммит ccf3fdab41
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
18 изменённых файлов: 844 добавлений и 408 удалений

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

@ -79,7 +79,7 @@ class CinderEntity:
'authorization': f'Bearer {settings.CINDER_API_TOKEN}',
}
def build_report_payload(self, *, report, reporter):
def build_report_payload(self, *, report, reporter, message=''):
generator = self.get_context_generator()
context = next(generator, self.get_empty_context())
if report:
@ -96,9 +96,7 @@ class CinderEntity:
context['relationships'] += [
reporter.get_relationship_data(report, 'amo_reporter_of')
]
message = report.abuse_report.message
else:
message = ''
message = message or report.abuse_report.message
entity_attributes = {**self.get_attributes(), **self.get_extended_attributes()}
return {
'queue_slug': self.queue,
@ -108,7 +106,7 @@ class CinderEntity:
'context': context,
}
def report(self, *, report, reporter):
def report(self, *, report, reporter, message=''):
"""Build the payload and send the report to Cinder API.
Return a job_id that can be used by CinderJob.report() to either get an
@ -117,7 +115,9 @@ class CinderEntity:
# type needs to be defined by subclasses
raise NotImplementedError
url = f'{settings.CINDER_SERVER_URL}create_report'
data = self.build_report_payload(report=report, reporter=reporter)
data = self.build_report_payload(
report=report, reporter=reporter, message=message
)
response = requests.post(url, json=data, headers=self.get_cinder_http_headers())
if response.status_code == 201:
return response.json().get('job_id')
@ -206,6 +206,10 @@ class CinderEntity:
a keyword argument."""
pass
def workflow_recreate(self, *, job):
"""Recreate a job in a queue."""
raise NotImplementedError
class CinderUser(CinderEntity):
type = 'amo_user'
@ -294,9 +298,9 @@ class CinderUnauthenticatedReporter(CinderEntity):
class CinderAddon(CinderEntity):
type = 'amo_addon'
def __init__(self, addon, version=None):
def __init__(self, addon, version_string=None):
self.addon = addon
self.version = version
self.version_string = version_string
self.related_users = self.addon.authors.all()
@property
@ -470,63 +474,103 @@ class CinderAddonHandledByReviewers(CinderAddon):
def queue(cls):
return f'{settings.CINDER_QUEUE_PREFIX}{cls.queue_suffix}'
def flag_for_human_review(self, appeal=False):
def flag_for_human_review(
self, *, reported_versions, appeal=False, forwarded=False
):
from olympia.reviewers.models import NeedsHumanReview
waffle_switch_name = (
'dsa-appeals-review' if appeal else 'dsa-abuse-reports-review'
'dsa-appeals-review'
if appeal
else 'dsa-cinder-forwarded-review'
if forwarded
else 'dsa-abuse-reports-review'
)
if not waffle.switch_is_active(waffle_switch_name):
log.info(
'Not adding %s to review queue despite %s because %s switch is off',
self.addon,
'appeal' if appeal else 'report',
'appeal' if appeal else 'forward' if forwarded else 'report',
waffle_switch_name,
)
return
reason = (
NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL
if appeal
else NeedsHumanReview.REASONS.CINDER_ESCALATION
if forwarded
else NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
)
nhr_object = NeedsHumanReview(
version=self.version, reason=reason, is_active=True
)
if self.version:
if not NeedsHumanReview.objects.filter(
version=self.version, reason=reason, is_active=True
).exists():
nhr_object.save(_no_automatic_activity_log=True)
versions_to_log = [self.version]
else:
versions_to_log = []
else:
versions_to_log = self.addon.set_needs_human_review_on_latest_versions(
reason=reason,
ignore_reviewed=False,
unique_reason=True,
skip_activity_log=True,
version_objs = (
set(
self.addon.versions(manager='unfiltered_for_relations')
.filter(version__in=reported_versions)
.exclude(
needshumanreview__reason=reason,
needshumanreview__is_active=True,
)
.no_transforms()
)
if reported_versions
else set()
)
nhr_object = None
# We need custom save() and post_save to be triggered, so we can't
# optimize this via bulk_create().
for version in version_objs:
nhr_object = NeedsHumanReview(
version=version, reason=reason, is_active=True
)
nhr_object.save(_no_automatic_activity_log=True)
# If we have more versions specified than versions we flagged, flag latest
# to be safe. (Either because there was an unknown version, or a None)
if len(version_objs) != len(reported_versions) or len(reported_versions) == 0:
version_objs = version_objs.union(
self.addon.set_needs_human_review_on_latest_versions(
reason=reason,
ignore_reviewed=False,
unique_reason=True,
skip_activity_log=True,
)
)
if version_objs:
version_objs = sorted(version_objs, key=lambda v: v.id)
# we just need this for get_reason_display
nhr_object = nhr_object or NeedsHumanReview(
version=version_objs[-1],
reason=reason,
is_active=True,
)
if versions_to_log:
activity.log_create(
amo.LOG.NEEDS_HUMAN_REVIEW_CINDER,
*versions_to_log,
*version_objs,
details={'comments': nhr_object.get_reason_display()},
user=core.get_user() or get_task_user(),
)
def post_report(self, job):
if not job.is_appeal:
self.flag_for_human_review(appeal=False)
reported_version = self.version_string
self.flag_for_human_review(
reported_versions={reported_version}, appeal=False
)
# If our report was added to an appeal job (i.e. an appeal was ongoing,
# and a report was made against the add-on), don't flag the add-on for
# human review again: we should already have one because of the appeal.
def appeal(self, *args, **kwargs):
self.flag_for_human_review(appeal=True)
self.flag_for_human_review(reported_versions=set(), appeal=True)
return super().appeal(*args, **kwargs)
def workflow_recreate(self, *, job):
reported_versions = set(
job.abusereport_set.values_list('addon_version', flat=True)
)
notes = job.decision.notes if job.decision else ''
self.flag_for_human_review(reported_versions=reported_versions, forwarded=True)
return self.report(report=None, reporter=None, message=notes)
class CinderReport(CinderEntity):
type = 'amo_report'

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

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
import olympia.core.logger
from olympia.abuse.models import CinderDecision
from olympia.abuse.tasks import handle_escalate_action
from olympia.constants.abuse import DECISION_ACTIONS
class Command(BaseCommand):
log = olympia.core.logger.getLogger('z.abuse')
def handle(self, *args, **options):
qs = CinderDecision.objects.filter(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
cinder_job__forwarded_to_job__isnull=True,
cinder_job__isnull=False,
)
for decision in qs:
handle_escalate_action.delay(job_pk=decision.cinder_job.pk)

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

@ -0,0 +1,16 @@
# Generated by Django 4.2.15 on 2024-08-23 12:36
from django.db import migrations
from olympia.core.db.migrations import RenameWaffleSwitch
class Migration(migrations.Migration):
dependencies = [
('abuse', '0036_alter_abusereport_appellant_job_cinderappealtext'),
]
operations = [
RenameWaffleSwitch('dsa-cinder-escalations-review', 'dsa-cinder-forwarded-review'),
]

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

@ -0,0 +1,29 @@
# Generated by Django 4.2.15 on 2024-08-27 17:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('abuse', '0037_auto_20240823_1236'),
]
operations = [
migrations.AddField(
model_name='cinderjob',
name='forwarded_to_job',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='forwarded_from_jobs', to='abuse.cinderjob'),
),
migrations.AlterField(
model_name='cinderdecision',
name='action',
field=models.PositiveSmallIntegerField(choices=[(1, 'User ban'), (2, 'Add-on disable'), (3, 'Forward add-on to reviewers'), (5, 'Rating delete'), (6, 'Collection delete'), (7, 'Approved (no action)'), (8, 'Add-on version reject'), (9, 'Add-on version delayed reject warning'), (10, 'Approved (new version approval)'), (11, 'Invalid report, so ignored'), (12, 'Content already removed (no action)')]),
),
migrations.AlterField(
model_name='cinderpolicy',
name='default_cinder_action',
field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'User ban'), (2, 'Add-on disable'), (3, 'Forward add-on to reviewers'), (5, 'Rating delete'), (6, 'Collection delete'), (7, 'Approved (no action)'), (8, 'Add-on version reject'), (9, 'Add-on version delayed reject warning'), (10, 'Approved (new version approval)'), (11, 'Invalid report, so ignored'), (12, 'Content already removed (no action)')], null=True),
),
]

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

@ -55,10 +55,7 @@ class CinderJobQuerySet(BaseQuerySet):
return self.filter(target_addon=addon).order_by('-pk')
def unresolved(self):
return self.filter(
models.Q(decision__isnull=True)
| models.Q(decision__action__in=tuple(DECISION_ACTIONS.UNRESOLVED.values))
)
return self.filter(decision__isnull=True)
def resolvable_in_reviewer_tools(self):
return self.filter(resolvable_in_reviewer_tools=True)
@ -89,6 +86,12 @@ class CinderJob(ModelBase):
related_name='cinder_job',
)
resolvable_in_reviewer_tools = models.BooleanField(default=None, null=True)
forwarded_to_job = models.ForeignKey(
to='self',
null=True,
on_delete=models.SET_NULL,
related_name='forwarded_from_jobs',
)
objects = CinderJobManager()
@ -129,17 +132,10 @@ class CinderJob(ModelBase):
cls, target, *, resolved_in_reviewer_tools, addon_version_string=None
):
if isinstance(target, Addon):
version = (
addon_version_string
and target.versions(manager='unfiltered_for_relations')
.filter(version=addon_version_string)
.no_transforms()
.first()
)
if resolved_in_reviewer_tools:
return CinderAddonHandledByReviewers(target, version)
return CinderAddonHandledByReviewers(target, addon_version_string)
else:
return CinderAddon(target, version)
return CinderAddon(target, addon_version_string)
elif isinstance(target, UserProfile):
return CinderUser(target)
elif isinstance(target, Rating):
@ -235,6 +231,20 @@ class CinderJob(ModelBase):
is_appeal=True,
)
def handle_job_recreated(self, new_job_id):
new_job, _ = CinderJob.objects.update_or_create(
job_id=new_job_id,
defaults={
'resolvable_in_reviewer_tools': True,
'target_addon': self.target_addon,
},
)
# Update our fks to connected objects
AbuseReport.objects.filter(cinder_job=self).update(cinder_job=new_job)
AbuseReport.objects.filter(appellant_job=self).update(appellant_job=new_job)
CinderDecision.objects.filter(appeal_job=self).update(appeal_job=new_job)
self.update(forwarded_to_job=new_job)
def process_decision(
self,
*,
@ -270,11 +280,7 @@ class CinderJob(ModelBase):
],
},
)
self.update(
decision=cinder_decision,
resolvable_in_reviewer_tools=self.resolvable_in_reviewer_tools
or decision_action == DECISION_ACTIONS.AMO_ESCALATE_ADDON,
)
self.update(decision=cinder_decision)
policies = CinderPolicy.objects.filter(
uuid__in=policy_ids
).without_parents_if_their_children_are_present()
@ -290,8 +296,6 @@ class CinderJob(ModelBase):
def resolve_job(self, *, log_entry):
"""This is called for reviewer tools originated decisions.
See process_decision for cinder originated decisions."""
from olympia.reviewers.models import NeedsHumanReview
abuse_report_or_decision = (
self.appealed_decisions.first() or self.abusereport_set.first()
)
@ -302,10 +306,6 @@ class CinderJob(ModelBase):
abuse_report_or_decision.target,
resolved_in_reviewer_tools=self.resolvable_in_reviewer_tools,
)
was_escalated = (
self.decision
and self.decision.action == DECISION_ACTIONS.AMO_ESCALATE_ADDON
)
cinder_decision = self.decision or CinderDecision(
addon=abuse_report_or_decision.addon,
@ -328,41 +328,41 @@ class CinderJob(ModelBase):
else:
self.pending_rejections.clear()
if cinder_decision.addon_id:
# We don't want to clear a NeedsHumanReview caused by a job that
# isn't resolved yet, but there is no link between NHR and jobs.
# So for each possible reason, we look if there are unresolved jobs
# and only clear NHR for that reason if there aren't any jobs left.
base_unresolved_jobs_qs = (
self.__class__.objects.for_addon(cinder_decision.addon)
.unresolved()
.resolvable_in_reviewer_tools()
)
if was_escalated:
has_unresolved_jobs_with_similar_reason = (
base_unresolved_jobs_qs.filter(
decision__action=DECISION_ACTIONS.AMO_ESCALATE_ADDON
).exists()
)
reason = NeedsHumanReview.REASONS.CINDER_ESCALATION
elif self.is_appeal:
has_unresolved_jobs_with_similar_reason = (
base_unresolved_jobs_qs.filter(
appealed_decisions__isnull=False
).exists()
)
reason = NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL
else:
# If the job we're resolving was not an appeal or escalation
# then all abuse reports are considered dealt with.
has_unresolved_jobs_with_similar_reason = None
reason = NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
if not has_unresolved_jobs_with_similar_reason:
NeedsHumanReview.objects.filter(
version__addon_id=cinder_decision.addon_id,
is_active=True,
reason=reason,
).update(is_active=False)
cinder_decision.addon.update_all_due_dates()
self.clear_needs_human_review_flags()
def clear_needs_human_review_flags(self):
from olympia.reviewers.models import NeedsHumanReview
# We don't want to clear a NeedsHumanReview caused by a job that
# isn't resolved yet, but there is no link between NHR and jobs.
# So for each possible reason, we look if there are unresolved jobs
# and only clear NHR for that reason if there aren't any jobs left.
addon = self.decision.addon
base_unresolved_jobs_qs = (
self.__class__.objects.for_addon(addon)
.unresolved()
.resolvable_in_reviewer_tools()
)
if self.forwarded_from_jobs.exists():
has_unresolved_jobs_with_similar_reason = base_unresolved_jobs_qs.filter(
forwarded_from_jobs__isnull=False
).exists()
reason = NeedsHumanReview.REASONS.CINDER_ESCALATION
elif self.is_appeal:
has_unresolved_jobs_with_similar_reason = base_unresolved_jobs_qs.filter(
appealed_decisions__isnull=False
).exists()
reason = NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL
else:
# If the job we're resolving was not an appeal or escalation
# then all abuse reports are considered dealt with.
has_unresolved_jobs_with_similar_reason = None
reason = NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION
if not has_unresolved_jobs_with_similar_reason:
NeedsHumanReview.objects.filter(
version__addon_id=addon.id, is_active=True, reason=reason
).update(is_active=False)
addon.update_all_due_dates()
class AbuseReportQuerySet(BaseQuerySet):
@ -678,6 +678,7 @@ class AbuseReport(ModelBase):
)
cinder_job = models.ForeignKey(CinderJob, null=True, on_delete=models.SET_NULL)
reporter_appeal_date = models.DateTimeField(default=None, null=True)
# The appeal from the reporter of this report, if they have appealed
appellant_job = models.ForeignKey(
CinderJob,
null=True,

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

@ -189,3 +189,17 @@ def sync_cinder_policies():
raise
else:
statsd.incr('abuse.tasks.sync_cinder_policies.success')
@task
@use_primary_db
def handle_escalate_action(*, job_pk):
old_job = CinderJob.objects.get(id=job_pk)
entity_helper = CinderJob.get_entity_helper(
old_job.target,
addon_version_string=None,
resolved_in_reviewer_tools=True,
)
job_id = entity_helper.workflow_recreate(job=old_job)
old_job.handle_job_recreated(new_job_id=job_id)

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

@ -46,13 +46,13 @@ from ..cinder import (
class BaseTestCinderCase:
cinder_class = None # Override in child classes
CinderClass = None # Override in child classes
expected_queue_suffix = None # Override in child classes
expected_queries_for_report = -1 # Override in child classes
def test_queue(self):
target = self._create_dummy_target()
cinder_entity = self.cinder_class(target)
cinder_entity = self.CinderClass(target)
assert cinder_entity.queue_suffix == self.expected_queue_suffix
assert (
cinder_entity.queue
@ -106,7 +106,7 @@ class BaseTestCinderCase:
# loaded before.
abuse_report.reload()
report = CinderReport(abuse_report)
cinder_instance = self.cinder_class(abuse_report.target)
cinder_instance = self.CinderClass(abuse_report.target)
with self.assertNumQueries(self.expected_queries_for_report):
assert cinder_instance.report(report=report, reporter=None) == '1234-xyz'
assert (
@ -132,7 +132,7 @@ class BaseTestCinderCase:
def _test_appeal(self, appealer, cinder_instance=None):
fake_decision_id = 'decision-id-to-appeal-666'
cinder_instance = cinder_instance or self.cinder_class(
cinder_instance = cinder_instance or self.CinderClass(
self._create_dummy_target()
)
@ -170,14 +170,14 @@ class BaseTestCinderCase:
self._test_appeal(CinderUnauthenticatedReporter('itsme', 'm@r.io'))
def test_get_str(self):
instance = self.cinder_class(self._create_dummy_target())
instance = self.CinderClass(self._create_dummy_target())
assert instance.get_str(123) == '123'
assert instance.get_str(None) == ''
assert instance.get_str(' ') == ''
class TestCinderAddon(BaseTestCinderCase, TestCase):
cinder_class = CinderAddon
CinderClass = CinderAddon
# 2 queries expected:
# - Authors (can't use the listed_authors transformer, we want non-listed as well,
# and we have custom limits for batch-sending relationships)
@ -190,7 +190,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
def test_queue_theme(self):
target = self._create_dummy_target(type=amo.ADDON_STATICTHEME)
cinder_entity = self.cinder_class(target)
cinder_entity = self.CinderClass(target)
expected_queue_suffix = 'themes'
assert cinder_entity.queue_suffix == expected_queue_suffix
assert (
@ -208,7 +208,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
version_kw={'release_notes': 'Søme release notes'},
)
message = ' bad addon!'
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
data = cinder_addon.build_report_payload(
@ -373,7 +373,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
)
self.make_addon_promoted(addon, group=RECOMMENDED)
message = ' bad addon!'
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
data = cinder_addon.build_report_payload(
@ -440,7 +440,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
)
self.make_addon_promoted(addon, group=NOTABLE)
message = ' bad addon!'
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
data = cinder_addon.build_report_payload(
@ -506,7 +506,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
author = user_factory()
addon = self._create_dummy_target(users=[author])
message = '@bad addon!'
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
data = cinder_addon.build_report_payload(
@ -625,7 +625,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
def test_build_report_payload_with_author_and_reporter_being_the_same(self):
user = user_factory()
addon = self._create_dummy_target(users=[user])
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
message = 'self reporting! '
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
@ -710,7 +710,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
)
(p0, p1) = list(addon.previews.all())
Preview.objects.create(addon=addon, position=5) # No file, ignored
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
message = ' report with images '
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
@ -812,7 +812,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
VersionPreview.objects.create(
version=addon.current_version, position=5
) # No file, ignored
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
message = 'report with images'
encoded_message = cinder_addon.get_str(message)
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
@ -882,7 +882,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
addon = self._create_dummy_target()
for _ in range(0, 6):
addon.authors.add(user_factory())
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
message = 'report for lots of relationships'
abuse_report = AbuseReport.objects.create(guid=addon.guid, message=message)
data = cinder_addon.build_report_payload(
@ -960,7 +960,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
addon = self._create_dummy_target()
for _ in range(0, 6):
addon.authors.add(user_factory())
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
responses.add(
responses.POST,
@ -1064,7 +1064,7 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
addon = self._create_dummy_target()
for _ in range(0, 6):
addon.authors.add(user_factory())
cinder_addon = self.cinder_class(addon)
cinder_addon = self.CinderClass(addon)
responses.add(
responses.POST,
@ -1078,8 +1078,9 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
@override_switch('dsa-abuse-reports-review', active=True)
@override_switch('dsa-appeals-review', active=True)
@override_switch('dsa-cinder-forwarded-review', active=True)
class TestCinderAddonHandledByReviewers(TestCinderAddon):
cinder_class = CinderAddonHandledByReviewers
CinderClass = CinderAddonHandledByReviewers
# For rendering the payload to Cinder like CinderAddon:
# - 1 Fetch Addon authors
# - 2 Fetch Promoted Addon
@ -1089,14 +1090,14 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
def test_queue(self):
super().test_queue()
# For this class the property should be guaranteed to be static.
assert self.cinder_class.queue == 'amo-env-addon-infringement'
assert self.CinderClass.queue == 'amo-env-addon-infringement'
def test_queue_theme(self):
# Contrary to reports handled by Cinder moderators, for reports handled
# by AMO reviewers the queue should remain the same regardless of the
# addon-type.
target = self._create_dummy_target(type=amo.ADDON_STATICTHEME)
cinder_entity = self.cinder_class(target)
cinder_entity = self.CinderClass(target)
assert cinder_entity.queue_suffix == self.expected_queue_suffix
assert (
cinder_entity.queue
@ -1104,7 +1105,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
)
def setUp(self):
user_factory(id=settings.TASK_USER_ID)
self.task_user = user_factory(id=settings.TASK_USER_ID)
def test_report(self):
addon = self._create_dummy_target()
@ -1119,7 +1120,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
# Adding the NHR is done by post_report().
assert addon.current_version.needshumanreview_set.count() == 0
job = CinderJob.objects.create(job_id='1234-xyz')
cinder_instance = self.cinder_class(addon)
cinder_instance = self.CinderClass(addon)
with self.assertNumQueries(13):
# - 1 Fetch Cinder Decision
# - 2 Fetch Version
@ -1174,7 +1175,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
guid=addon.guid, addon_version=other_version.version
)
report = CinderReport(abuse_report)
cinder_instance = self.cinder_class(addon, other_version)
cinder_instance = self.CinderClass(addon, other_version)
assert cinder_instance.report(report=report, reporter=None)
job = CinderJob.objects.create(job_id='1234-xyz')
assert not addon.current_version.needshumanreview_set.exists()
@ -1192,7 +1193,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
addon = self._create_dummy_target()
addon.current_version.file.update(is_signed=True)
self._test_appeal(
CinderUnauthenticatedReporter('itsme', 'm@r.io'), self.cinder_class(addon)
CinderUnauthenticatedReporter('itsme', 'm@r.io'), self.CinderClass(addon)
)
assert (
addon.current_version.needshumanreview_set.get().reason
@ -1202,7 +1203,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
def test_appeal_logged_in(self):
addon = self._create_dummy_target()
addon.current_version.file.update(is_signed=True)
self._test_appeal(CinderUser(user_factory()), self.cinder_class(addon))
self._test_appeal(CinderUser(user_factory()), self.CinderClass(addon))
assert (
addon.current_version.needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL
@ -1216,7 +1217,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
# etc since the waffle switch is off. So we're back to the same number of
# queries made by the reports that go to Cinder.
self.expected_queries_for_report = TestCinderAddon.expected_queries_for_report
self._test_appeal(CinderUser(user_factory()), self.cinder_class(addon))
self._test_appeal(CinderUser(user_factory()), self.CinderClass(addon))
assert addon.current_version.needshumanreview_set.count() == 0
def test_report_with_ongoing_appeal(self):
@ -1234,15 +1235,16 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
# count more predictable.
waffle.switch_is_active('dsa-abuse-reports-review')
self._test_report(addon)
cinder_instance = self.cinder_class(addon)
cinder_instance = self.CinderClass(addon)
cinder_instance.post_report(job)
# The add-on does not get flagged again while the appeal is ongoing.
assert addon.current_version.needshumanreview_set.count() == 0
def test_report_with_ongoing_escalated_appeal(self):
def test_report_with_ongoing_forwarded_appeal(self):
addon = self._create_dummy_target()
addon.current_version.file.update(is_signed=True)
job = CinderJob.objects.create(job_id='1234-xyz')
CinderJob.objects.create(forwarded_to_job=job)
job.appealed_decisions.add(
CinderDecision.objects.create(
addon=addon,
@ -1250,20 +1252,11 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
action=DECISION_ACTIONS.AMO_REJECT_VERSION_ADDON,
)
)
# Job we're attaching the report does have a decision but it's an
# escalation so it's still considered ongoing/open.
job.update(
decision=CinderDecision.objects.create(
addon=addon,
cinder_id='1234-escalation',
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
)
)
# Trigger switch_is_active to ensure it's cached to make db query
# count more predictable.
waffle.switch_is_active('dsa-abuse-reports-review')
self._test_report(addon)
cinder_instance = self.cinder_class(addon)
cinder_instance = self.CinderClass(addon)
cinder_instance.post_report(job)
# The add-on does not get flagged again while the appeal is ongoing.
assert addon.current_version.needshumanreview_set.count() == 0
@ -1284,7 +1277,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
json={'error': 'reason'},
status=400,
)
cinder_instance = self.cinder_class(target)
cinder_instance = self.CinderClass(target)
assert (
cinder_instance.create_decision(
action=DECISION_ACTIONS.AMO_REJECT_VERSION_ADDON.api_value,
@ -1326,7 +1319,7 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
json={'error': 'reason'},
status=400,
)
cinder_instance = self.cinder_class(target)
cinder_instance = self.CinderClass(target)
assert (
cinder_instance.create_job_decision(
job_id=job.job_id,
@ -1368,12 +1361,165 @@ class TestCinderAddonHandledByReviewers(TestCinderAddon):
json={'error': 'reason'},
status=400,
)
cinder_instance = self.cinder_class(target)
cinder_instance = self.CinderClass(target)
assert cinder_instance.close_job(job_id=job_id) == job_id
def test_workflow_recreate(self):
addon = self._create_dummy_target()
listed_version = addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
ActivityLog.objects.all().delete()
cinder_instance = self.CinderClass(addon)
cinder_job = CinderJob.objects.create(target_addon=addon, job_id='1')
AbuseReport.objects.create(
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
guid=addon.guid,
cinder_job=cinder_job,
reporter_email='email@domain.com',
)
AbuseReport.objects.create(
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
guid=addon.guid,
cinder_job=cinder_job,
reporter=user_factory(),
)
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '2'},
status=201,
)
assert cinder_instance.workflow_recreate(job=cinder_job) == '2'
assert addon.reload().status == amo.STATUS_APPROVED
assert (
listed_version.reload().needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert (
unlisted_version.reload().needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.filter(
action=amo.LOG.NEEDS_HUMAN_REVIEW_CINDER.id
).get()
assert activity.arguments == [listed_version, unlisted_version]
assert activity.user == self.task_user
# but if we have a version specified, we flag that version
NeedsHumanReview.objects.all().delete()
other_version = version_factory(
addon=addon, file_kw={'status': amo.STATUS_DISABLED, 'is_signed': True}
)
assert not other_version.due_date
ActivityLog.objects.all().delete()
cinder_job.abusereport_set.update(addon_version=other_version.version)
cinder_instance.workflow_recreate(job=cinder_job)
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
other_version.reload()
assert other_version.due_date
assert (
other_version.needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.get(action=amo.LOG.NEEDS_HUMAN_REVIEW_CINDER.id)
assert activity.arguments == [other_version]
assert activity.user == self.task_user
def test_workflow_recreate_no_versions_to_flag(self):
addon = self._create_dummy_target()
listed_version = addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
NeedsHumanReview.objects.create(
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION, version=listed_version
)
NeedsHumanReview.objects.create(
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION, version=unlisted_version
)
assert NeedsHumanReview.objects.count() == 2
ActivityLog.objects.all().delete()
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '2'},
status=201,
)
cinder_instance = self.CinderClass(addon)
cinder_job = CinderJob.objects.create(target_addon=addon, job_id='1')
assert cinder_instance.workflow_recreate(job=cinder_job) == '2'
assert NeedsHumanReview.objects.count() == 2
assert ActivityLog.objects.count() == 0
@override_switch('dsa-cinder-forwarded-review', active=False)
def test_workflow_recreate_waffle_switch_off(self):
# Escalation when the waffle switch is off is essentially a no-op on
# AMO side.
addon = self._create_dummy_target()
listed_version = addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
ActivityLog.objects.all().delete()
cinder_instance = self.CinderClass(addon)
cinder_job = CinderJob.objects.create(target_addon=addon)
AbuseReport.objects.create(
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
guid=addon.guid,
cinder_job=cinder_job,
reporter_email='email@domain.com',
)
AbuseReport.objects.create(
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
guid=addon.guid,
cinder_job=cinder_job,
reporter=user_factory(),
)
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '2'},
status=201,
)
assert cinder_instance.workflow_recreate(job=cinder_job) == '2'
assert addon.reload().status == amo.STATUS_APPROVED
assert not listed_version.reload().needshumanreview_set.exists()
assert not listed_version.due_date
assert not unlisted_version.reload().needshumanreview_set.exists()
assert not unlisted_version.due_date
assert ActivityLog.objects.count() == 0
other_version = version_factory(
addon=addon, file_kw={'status': amo.STATUS_DISABLED, 'is_signed': True}
)
assert not other_version.due_date
ActivityLog.objects.all().delete()
cinder_job.abusereport_set.update(addon_version=other_version.version)
cinder_instance.workflow_recreate(job=cinder_job)
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
other_version.reload()
assert not other_version.due_date
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
class TestCinderUser(BaseTestCinderCase, TestCase):
cinder_class = CinderUser
CinderClass = CinderUser
# 2 queries expected:
# - Related add-ons
# - Number of listed add-ons
@ -1391,7 +1537,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
homepage='http://home.example.com',
)
message = ' bad person!'
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
encoded_message = cinder_user.get_str(message)
abuse_report = AbuseReport.objects.create(user=user, message=message)
@ -1544,7 +1690,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
def test_build_report_payload_with_author_and_reporter_being_the_same(self):
user = self._create_dummy_target()
addon = addon_factory(users=[user])
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
message = 'I dont like this guy'
encoded_message = cinder_user.get_str(message)
abuse_report = AbuseReport.objects.create(user=user, message=message)
@ -1620,7 +1766,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
def test_build_report_payload_addon_author(self):
user = self._create_dummy_target()
addon = addon_factory(users=[user])
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
message = '@bad person!'
encoded_message = cinder_user.get_str(message)
abuse_report = AbuseReport.objects.create(user=user, message=message)
@ -1764,7 +1910,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
user.update(picture_type='image/png')
message = '=bad person!'
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
encoded_message = cinder_user.get_str(message)
abuse_report = AbuseReport.objects.create(user=user, message=message)
@ -1834,7 +1980,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
user = self._create_dummy_target()
for _ in range(0, 6):
user.addons.add(addon_factory())
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
message = 'report for lots of relationships'
abuse_report = AbuseReport.objects.create(user=user, message=message)
data = cinder_user.build_report_payload(
@ -1920,7 +2066,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
user = self._create_dummy_target()
for _ in range(0, 6):
user.addons.add(addon_factory())
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
responses.add(
responses.POST,
@ -2040,7 +2186,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
user = self._create_dummy_target()
for _ in range(0, 6):
user.addons.add(addon_factory())
cinder_user = self.cinder_class(user)
cinder_user = self.CinderClass(user)
responses.add(
responses.POST,
@ -2053,7 +2199,7 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
class TestCinderRating(BaseTestCinderCase, TestCase):
cinder_class = CinderRating
CinderClass = CinderRating
expected_queries_for_report = 1 # For the author
expected_queue_suffix = 'ratings'
@ -2068,7 +2214,7 @@ class TestCinderRating(BaseTestCinderCase, TestCase):
def test_build_report_payload(self):
rating = self._create_dummy_target()
cinder_rating = self.cinder_class(rating)
cinder_rating = self.CinderClass(rating)
message = '-bad rating!'
encoded_message = cinder_rating.get_str(message)
abuse_report = AbuseReport.objects.create(rating=rating, message=message)
@ -2134,7 +2280,7 @@ class TestCinderRating(BaseTestCinderCase, TestCase):
def test_build_report_payload_with_author_and_reporter_being_the_same(self):
rating = self._create_dummy_target()
user = rating.user
cinder_rating = self.cinder_class(rating)
cinder_rating = self.CinderClass(rating)
message = '@my own words!'
encoded_message = cinder_rating.get_str(message)
abuse_report = AbuseReport.objects.create(rating=rating, message=message)
@ -2213,7 +2359,7 @@ class TestCinderRating(BaseTestCinderCase, TestCase):
rating = Rating.objects.create(
addon=self.addon, user=addon_author, reply_to=original_rating
)
cinder_rating = self.cinder_class(rating)
cinder_rating = self.CinderClass(rating)
message = '-bad reply!'
encoded_message = cinder_rating.get_str(message)
abuse_report = AbuseReport.objects.create(rating=rating, message=message)
@ -2294,7 +2440,7 @@ class TestCinderRating(BaseTestCinderCase, TestCase):
class TestCinderCollection(BaseTestCinderCase, TestCase):
cinder_class = CinderCollection
CinderClass = CinderCollection
expected_queries_for_report = 1 # For the author
expected_queue_suffix = 'collections'
@ -2318,7 +2464,7 @@ class TestCinderCollection(BaseTestCinderCase, TestCase):
def test_build_report_payload(self):
collection = self._create_dummy_target()
cinder_collection = self.cinder_class(collection)
cinder_collection = self.CinderClass(collection)
message = '@bad collection!'
encoded_message = cinder_collection.get_str(message)
abuse_report = AbuseReport.objects.create(
@ -2388,7 +2534,7 @@ class TestCinderCollection(BaseTestCinderCase, TestCase):
def test_build_report_payload_with_author_and_reporter_being_the_same(self):
collection = self._create_dummy_target()
cinder_collection = self.cinder_class(collection)
cinder_collection = self.CinderClass(collection)
user = collection.author
message = '=Collect me!'
encoded_message = cinder_collection.get_str(message)
@ -2466,14 +2612,14 @@ class TestCinderCollection(BaseTestCinderCase, TestCase):
class TestCinderReport(TestCase):
cinder_class = CinderReport
CinderClass = CinderReport
def test_reason_in_attributes(self):
abuse_report = AbuseReport.objects.create(
guid=addon_factory().guid,
reason=AbuseReport.REASONS.POLICY_VIOLATION,
)
assert self.cinder_class(abuse_report).get_attributes() == {
assert self.CinderClass(abuse_report).get_attributes() == {
'id': str(abuse_report.pk),
'created': str(abuse_report.created),
'locale': None,
@ -2488,7 +2634,7 @@ class TestCinderReport(TestCase):
abuse_report = AbuseReport.objects.create(
guid=addon_factory().guid, application_locale='en_US'
)
assert self.cinder_class(abuse_report).get_attributes() == {
assert self.CinderClass(abuse_report).get_attributes() == {
'id': str(abuse_report.pk),
'created': str(abuse_report.created),
'locale': 'en_US',
@ -2506,7 +2652,7 @@ class TestCinderReport(TestCase):
illegal_category=ILLEGAL_CATEGORIES.ANIMAL_WELFARE,
illegal_subcategory=ILLEGAL_SUBCATEGORIES.OTHER,
)
assert self.cinder_class(abuse_report).get_attributes() == {
assert self.CinderClass(abuse_report).get_attributes() == {
'id': str(abuse_report.pk),
'created': str(abuse_report.created),
'locale': None,

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

@ -0,0 +1,79 @@
from django.conf import settings
from django.core.management import call_command
import pytest
import responses
from olympia.amo.tests import addon_factory
from olympia.constants.abuse import DECISION_ACTIONS
from ..models import AbuseReport, CinderDecision, CinderJob
@pytest.mark.django_db
def test_backfill_cinder_escalations():
addon = addon_factory()
job_with_reports = CinderJob.objects.create(
job_id='1',
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon
),
)
abuse = AbuseReport.objects.create(guid=addon.guid, cinder_job=job_with_reports)
appeal_job = CinderJob.objects.create(
job_id='2',
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon
),
)
appealled_decision = CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon, appeal_job=appeal_job
)
# And some jobs/decisions that should be skipped:
# decision that wasn't an escalation (or isn't any longer)
CinderJob.objects.create(
job_id='3',
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE, addon=addon
),
)
# decision without an associated cinder job (shouldn't occur, but its handled)
CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon
)
# decision that already has a forwarded job created, so we don't need to backfill
CinderJob.objects.create(
job_id='4',
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon
),
forwarded_to_job=CinderJob.objects.create(job_id='5'),
)
assert CinderJob.objects.count() == 5
assert CinderDecision.objects.count() == 6
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '6'},
status=201,
)
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '7'},
status=201,
)
call_command('backfill_cinder_escalations')
assert CinderJob.objects.count() == 7
assert CinderDecision.objects.count() == 6
new_job_with_reports = job_with_reports.reload().forwarded_to_job
assert new_job_with_reports
assert new_job_with_reports.resolvable_in_reviewer_tools is True
assert abuse.reload().cinder_job == new_job_with_reports
new_appeal_job = appeal_job.reload().forwarded_to_job
assert new_appeal_job
assert new_appeal_job.resolvable_in_reviewer_tools is True
assert appealled_decision.reload().appeal_job == new_appeal_job

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

@ -591,15 +591,8 @@ class TestCinderJobManager(TestCase):
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, addon=addon
),
)
escalated_job = CinderJob.objects.create(
job_id='3',
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon
),
)
qs = CinderJob.objects.unresolved()
assert len(qs) == 2
assert list(qs) == [job, escalated_job]
assert list(qs) == [job]
def test_reviewer_handled(self):
not_policy_report = AbuseReport.objects.create(
@ -639,13 +632,8 @@ class TestCinderJobManager(TestCase):
qs = CinderJob.objects.resolvable_in_reviewer_tools()
assert list(qs) == [job, appeal_job]
not_policy_report.cinder_job.update(
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
addon=not_policy_report.target,
),
resolvable_in_reviewer_tools=True,
)
not_policy_report.cinder_job.update(resolvable_in_reviewer_tools=True)
CinderJob.objects.create(forwarded_to_job=not_policy_report.cinder_job)
qs = CinderJob.objects.resolvable_in_reviewer_tools()
assert list(qs) == [not_policy_report.cinder_job, job, appeal_job]
@ -696,7 +684,7 @@ class TestCinderJob(TestCase):
assert isinstance(helper, CinderAddon)
assert not isinstance(helper, CinderAddonHandledByReviewers)
assert helper.addon == addon
assert helper.version is None
assert helper.version_string is None
helper = CinderJob.get_entity_helper(
addon,
@ -707,14 +695,14 @@ class TestCinderJob(TestCase):
assert isinstance(helper, CinderAddon)
assert not isinstance(helper, CinderAddonHandledByReviewers)
assert helper.addon == addon
assert helper.version == addon.current_version
assert helper.version_string == addon.current_version
helper = CinderJob.get_entity_helper(addon, resolved_in_reviewer_tools=True)
# if now reason is in REVIEWER_HANDLED it will be reported differently
assert isinstance(helper, CinderAddon)
assert isinstance(helper, CinderAddonHandledByReviewers)
assert helper.addon == addon
assert helper.version is None
assert helper.version_string is None
helper = CinderJob.get_entity_helper(
addon,
@ -725,7 +713,7 @@ class TestCinderJob(TestCase):
assert isinstance(helper, CinderAddon)
assert isinstance(helper, CinderAddonHandledByReviewers)
assert helper.addon == addon
assert helper.version == addon.current_version
assert helper.version_string == addon.current_version.version
helper = CinderJob.get_entity_helper(user, resolved_in_reviewer_tools=False)
assert isinstance(helper, CinderUser)
@ -901,6 +889,89 @@ class TestCinderJob(TestCase):
assert cinder_job.target_addon == abuse_report.target
assert cinder_job.resolvable_in_reviewer_tools
def test_handle_job_recreated(self):
addon = addon_factory()
decision = CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon, notes='blah'
)
job = CinderJob.objects.create(
job_id='1234', target_addon=addon, decision=decision
)
report = AbuseReport.objects.create(guid=addon.guid, cinder_job=job)
assert not job.resolvable_in_reviewer_tools
job.handle_job_recreated(new_job_id='5678')
job.reload()
new_job = job.forwarded_to_job
assert new_job.job_id == '5678'
assert list(new_job.forwarded_from_jobs.all()) == [job]
assert new_job.resolvable_in_reviewer_tools
assert new_job.target_addon == addon
assert report.reload().cinder_job == new_job
def test_handle_job_recreated_existing_job(self):
addon = addon_factory()
exisiting_escalation_job = CinderJob.objects.create(
job_id='5678', target_addon=addon
)
other_forwarded_job = CinderJob.objects.create(
job_id='9999', target_addon=addon, forwarded_to_job=exisiting_escalation_job
)
decision = CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon, notes='blah'
)
old_job = CinderJob.objects.create(
job_id='1234', target_addon=addon, decision=decision
)
report = AbuseReport.objects.create(guid=addon.guid, cinder_job=old_job)
old_job.handle_job_recreated(new_job_id='5678')
old_job.reload()
exisiting_escalation_job.reload()
assert old_job.forwarded_to_job == exisiting_escalation_job
assert list(exisiting_escalation_job.forwarded_from_jobs.all()) == [
other_forwarded_job,
old_job,
]
assert report.reload().cinder_job == exisiting_escalation_job
def test_handle_job_recreated_appeal(self):
addon = addon_factory()
decision = CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon, notes='blah'
)
appeal_job = CinderJob.objects.create(
job_id='1234', target_addon=addon, decision=decision
)
original_job = CinderJob.objects.create(
job_id='0000',
target_addon=addon,
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon,
notes='its okay',
appeal_job=appeal_job,
),
)
report = AbuseReport.objects.create(
guid=addon.guid, cinder_job=original_job, appellant_job=appeal_job
)
assert not appeal_job.resolvable_in_reviewer_tools
appeal_job.handle_job_recreated(new_job_id='5678')
appeal_job.reload()
new_job = appeal_job.forwarded_to_job
assert new_job.job_id == '5678'
assert list(new_job.forwarded_from_jobs.all()) == [appeal_job]
assert new_job.resolvable_in_reviewer_tools
assert new_job.target_addon == addon
assert original_job.decision.reload().appeal_job == new_job
assert report.reload().appellant_job == new_job
def test_process_decision(self):
cinder_job = CinderJob.objects.create(job_id='1234')
target = user_factory()
@ -965,12 +1036,19 @@ class TestCinderJob(TestCase):
assert notify_mock.call_count == 1
assert list(cinder_job.decision.policies.all()) == [policy]
def test_process_decision_escalate_addon(self):
def test_process_decision_escalate_addon_action(self):
addon = addon_factory()
cinder_job = CinderJob.objects.create(job_id='1234', target_addon=addon)
AbuseReport.objects.create(guid=addon.guid, cinder_job=cinder_job)
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',
json={'job_id': '5678'},
status=201,
)
cinder_job.process_decision(
decision_cinder_id='12345',
decision_date=new_date,
@ -978,13 +1056,18 @@ class TestCinderJob(TestCase):
decision_notes='blah',
policy_ids=[],
)
assert cinder_job.decision.cinder_id == '12345'
assert cinder_job.decision.date == new_date
cinder_job.reload()
assert cinder_job.decision
assert cinder_job.decision.action == DECISION_ACTIONS.AMO_ESCALATE_ADDON
assert cinder_job.decision.notes == 'blah'
assert cinder_job.decision.addon == addon
assert cinder_job.resolvable_in_reviewer_tools
assert cinder_job.target_addon == addon
new_job = cinder_job.forwarded_to_job
assert new_job
assert new_job.job_id == '5678'
assert list(new_job.forwarded_from_jobs.all()) == [cinder_job]
assert new_job.resolvable_in_reviewer_tools
assert new_job.target_addon == addon
assert report.reload().cinder_job == new_job
def _test_resolve_job(self, activity_action, cinder_action, *, expect_target_email):
addon_developer = user_factory()
@ -1271,15 +1354,11 @@ class TestCinderJob(TestCase):
).exists()
assert NeedsHumanReview.objects.filter(is_active=True).count() == 2
def test_resolve_job_escalation(self):
def test_resolve_job_forwarded(self):
addon_developer = user_factory()
addon = addon_factory(users=[addon_developer])
cinder_job = CinderJob.objects.create(
job_id='999',
decision=CinderDecision.objects.create(
addon=addon, action=DECISION_ACTIONS.AMO_ESCALATE_ADDON
),
)
cinder_job = CinderJob.objects.create(job_id='999')
CinderJob.objects.create(forwarded_to_job=cinder_job)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION,
@ -1301,7 +1380,7 @@ class TestCinderJob(TestCase):
)
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_decision',
f'{settings.CINDER_SERVER_URL}jobs/{cinder_job.job_id}/decision',
json={'uuid': uuid.uuid4().hex},
status=201,
)
@ -1405,6 +1484,113 @@ class TestCinderJob(TestCase):
assert not job.is_appeal
assert appeal.is_appeal
def test_clear_needs_human_review_flags(self):
def nhr_exists(reason):
return NeedsHumanReview.objects.filter(
reason=reason, is_active=True
).exists()
addon = addon_factory()
job = CinderJob.objects.create(
job_id='1',
target_addon=addon,
resolvable_in_reviewer_tools=True,
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE, addon=addon
),
)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION,
)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION,
)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL,
)
# for a non-forwarded or appealed job, this should clear the abuse NHR only
job.clear_needs_human_review_flags()
assert not nhr_exists(NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION)
assert nhr_exists(NeedsHumanReview.REASONS.CINDER_ESCALATION)
assert nhr_exists(NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION,
)
# if the job is forwarded, we make sure that there are no other forwarded jobs
CinderJob.objects.create(job_id='2', target_addon=addon, forwarded_to_job=job)
other_forward = CinderJob.objects.create(
job_id='3',
target_addon=addon,
resolvable_in_reviewer_tools=True,
)
CinderJob.objects.create(
job_id='4', target_addon=addon, forwarded_to_job=other_forward
)
job.clear_needs_human_review_flags()
assert nhr_exists(NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION)
assert nhr_exists(NeedsHumanReview.REASONS.CINDER_ESCALATION)
assert nhr_exists(NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL)
# unless the other job is closed too
other_forward.update(
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE, addon=addon
)
)
job.clear_needs_human_review_flags()
assert nhr_exists(NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION)
assert not nhr_exists(NeedsHumanReview.REASONS.CINDER_ESCALATION)
assert nhr_exists(NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL)
NeedsHumanReview.objects.create(
version=addon.current_version,
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION,
)
# similarly if the job is an appeal we make sure that there are no other appeals
CinderJob.objects.create(
job_id='5',
target_addon=addon,
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE, addon=addon, appeal_job=job
),
)
job.forwarded_from_jobs.get().delete()
other_appeal = CinderJob.objects.create(
job_id='6',
target_addon=addon,
resolvable_in_reviewer_tools=True,
)
CinderJob.objects.create(
job_id='7',
target_addon=addon,
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon,
appeal_job=other_appeal,
),
)
job.clear_needs_human_review_flags()
assert nhr_exists(NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION)
assert nhr_exists(NeedsHumanReview.REASONS.CINDER_ESCALATION)
assert nhr_exists(NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL)
# unless the other job is closed too
other_appeal.update(
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_APPROVE, addon=addon
)
)
job.clear_needs_human_review_flags()
assert nhr_exists(NeedsHumanReview.REASONS.ABUSE_ADDON_VIOLATION)
assert nhr_exists(NeedsHumanReview.REASONS.CINDER_ESCALATION)
assert not nhr_exists(NeedsHumanReview.REASONS.ADDON_REVIEW_APPEAL)
class TestCinderDecisionCanBeAppealed(TestCase):
def setUp(self):
@ -1625,13 +1811,9 @@ class TestCinderDecisionCanBeAppealed(TestCase):
)
self.decision.can_be_appealed(is_reporter=False)
def test_author_cant_appeal_approve_or_escalation_decision(self):
for decision_action in (
DECISION_ACTIONS.AMO_ESCALATE_ADDON,
DECISION_ACTIONS.AMO_APPROVE,
):
self.decision.update(action=decision_action)
assert not self.decision.can_be_appealed(is_reporter=False)
def test_author_cant_appeal_approve_decision(self):
self.decision.update(action=DECISION_ACTIONS.AMO_APPROVE)
assert not self.decision.can_be_appealed(is_reporter=False)
def test_author_cant_appeal_disable_decision_already_appealed(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DISABLE_ADDON)

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

@ -28,6 +28,7 @@ from olympia.zadmin.models import set_config
from ..models import AbuseReport, CinderDecision, CinderJob, CinderPolicy
from ..tasks import (
appeal_to_cinder,
handle_escalate_action,
notify_addon_decision_to_cinder,
report_to_cinder,
resolve_job_in_cinder,
@ -985,3 +986,30 @@ class TestSyncCinderPolicies(TestCase):
sync_cinder_policies()
assert CinderPolicy.objects.count() == 6
assert CinderPolicy.objects.filter(text='ADDED').count() == 6
@pytest.mark.django_db
def test_handle_escalate_action():
addon = addon_factory()
decision = CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, addon=addon, notes='blah'
)
job = CinderJob.objects.create(job_id='1234', target_addon=addon, decision=decision)
report = AbuseReport.objects.create(guid=addon.guid, cinder_job=job)
assert not job.resolvable_in_reviewer_tools
responses.add(
responses.POST,
f'{settings.CINDER_SERVER_URL}create_report',
json={'job_id': '5678'},
status=201,
)
handle_escalate_action(job_pk=job.pk)
job.reload()
new_job = job.forwarded_to_job
assert new_job.job_id == '5678'
assert list(new_job.forwarded_from_jobs.all()) == [job]
assert new_job.resolvable_in_reviewer_tools
assert new_job.target_addon == addon
assert report.reload().cinder_job == new_job

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

@ -7,17 +7,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.amo.tests import (
TestCase,
addon_factory,
collection_factory,
user_factory,
version_factory,
)
from olympia.amo.tests import TestCase, addon_factory, collection_factory, user_factory
from olympia.constants.abuse import DECISION_ACTIONS
from olympia.core import set_user
from olympia.ratings.models import Rating
from olympia.reviewers.models import NeedsHumanReview
from ..models import AbuseReport, CinderDecision, CinderJob, CinderPolicy
from ..utils import (
@ -27,7 +20,6 @@ from ..utils import (
CinderActionDeleteCollection,
CinderActionDeleteRating,
CinderActionDisableAddon,
CinderActionEscalateAddon,
CinderActionIgnore,
CinderActionOverrideApprove,
CinderActionRejectVersion,
@ -427,7 +419,7 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase):
self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.user.name}')
@override_switch('dsa-cinder-escalations-review', active=True)
@override_switch('dsa-cinder-forwarded-review', active=True)
@override_switch('dsa-appeals-review', active=True)
class TestCinderActionAddon(BaseTestCinderAction, TestCase):
ActionClass = CinderActionDisableAddon
@ -514,111 +506,6 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase):
action.notify_owners()
return f'Mozilla Add-ons: {self.addon.name}'
def test_escalate_addon(self):
listed_version = self.addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=self.addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
ActivityLog.objects.all().delete()
action = CinderActionEscalateAddon(self.decision)
assert action.process_action() is None
assert self.addon.reload().status == amo.STATUS_APPROVED
assert (
listed_version.reload().needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert (
unlisted_version.reload().needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.filter(
action=amo.LOG.NEEDS_HUMAN_REVIEW_CINDER.id
).get()
assert activity.arguments == [listed_version, unlisted_version]
assert activity.user == self.task_user
# but if we have a version specified, we flag that version
NeedsHumanReview.objects.all().delete()
other_version = version_factory(
addon=self.addon, file_kw={'status': amo.STATUS_DISABLED, 'is_signed': True}
)
assert not other_version.due_date
ActivityLog.objects.all().delete()
self.cinder_job.abusereport_set.update(addon_version=other_version.version)
assert action.process_action() is None
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
other_version.reload()
assert other_version.due_date
assert (
other_version.needshumanreview_set.get().reason
== NeedsHumanReview.REASONS.CINDER_ESCALATION
)
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.get(action=amo.LOG.NEEDS_HUMAN_REVIEW_CINDER.id)
assert activity.arguments == [other_version]
assert activity.user == self.task_user
self.cinder_job.notify_reporters(action)
assert len(mail.outbox) == 0
def test_escalate_addon_no_versions_to_flag(self):
listed_version = self.addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=self.addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
NeedsHumanReview.objects.create(
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION, version=listed_version
)
NeedsHumanReview.objects.create(
reason=NeedsHumanReview.REASONS.CINDER_ESCALATION, version=unlisted_version
)
assert NeedsHumanReview.objects.count() == 2
ActivityLog.objects.all().delete()
action = CinderActionEscalateAddon(self.decision)
assert action.process_action() is None
assert NeedsHumanReview.objects.count() == 2
assert ActivityLog.objects.count() == 0
assert len(mail.outbox) == 0
@override_switch('dsa-cinder-escalations-review', active=False)
def test_escalate_addon_waffle_switch_off(self):
# Escalation when the waffle switch is off is essentially a no-op on
# AMO side.
listed_version = self.addon.current_version
listed_version.file.update(is_signed=True)
unlisted_version = version_factory(
addon=self.addon, channel=amo.CHANNEL_UNLISTED, file_kw={'is_signed': True}
)
ActivityLog.objects.all().delete()
action = CinderActionEscalateAddon(self.decision)
assert action.process_action() is None
assert self.addon.reload().status == amo.STATUS_APPROVED
assert not listed_version.reload().needshumanreview_set.exists()
assert not listed_version.due_date
assert not unlisted_version.reload().needshumanreview_set.exists()
assert not unlisted_version.due_date
assert ActivityLog.objects.count() == 0
other_version = version_factory(
addon=self.addon, file_kw={'status': amo.STATUS_DISABLED, 'is_signed': True}
)
assert not other_version.due_date
ActivityLog.objects.all().delete()
self.cinder_job.abusereport_set.update(addon_version=other_version.version)
assert action.process_action() is None
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
other_version.reload()
assert not other_version.due_date
assert not listed_version.reload().needshumanreview_set.exists()
assert not unlisted_version.reload().needshumanreview_set.exists()
def test_target_appeal_decline(self):
self.addon.update(status=amo.STATUS_DISABLED)
ActivityLog.objects.all().delete()

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

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
import waffle
import olympia
from olympia import activity, amo
from olympia import amo
from olympia.activity import log_create
from olympia.addons.models import Addon
from olympia.amo.templatetags.jinja_helpers import absolutify
@ -284,81 +284,9 @@ class CinderActionEscalateAddon(CinderAction):
valid_targets = (Addon,)
def process_action(self):
"""This will return always return a falsey value because we've not taken any
action at this point, just flagging for human review."""
return self.flag_for_human_review()
from olympia.abuse.tasks import handle_escalate_action
def flag_for_human_review(self):
from olympia.reviewers.models import NeedsHumanReview
if not waffle.switch_is_active('dsa-cinder-escalations-review'):
log.info(
'Not adding %s to review queue despite %s because waffle switch is off',
self.target,
'escalation',
)
return
if isinstance(self.target, Addon) and self.decision.is_third_party_initiated:
reason = NeedsHumanReview.REASONS.CINDER_ESCALATION
reported_versions = set(
self.decision.cinder_job.abusereport_set.values_list(
'addon_version', flat=True
)
)
version_objs = (
set(
self.target.versions(manager='unfiltered_for_relations')
.filter(version__in=reported_versions)
.exclude(
needshumanreview__reason=reason,
needshumanreview__is_active=True,
)
.no_transforms()
)
if reported_versions
else set()
)
nhr_object = None
# We need custom save() and post_save to be triggered, so we can't
# optimize this via bulk_create().
for version in version_objs:
nhr_object = NeedsHumanReview(
version=version, reason=reason, is_active=True
)
nhr_object.save(_no_automatic_activity_log=True)
# If we have more versions specified than versions we flagged, flag latest
# to be safe. (Either because there was an unknown version, or a None)
if (
len(version_objs) != len(reported_versions)
or len(reported_versions) == 0
):
version_objs = version_objs.union(
self.target.set_needs_human_review_on_latest_versions(
reason=reason,
ignore_reviewed=False,
unique_reason=True,
skip_activity_log=True,
)
)
if version_objs:
version_objs = sorted(version_objs, key=lambda v: v.id)
# we just need this to exact to do get_reason_display
nhr_object = nhr_object or NeedsHumanReview(
version=version_objs[-1],
reason=reason,
is_active=True,
)
activity.log_create(
amo.LOG.NEEDS_HUMAN_REVIEW_CINDER,
*version_objs,
details={'comments': nhr_object.get_reason_display()},
)
def get_owners(self):
# we don't send any emails for escalations
return ()
handle_escalate_action.delay(job_pk=self.decision.cinder_job.pk)
class CinderActionDeleteCollection(CinderAction):

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

@ -7,7 +7,8 @@ REPORTED_MEDIA_BACKUP_EXPIRATION_DAYS = 31 + APPEAL_EXPIRATION_DAYS
DECISION_ACTIONS = APIChoicesWithDash(
('AMO_BAN_USER', 1, 'User ban'),
('AMO_DISABLE_ADDON', 2, 'Add-on disable'),
('AMO_ESCALATE_ADDON', 3, 'Escalate add-on to reviewers'),
# Used to indicate the job has been forwarded to AMO
('AMO_ESCALATE_ADDON', 3, 'Forward add-on to reviewers'),
# 4 is unused
('AMO_DELETE_RATING', 5, 'Rating delete'),
('AMO_DELETE_COLLECTION', 6, 'Collection delete'),
@ -33,12 +34,7 @@ DECISION_ACTIONS.add_subset(
),
)
DECISION_ACTIONS.add_subset(
'APPEALABLE_BY_REPORTER',
('AMO_APPROVE', 'AMO_APPROVE_VERSION'),
)
DECISION_ACTIONS.add_subset(
'UNRESOLVED',
('AMO_ESCALATE_ADDON',),
'APPEALABLE_BY_REPORTER', ('AMO_APPROVE', 'AMO_APPROVE_VERSION')
)
DECISION_ACTIONS.add_subset(
'REMOVING',
@ -50,10 +46,7 @@ DECISION_ACTIONS.add_subset(
'AMO_REJECT_VERSION_ADDON',
),
)
DECISION_ACTIONS.add_subset(
'APPROVING',
('AMO_APPROVE', 'AMO_APPROVE_VERSION'),
)
DECISION_ACTIONS.add_subset('APPROVING', ('AMO_APPROVE', 'AMO_APPROVE_VERSION'))
# Illegal categories, only used when the reason is `illegal`. The constants
# are derived from the "spec" but without the `STATEMENT_CATEGORY_` prefix.

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

@ -17,6 +17,14 @@ def create_waffle_switch(name):
return inner
def rename_waffle_switch(old_name, new_name):
def inner(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.update_or_create(name=old_name, defaults={'name': new_name})
return inner
class DeleteWaffleSwitch(migrations.RunPython):
def __init__(self, name, **kwargs):
super().__init__(
@ -39,3 +47,15 @@ class CreateWaffleSwitch(migrations.RunPython):
def describe(self):
return 'Create Waffle Switch (Python operation)'
class RenameWaffleSwitch(migrations.RunPython):
def __init__(self, old_name, new_name, **kwargs):
super().__init__(
rename_waffle_switch(old_name, new_name),
reverse_code=rename_waffle_switch(new_name, old_name),
**kwargs,
)
def describe(self):
return 'Rename Waffle Switch, safely (Python operation)'

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

@ -7,7 +7,11 @@ from django.apps.registry import apps
import pytest
from waffle.models import Switch
from olympia.core.db.migrations import CreateWaffleSwitch, DeleteWaffleSwitch
from olympia.core.db.migrations import (
CreateWaffleSwitch,
DeleteWaffleSwitch,
RenameWaffleSwitch,
)
@pytest.mark.django_db
@ -78,3 +82,41 @@ def test_create_waffle_switch_reverse():
'fake_app', schema_editor, from_state, to_state
)
assert not Switch.objects.filter(name='foo').exists()
@pytest.mark.django_db
def test_rename_waffle_switch_forward():
schema_editor = mock.Mock(connection=mock.Mock(alias='default'))
from_state = mock.Mock(apps=apps)
to_state = mock.Mock()
RenameWaffleSwitch('foo', 'baa').database_forwards(
'fake_app', schema_editor, from_state, to_state
)
assert not Switch.objects.filter(name='foo').exists()
assert Switch.objects.filter(name='baa').exists()
@pytest.mark.django_db
def test_rename_waffle_switch_forward_already_exists():
Switch.objects.create(name='foo')
schema_editor = mock.Mock(connection=mock.Mock(alias='default'))
from_state = mock.Mock(apps=apps)
to_state = mock.Mock()
RenameWaffleSwitch('foo', 'baa').database_forwards(
'fake_app', schema_editor, from_state, to_state
)
assert not Switch.objects.filter(name='foo').exists()
assert Switch.objects.filter(name='baa').exists()
@pytest.mark.django_db
def test_rename_waffle_switch_reverse():
Switch.objects.create(name='baa')
schema_editor = mock.Mock(connection=mock.Mock(alias='default'))
from_state = mock.Mock(apps=apps)
to_state = mock.Mock()
RenameWaffleSwitch('foo', 'baa').database_backwards(
'fake_app', schema_editor, from_state, to_state
)
assert Switch.objects.filter(name='foo').exists()
assert not Switch.objects.filter(name='baa').exists()

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

@ -848,6 +848,7 @@ CELERY_TASK_ROUTES = {
# Misc AMO tasks.
'olympia.blocklist.tasks.monitor_remote_settings': {'queue': 'amo'},
'olympia.abuse.tasks.appeal_to_cinder': {'queue': 'amo'},
'olympia.abuse.tasks.handle_escalate_action': {'queue': 'amo'},
'olympia.abuse.tasks.report_to_cinder': {'queue': 'amo'},
'olympia.abuse.tasks.notify_addon_decision_to_cinder': {'queue': 'amo'},
'olympia.abuse.tasks.resolve_job_in_cinder': {'queue': 'amo'},

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

@ -18,7 +18,6 @@ from olympia import amo, ratings
from olympia.abuse.models import CinderJob, CinderPolicy
from olympia.access import acl
from olympia.amo.forms import AMOModelForm
from olympia.constants.abuse import DECISION_ACTIONS
from olympia.constants.reviewers import REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT
from olympia.ratings.models import Rating
from olympia.ratings.permissions import user_can_delete_rating
@ -229,8 +228,8 @@ class CinderJobsWidget(forms.CheckboxSelectMultiple):
# label_from_instance() on WidgetRenderedModelMultipleChoiceField returns the
# full object, not a label, this is what makes this work.
obj = label
is_escalation = (
obj.decision and obj.decision.action == DECISION_ACTIONS.AMO_ESCALATE_ADDON
forwarded_notes = obj.forwarded_from_jobs.all().values_list(
'decision__notes', flat=True
)
is_appeal = obj.is_appeal
reports = obj.all_abuse_reports
@ -244,14 +243,18 @@ class CinderJobsWidget(forms.CheckboxSelectMultiple):
)
for report in reports
)
escalation = ((f'Reasoning: {obj.decision.notes}',),) if is_escalation else ()
forwarded = (
((f'Reasoning: {"; ".join(notes for notes in forwarded_notes)}',),)
if forwarded_notes
else ()
)
appeals = (
(appeal_text_obj.text, appeal_text_obj.reporter_report is not None)
for appealed_decision in obj.appealed_decisions.all()
for appeal_text_obj in appealed_decision.appeals.all()
)
subtexts_gen = [
*escalation,
*forwarded,
*(
(f'{"Reporter" if is_reporter else "Developer"} Appeal: {text}',)
for text, is_reporter in appeals
@ -263,7 +266,7 @@ class CinderJobsWidget(forms.CheckboxSelectMultiple):
'{}{}{}<details><summary>Show detail on {} reports</summary>'
'<span>{}</span><ul>{}</ul></details>',
'[Appeal] ' if is_appeal else '',
'[Escalation] ' if is_escalation else '',
'[Forwarded] ' if forwarded else '',
format_html_join(', ', '"{}"', reasons_set),
len(reports),
format_html_join('', '{}<br/>', subtexts_gen),

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

@ -1034,20 +1034,24 @@ class TestReviewForm(TestCase):
reporter_report=appealed_abuse_report,
)
cinder_job_escalated = CinderJob.objects.create(
job_id='escalated',
cinder_job_forwarded = CinderJob.objects.create(
job_id='forwarded',
resolvable_in_reviewer_tools=True,
target_addon=self.addon,
)
CinderJob.objects.create(
job_id='forwarded_from',
forwarded_to_job=cinder_job_forwarded,
decision=CinderDecision.objects.create(
action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
notes='Why o why',
addon=self.addon,
),
resolvable_in_reviewer_tools=True,
target_addon=self.addon,
)
AbuseReport.objects.create(
**{**abuse_kw, 'location': AbuseReport.LOCATION.AMO},
message='ddd',
cinder_job=cinder_job_escalated,
cinder_job=cinder_job_forwarded,
addon_version='<script>alert()</script>',
)
@ -1078,7 +1082,7 @@ class TestReviewForm(TestCase):
qs_list = list(choices.queryset)
assert qs_list == [
# Only unresolved, reviewer handled, jobs are shown
cinder_job_escalated,
cinder_job_forwarded,
cinder_job_appeal,
cinder_job_2_reports,
]
@ -1087,7 +1091,7 @@ class TestReviewForm(TestCase):
doc = pq(content)
label_0 = doc('label[for="id_cinder_jobs_to_resolve_0"]')
assert label_0.text() == (
'[Escalation] "DSA: It violates Mozilla\'s Add-on Policies"\n'
'[Forwarded] "DSA: It violates Mozilla\'s Add-on Policies"\n'
'Show detail on 1 reports\n'
'Reasoning: Why o why\n\n'
'v[<script>alert()</script>]: ddd'