Allow Abuse reports to be sent against Rating (#21364)
* Allow Abuse reports to be sent against Rating Includes refactors to share some logic between different kinds of abuse reports
This commit is contained in:
Родитель
1c6a16ada8
Коммит
1657c2d938
|
@ -53,7 +53,9 @@ to if necessary.
|
|||
:<json string|null install_date: The add-on install date.
|
||||
:<json string|null operating_system: The client's operating system.
|
||||
:<json string|null operating_system_version: The client's operating system version.
|
||||
:<json string|null reason: The reason for the report. The accepted values are documented in the :ref:`table below <abuse-reason-parameter>`.
|
||||
:<json string|null reason: The reason for the report. The accepted values are documented in the :ref:`table below <abuse-addon-reason-parameter>`.
|
||||
:<json string|null reporter_name: The provided name of the reporter, if not authenticated.
|
||||
:<json string|null reporter_email: The provided email of the reporter, if not authenticated.
|
||||
:>json object|null reporter: The user who submitted the report, if authenticated.
|
||||
:>json int reporter.id: The id of the user who submitted the report.
|
||||
:>json string reporter.name: The name of the user who submitted the report.
|
||||
|
@ -191,9 +193,9 @@ to if necessary.
|
|||
privileged Privileged
|
||||
=========================== =================================================
|
||||
|
||||
.. _abuse-reason-parameter:
|
||||
.. _abuse-addon-reason-parameter:
|
||||
|
||||
Accepted values for the ``reason`` parameter:
|
||||
Accepted values for the ``reason`` parameter (for add-on abuse reports):
|
||||
|
||||
=========================== ================================================================
|
||||
Value Description
|
||||
|
@ -205,6 +207,10 @@ to if necessary.
|
|||
policy Hateful, violent, or illegal content
|
||||
deceptive Doesn't match description
|
||||
unwanted Wasn't wanted / impossible to get rid of
|
||||
hateful_violent_deceptive Hateful, violent, deceptive, or other inappropriate content
|
||||
illegal Violates the law or contains content that violates the law
|
||||
does_not_work Doesn’t work, breaks websites, or slows Firefox down
|
||||
feedback_spam Spam
|
||||
other Something else
|
||||
=========================== ================================================================
|
||||
|
||||
|
@ -238,6 +244,8 @@ so reports can be responded to if necessary.
|
|||
|
||||
:<json string user: The id or username of the user to report for abuse (required).
|
||||
:<json string message: The body/content of the abuse report (required).
|
||||
:<json string|null reporter_name: The provided name of the reporter, if not authenticated.
|
||||
:<json string|null reporter_email: The provided email of the reporter, if not authenticated.
|
||||
:>json object|null reporter: The user who submitted the report, if authenticated.
|
||||
:>json int reporter.id: The id of the user who submitted the report.
|
||||
:>json string reporter.name: The name of the user who submitted the report.
|
||||
|
@ -251,3 +259,48 @@ so reports can be responded to if necessary.
|
|||
:>json string user.url: The link to the profile page for of the user reported.
|
||||
:>json string user.username: The username of the user reported.
|
||||
:>json string message: The body/content of the abuse report.
|
||||
|
||||
|
||||
--------------------------------
|
||||
Submitting a rating abuse report
|
||||
--------------------------------
|
||||
|
||||
.. _`ratingabusereport-create`:
|
||||
|
||||
The following API endpoint allows an abuse report to be submitted for a rating
|
||||
on https://addons.mozilla.org. Authentication is not required, but is recommended
|
||||
so reports can be responded to if necessary.
|
||||
|
||||
.. http:post:: /api/v5/abuse/report/rating/
|
||||
|
||||
.. _ratingabusereport-create-request:
|
||||
|
||||
:<json string rating: The id of the rating to report for abuse (required).
|
||||
:<json string message: The body/content of the abuse report (required).
|
||||
:<json string|null reason: The reason for the report. The accepted values are documented in the :ref:`table below <abuse-rating-reason-parameter>`.
|
||||
:<json string|null reporter_name: The provided name of the reporter, if not authenticated.
|
||||
:<json string|null reporter_email: The provided email of the reporter, if not authenticated.
|
||||
:>json object|null reporter: The user who submitted the report, if authenticated.
|
||||
:>json int reporter.id: The id of the user who submitted the report.
|
||||
:>json string reporter.name: The name of the user who submitted the report.
|
||||
:>json string reporter.url: The link to the profile page for of the user who submitted the report.
|
||||
:>json string reporter.username: The username of the user who submitted the report.
|
||||
:>json string|null reporter_name: The provided name of the reporter, if not authenticated.
|
||||
:>json string|null reporter_email: The provided email of the reporter, if not authenticated.
|
||||
:>json object rating: The user reported for abuse.
|
||||
:>json int rating.id: The id of the rating reported.
|
||||
:>json string message: The body/content of the abuse report.
|
||||
:>json string|null reason: The reason for the report.
|
||||
|
||||
|
||||
.. _abuse-rating-reason-parameter:
|
||||
|
||||
Accepted values for the ``reason`` parameter (for rating abuse reports):
|
||||
|
||||
=========================== ================================================================
|
||||
Value Description
|
||||
=========================== ================================================================
|
||||
hateful_violent_deceptive Hateful, violent, deceptive, or other inappropriate content
|
||||
illegal Violates the law or contains content that violates the law
|
||||
other Something else
|
||||
=========================== ================================================================
|
||||
|
|
|
@ -2,11 +2,17 @@ from django.urls import include, re_path
|
|||
|
||||
from rest_framework.routers import SimpleRouter
|
||||
|
||||
from .views import AddonAbuseViewSet, UserAbuseViewSet, cinder_webhook
|
||||
from .views import (
|
||||
AddonAbuseViewSet,
|
||||
RatingAbuseViewSet,
|
||||
UserAbuseViewSet,
|
||||
cinder_webhook,
|
||||
)
|
||||
|
||||
|
||||
reporting = SimpleRouter()
|
||||
reporting.register(r'addon', AddonAbuseViewSet, basename='abusereportaddon')
|
||||
reporting.register(r'rating', RatingAbuseViewSet, basename='abusereportrating')
|
||||
reporting.register(r'user', UserAbuseViewSet, basename='abusereportuser')
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -109,11 +109,14 @@ class CinderUser(Cinder):
|
|||
}
|
||||
|
||||
def get_context(self):
|
||||
addons = [CinderAddon(addon) for addon in self.user.addons.all()]
|
||||
cinder_addons = [CinderAddon(addon) for addon in self.user.addons.all()]
|
||||
return {
|
||||
'entities': [addon.get_entity_data() for addon in addons],
|
||||
'entities': [
|
||||
cinder_addon.get_entity_data() for cinder_addon in cinder_addons
|
||||
],
|
||||
'relationships': [
|
||||
self.get_relationship_data(addon, 'amo_author_of') for addon in addons
|
||||
self.get_relationship_data(cinder_addon, 'amo_author_of')
|
||||
for cinder_addon in cinder_addons
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -163,11 +166,39 @@ class CinderAddon(Cinder):
|
|||
}
|
||||
|
||||
def get_context(self):
|
||||
authors = [CinderUser(author) for author in self.addon.authors.all()]
|
||||
cinder_users = [CinderUser(author) for author in self.addon.authors.all()]
|
||||
return {
|
||||
'entities': [author.get_entity_data() for author in authors],
|
||||
'entities': [cinder_user.get_entity_data() for cinder_user in cinder_users],
|
||||
'relationships': [
|
||||
author.get_relationship_data(self, 'amo_author_of')
|
||||
for author in authors
|
||||
cinder_user.get_relationship_data(self, 'amo_author_of')
|
||||
for cinder_user in cinder_users
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class CinderRating(Cinder):
|
||||
type = 'amo_rating'
|
||||
|
||||
def __init__(self, rating):
|
||||
self.rating = rating
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return str(self.rating.id)
|
||||
|
||||
def get_attributes(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'body': self.rating.body,
|
||||
}
|
||||
|
||||
def get_context(self):
|
||||
# Note: we are not currently sending the add-on the rating is for as
|
||||
# part of the context.
|
||||
cinder_user = CinderUser(self.rating.user)
|
||||
return {
|
||||
'entities': [cinder_user.get_entity_data()],
|
||||
'relationships': [
|
||||
cinder_user.get_relationship_data(self, 'amo_rating_author_of')
|
||||
],
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 4.2.6 on 2023-10-27 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('ratings', '0009_alter_deniedratingword_word'),
|
||||
('abuse', '0014_abusereport_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='abusereport',
|
||||
name='just_one_of_guid_and_user_must_be_set',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='abusereport',
|
||||
name='rating',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='abuse_reports',
|
||||
to='ratings.rating',
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='abusereport',
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
models.Q(
|
||||
models.Q(('guid', ''), _negated=True),
|
||||
('guid__isnull', False),
|
||||
('rating__isnull', True),
|
||||
('user__isnull', True),
|
||||
),
|
||||
models.Q(
|
||||
('guid__isnull', True),
|
||||
('rating__isnull', True),
|
||||
('user__isnull', False),
|
||||
),
|
||||
models.Q(
|
||||
('guid__isnull', True),
|
||||
('rating__isnull', False),
|
||||
('user__isnull', True),
|
||||
),
|
||||
_connector='OR',
|
||||
),
|
||||
name='just_one_of_guid_user_rating_must_be_set',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -8,9 +8,10 @@ from olympia import amo
|
|||
from olympia.addons.models import Addon
|
||||
from olympia.amo.models import BaseQuerySet, ManagerBase, ModelBase
|
||||
from olympia.api.utils import APIChoices, APIChoicesWithNone
|
||||
from olympia.ratings.models import Rating
|
||||
from olympia.users.models import UserProfile
|
||||
|
||||
from .cinder import CinderAddon, CinderUnauthenticatedReporter, CinderUser
|
||||
from .cinder import CinderAddon, CinderRating, CinderUnauthenticatedReporter, CinderUser
|
||||
|
||||
|
||||
class AbuseReportQuerySet(BaseQuerySet):
|
||||
|
@ -107,11 +108,13 @@ class AbuseReport(ModelBase):
|
|||
),
|
||||
('OTHER', 127, 'Other'),
|
||||
)
|
||||
REPORTABLE_REASONS = (
|
||||
REASONS.HATEFUL_VIOLENT_DECEPTIVE,
|
||||
REASONS.ILLEGAL,
|
||||
REASONS.POLICY_VIOLATION,
|
||||
REASONS.OTHER,
|
||||
REASONS.add_subset(
|
||||
'RATING_REASONS', ('HATEFUL_VIOLENT_DECEPTIVE', 'ILLEGAL', 'OTHER')
|
||||
)
|
||||
# Those reasons will be reported to Cinder.
|
||||
REASONS.add_subset(
|
||||
'REPORTABLE_REASONS',
|
||||
('HATEFUL_VIOLENT_DECEPTIVE', 'ILLEGAL', 'POLICY_VIOLATION', 'OTHER'),
|
||||
)
|
||||
|
||||
# https://searchfox.org
|
||||
|
@ -204,13 +207,17 @@ class AbuseReport(ModelBase):
|
|||
reporter_email = models.CharField(max_length=255, default=None, null=True)
|
||||
reporter_name = models.CharField(max_length=255, default=None, null=True)
|
||||
country_code = models.CharField(max_length=2, default=None, null=True)
|
||||
# An abuse report can be for an addon or a user.
|
||||
# If user is set then guid should be null.
|
||||
# If user is null then guid should be set.
|
||||
# An abuse report can be for an addon, a user or a rating.
|
||||
# - If user is set then guid and rating should be null.
|
||||
# - If guid is set then user and rating should be null.
|
||||
# - If rating is set then user and guid should be null.
|
||||
guid = models.CharField(max_length=255, null=True)
|
||||
user = models.ForeignKey(
|
||||
UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL
|
||||
)
|
||||
rating = models.ForeignKey(
|
||||
Rating, null=True, related_name='abuse_reports', on_delete=models.SET_NULL
|
||||
)
|
||||
message = models.TextField(blank=True)
|
||||
|
||||
state = models.PositiveSmallIntegerField(
|
||||
|
@ -295,16 +302,23 @@ class AbuseReport(ModelBase):
|
|||
]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name='just_one_of_guid_and_user_must_be_set',
|
||||
name='just_one_of_guid_user_rating_must_be_set',
|
||||
check=(
|
||||
models.Q(
|
||||
~models.Q(guid=''),
|
||||
guid__isnull=False,
|
||||
user__isnull=True,
|
||||
rating__isnull=True,
|
||||
)
|
||||
| models.Q(
|
||||
guid__isnull=True,
|
||||
user__isnull=False,
|
||||
rating__isnull=True,
|
||||
)
|
||||
| models.Q(
|
||||
guid__isnull=True,
|
||||
user__isnull=True,
|
||||
rating__isnull=False,
|
||||
)
|
||||
),
|
||||
),
|
||||
|
@ -320,12 +334,16 @@ class AbuseReport(ModelBase):
|
|||
def type(self):
|
||||
if self.guid:
|
||||
type_ = 'Addon'
|
||||
else:
|
||||
elif self.user_id:
|
||||
type_ = 'User'
|
||||
elif self.rating_id:
|
||||
type_ = 'Rating'
|
||||
else:
|
||||
type_ = 'Unknown'
|
||||
return type_
|
||||
|
||||
def __str__(self):
|
||||
name = self.guid if self.guid else self.user
|
||||
name = self.guid or self.user_id or self.rating_id
|
||||
return f'Abuse Report for {self.type} {name}'
|
||||
|
||||
@property
|
||||
|
@ -336,8 +354,10 @@ class AbuseReport(ModelBase):
|
|||
|
||||
if self.guid:
|
||||
return Addon.unfiltered.filter(guid=self.guid).first()
|
||||
elif self.user:
|
||||
elif self.user_id:
|
||||
return self.user
|
||||
elif self.rating_id:
|
||||
return self.rating
|
||||
return None
|
||||
|
||||
|
||||
|
@ -385,7 +405,8 @@ class CinderReport(ModelBase):
|
|||
return CinderAddon(target)
|
||||
elif isinstance(target, UserProfile):
|
||||
return CinderUser(target)
|
||||
# TODO: More helpers here
|
||||
elif isinstance(target, Rating):
|
||||
return CinderRating(target)
|
||||
return None
|
||||
|
||||
def get_cinder_reporter(self):
|
||||
|
|
|
@ -38,6 +38,16 @@ class BaseAbuseReportSerializer(AMOModelSerializer):
|
|||
output['reporter'] = request.user
|
||||
return output
|
||||
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
if (
|
||||
waffle.switch_is_active('enable-cinder-reporting')
|
||||
and validated_data.get('reason') in AbuseReport.REASONS.REPORTABLE_REASONS
|
||||
):
|
||||
# call task to fire off cinder report
|
||||
report_to_cinder.delay(instance.id)
|
||||
return instance
|
||||
|
||||
|
||||
class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||
error_messages = {
|
||||
|
@ -194,7 +204,7 @@ class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
|||
}
|
||||
if view := self.context.get('view'):
|
||||
try:
|
||||
addon = view.get_addon_object()
|
||||
addon = view.get_target_object()
|
||||
output['id'] = addon.pk
|
||||
output['slug'] = addon.slug
|
||||
except Http404:
|
||||
|
@ -204,16 +214,6 @@ class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
|||
pass
|
||||
return output
|
||||
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
if (
|
||||
waffle.switch_is_active('enable-cinder-reporting')
|
||||
and validated_data.get('reason') in AbuseReport.REPORTABLE_REASONS
|
||||
):
|
||||
# call task to fire off cinder report
|
||||
report_to_cinder.delay(instance.id)
|
||||
return instance
|
||||
|
||||
|
||||
class UserAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||
user = BaseUserSerializer(required=False) # We validate it ourselves.
|
||||
|
@ -228,9 +228,36 @@ class UserAbuseReportSerializer(BaseAbuseReportSerializer):
|
|||
def to_internal_value(self, data):
|
||||
view = self.context.get('view')
|
||||
self.validate_target(data, 'user')
|
||||
output = {'user': view.get_user_object()}
|
||||
output = {'user': view.get_target_object()}
|
||||
# Pop 'user' before passing it to super(), we already have the
|
||||
# output value and did the validation above.
|
||||
data.pop('user')
|
||||
output.update(super().to_internal_value(data))
|
||||
return output
|
||||
|
||||
|
||||
class RatingAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||
rating = serializers.SerializerMethodField()
|
||||
reason = ReverseChoiceField(
|
||||
choices=list(AbuseReport.REASONS.RATING_REASONS.api_choices),
|
||||
required=True,
|
||||
allow_null=True,
|
||||
)
|
||||
message = serializers.CharField(required=True, allow_blank=False, max_length=10000)
|
||||
|
||||
class Meta:
|
||||
model = AbuseReport
|
||||
fields = BaseAbuseReportSerializer.Meta.fields + ('rating', 'reason')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
self.validate_target(data, 'rating')
|
||||
view = self.context.get('view')
|
||||
output = {'rating': view.get_target_object()}
|
||||
# Pop 'rating' before passing it to super(), we already have the
|
||||
# output value and did the validation above.
|
||||
data.pop('rating')
|
||||
output.update(super().to_internal_value(data))
|
||||
return output
|
||||
|
||||
def get_rating(self, obj):
|
||||
return {'id': obj.rating.pk}
|
||||
|
|
|
@ -3,11 +3,22 @@ from django.conf import settings
|
|||
import responses
|
||||
|
||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||
from olympia.ratings.models import Rating
|
||||
|
||||
from ..cinder import CinderAddon, CinderUnauthenticatedReporter, CinderUser
|
||||
from ..cinder import (
|
||||
CinderAddon,
|
||||
CinderRating,
|
||||
CinderUnauthenticatedReporter,
|
||||
CinderUser,
|
||||
)
|
||||
|
||||
|
||||
class BaseTestCinderCase:
|
||||
cinder_class = None # Override in child classes
|
||||
|
||||
def _create_dummy_target(self, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _test_report(self, cinder_instance):
|
||||
responses.add(
|
||||
responses.POST,
|
||||
|
@ -54,9 +65,12 @@ class BaseTestCinderCase:
|
|||
with self.assertRaises(ConnectionError):
|
||||
cinder_instance.report(report_text='reason', category=None, reporter=None)
|
||||
|
||||
def test_report(self):
|
||||
def test_build_report_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def test_report(self):
|
||||
self._test_report(self.cinder_class(self._create_dummy_target()))
|
||||
|
||||
def _test_appeal(self, appealer):
|
||||
fake_decision_id = 'decision-id-to-appeal-666'
|
||||
cinder_instance = self.cinder_class(self._create_dummy_target())
|
||||
|
@ -252,9 +266,6 @@ class TestCinderAddon(BaseTestCinderCase, TestCase):
|
|||
],
|
||||
}
|
||||
|
||||
def test_report(self):
|
||||
self._test_report(self.cinder_class(self._create_dummy_target()))
|
||||
|
||||
|
||||
class TestCinderUser(BaseTestCinderCase, TestCase):
|
||||
cinder_class = CinderUser
|
||||
|
@ -417,5 +428,53 @@ class TestCinderUser(BaseTestCinderCase, TestCase):
|
|||
],
|
||||
}
|
||||
|
||||
def test_report(self):
|
||||
self._test_report(self.cinder_class(self._create_dummy_target()))
|
||||
|
||||
class TestCinderRating(BaseTestCinderCase, TestCase):
|
||||
cinder_class = CinderRating
|
||||
|
||||
def setUp(self):
|
||||
self.user = user_factory()
|
||||
self.addon = addon_factory()
|
||||
|
||||
def _create_dummy_target(self, **kwargs):
|
||||
return Rating.objects.create(addon=self.addon, user=self.user, **kwargs)
|
||||
|
||||
def test_build_report_payload(self):
|
||||
rating = self._create_dummy_target()
|
||||
cinder_rating = self.cinder_class(rating)
|
||||
reason = 'bad rating!'
|
||||
|
||||
data = cinder_rating.build_report_payload(
|
||||
report_text=reason, category=None, reporter=None
|
||||
)
|
||||
assert data == {
|
||||
'queue_slug': 'amo-content-infringement',
|
||||
'entity_type': 'amo_rating',
|
||||
'entity': {
|
||||
'id': str(rating.id),
|
||||
'body': rating.body,
|
||||
},
|
||||
'reasoning': reason,
|
||||
'context': {
|
||||
'entities': [
|
||||
{
|
||||
'entity_type': 'amo_user',
|
||||
'attributes': {
|
||||
'id': str(self.user.id),
|
||||
'name': self.user.display_name,
|
||||
'email': self.user.email,
|
||||
'fxa_id': self.user.fxa_id,
|
||||
},
|
||||
}
|
||||
],
|
||||
'relationships': [
|
||||
{
|
||||
'source_id': str(self.user.id),
|
||||
'source_type': 'amo_user',
|
||||
'target_id': str(rating.id),
|
||||
'target_type': 'amo_rating',
|
||||
'relationship_type': 'amo_rating_author_of',
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import responses
|
||||
|
||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||
from olympia.ratings.models import Rating
|
||||
|
||||
from ..cinder import CinderAddon, CinderUser
|
||||
from ..cinder import CinderAddon, CinderRating, CinderUser
|
||||
from ..models import AbuseReport, CinderReport
|
||||
|
||||
|
||||
|
@ -238,6 +240,36 @@ class TestAbuse(TestCase):
|
|||
report.update(guid=None, user=user)
|
||||
assert report.target == user
|
||||
|
||||
rating = Rating.objects.create(user=user, addon=addon, rating=5)
|
||||
report.update(user=None, rating=rating)
|
||||
assert report.target == rating
|
||||
|
||||
def test_constraint(self):
|
||||
report = AbuseReport()
|
||||
constraints = report.get_constraints()
|
||||
assert len(constraints) == 1
|
||||
constraint = constraints[0][1][0]
|
||||
|
||||
# ooooh addon is wrong.
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
constraint.validate(AbuseReport, report)
|
||||
|
||||
report.user_id = 48151
|
||||
constraint.validate(AbuseReport, report)
|
||||
|
||||
report.guid = '@guid'
|
||||
with self.assertRaises(ValidationError):
|
||||
constraint.validate(AbuseReport, report)
|
||||
|
||||
report.guid = None
|
||||
report.rating_id = 62342
|
||||
with self.assertRaises(ValidationError):
|
||||
constraint.validate(AbuseReport, report)
|
||||
|
||||
report.user_id = None
|
||||
constraint.validate(AbuseReport, report)
|
||||
|
||||
|
||||
class TestAbuseManager(TestCase):
|
||||
def test_deleted(self):
|
||||
|
@ -293,6 +325,12 @@ class TestCinderReport(TestCase):
|
|||
assert isinstance(helper, CinderUser)
|
||||
assert helper.user == user
|
||||
|
||||
rating = Rating.objects.create(addon=addon, user=user, rating=4)
|
||||
cinder_report.abuse_report.update(user=None, rating=rating)
|
||||
helper = cinder_report.get_helper()
|
||||
assert isinstance(helper, CinderRating)
|
||||
assert helper.rating == rating
|
||||
|
||||
def test_report(self):
|
||||
cinder_report = CinderReport.objects.create(
|
||||
abuse_report=AbuseReport.objects.create(
|
||||
|
|
|
@ -10,10 +10,12 @@ from olympia import amo
|
|||
from olympia.abuse.models import AbuseReport
|
||||
from olympia.abuse.serializers import (
|
||||
AddonAbuseReportSerializer,
|
||||
RatingAbuseReportSerializer,
|
||||
UserAbuseReportSerializer,
|
||||
)
|
||||
from olympia.accounts.serializers import BaseUserSerializer
|
||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||
from olympia.ratings.models import Rating
|
||||
|
||||
|
||||
class TestAddonAbuseReportSerializer(TestCase):
|
||||
|
@ -26,8 +28,8 @@ class TestAddonAbuseReportSerializer(TestCase):
|
|||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_guid.return_value = addon.guid
|
||||
view.get_addon_object.return_value.slug = addon.slug
|
||||
view.get_addon_object.return_value.pk = addon.pk
|
||||
view.get_target_object.return_value.slug = addon.slug
|
||||
view.get_target_object.return_value.pk = addon.pk
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
|
@ -124,7 +126,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
|||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_guid.return_value = '@someguid'
|
||||
view.get_addon_object.return_value = None
|
||||
view.get_target_object.return_value = None
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
|
@ -186,7 +188,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
|||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_guid.return_value = '@someguid'
|
||||
view.get_addon_object.return_value = None
|
||||
view.get_target_object.return_value = None
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
|
@ -221,7 +223,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
|||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_guid.return_value = '@someguid'
|
||||
view.get_addon_object.return_value = None
|
||||
view.get_target_object.return_value = None
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
|
@ -239,7 +241,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
|||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_guid.return_value = '@someguid'
|
||||
view.get_addon_object.return_value = None
|
||||
view.get_target_object.return_value = None
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
|
@ -266,3 +268,37 @@ class TestUserAbuseReportSerializer(TestCase):
|
|||
'user': serialized_user,
|
||||
'message': 'bad stuff',
|
||||
}
|
||||
|
||||
|
||||
class TestRatingAbuseReportSerializer(TestCase):
|
||||
def serialize(self, report, context=None):
|
||||
return dict(RatingAbuseReportSerializer(report, context=context or {}).data)
|
||||
|
||||
def test_user_report(self):
|
||||
user = user_factory()
|
||||
addon = addon_factory()
|
||||
rating = Rating.objects.create(
|
||||
body='evil rating', addon=addon, user=user, rating=1
|
||||
)
|
||||
report = AbuseReport(
|
||||
rating=rating, message='bad stuff', reason=AbuseReport.REASONS.ILLEGAL
|
||||
)
|
||||
request = RequestFactory().get('/')
|
||||
request.user = AnonymousUser()
|
||||
view = Mock()
|
||||
view.get_target_object.return_value = rating
|
||||
context = {
|
||||
'request': request,
|
||||
'view': view,
|
||||
}
|
||||
serialized = self.serialize(report, context=context)
|
||||
assert serialized == {
|
||||
'reporter': None,
|
||||
'reporter_email': None,
|
||||
'reporter_name': None,
|
||||
'rating': {
|
||||
'id': rating.pk,
|
||||
},
|
||||
'reason': 'illegal',
|
||||
'message': 'bad stuff',
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ from olympia.amo.tests import (
|
|||
reverse_ns,
|
||||
user_factory,
|
||||
)
|
||||
from olympia.ratings.models import Rating
|
||||
|
||||
|
||||
class AddonAbuseViewSetTestBase:
|
||||
|
@ -45,7 +46,7 @@ class AddonAbuseViewSetTestBase:
|
|||
# It was a public add-on, so we found its guid.
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == 'abuse!'
|
||||
|
||||
def test_report_addon_by_slug(self):
|
||||
|
@ -61,7 +62,7 @@ class AddonAbuseViewSetTestBase:
|
|||
# It was a public add-on, so we found its guid.
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
|
||||
def test_report_addon_by_guid(self):
|
||||
addon = addon_factory(guid='@badman')
|
||||
|
@ -75,7 +76,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == 'abuse!'
|
||||
|
||||
def test_report_addon_by_id_not_public(self):
|
||||
|
@ -112,7 +113,7 @@ class AddonAbuseViewSetTestBase:
|
|||
# simply inexistant (see test below) since we're not linking them
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == 'abuse!'
|
||||
|
||||
def test_report_addon_guid_not_on_amo(self):
|
||||
|
@ -159,7 +160,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == 'abuse!'
|
||||
|
||||
def test_no_addon_fails(self):
|
||||
|
@ -195,7 +196,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == ''
|
||||
|
||||
def test_message_can_be_blank_if_reason_is_provided(self):
|
||||
|
@ -210,7 +211,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.message == ''
|
||||
|
||||
def test_message_length_limited(self):
|
||||
|
@ -427,7 +428,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.addon_install_method == (AbuseReport.ADDON_INSTALL_METHODS.OTHER)
|
||||
assert report.addon_install_source == (AbuseReport.ADDON_INSTALL_SOURCES.OTHER)
|
||||
|
||||
|
@ -444,7 +445,7 @@ class AddonAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||
report = AbuseReport.objects.get(guid=addon.guid)
|
||||
self.check_report(report, 'Abuse Report for Addon %s' % addon.guid)
|
||||
self.check_report(report, f'Abuse Report for Addon {addon.guid}')
|
||||
assert report.country_code == 'YY'
|
||||
|
||||
def test_abuse_report_with_invalid_data(self):
|
||||
|
@ -471,19 +472,19 @@ class AddonAbuseViewSetTestBase:
|
|||
)
|
||||
assert response.status_code == 201, response.content
|
||||
|
||||
@mock.patch('olympia.abuse.serializers.report_to_cinder.delay')
|
||||
@mock.patch('olympia.abuse.tasks.report_to_cinder.delay')
|
||||
@override_switch('enable-cinder-reporting', active=True)
|
||||
def test_reportable_reason_calls_cinder_task(self, task_mock):
|
||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||
task_mock.assert_called()
|
||||
|
||||
@mock.patch('olympia.abuse.serializers.report_to_cinder.delay')
|
||||
@mock.patch('olympia.abuse.tasks.report_to_cinder.delay')
|
||||
@override_switch('enable-cinder-reporting', active=False)
|
||||
def test_reportable_reason_does_not_call_cinder_with_waffle_off(self, task_mock):
|
||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||
task_mock.assert_not_called()
|
||||
|
||||
@mock.patch('olympia.abuse.serializers.report_to_cinder.delay')
|
||||
@mock.patch('olympia.abuse.tasks.report_to_cinder.delay')
|
||||
@override_switch('enable-cinder-reporting', active=True)
|
||||
def test_not_reportable_reason_does_not_call_cinder_task(self, task_mock):
|
||||
self._setup_reportable_reason('feedback_spam')
|
||||
|
@ -553,7 +554,7 @@ class UserAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||
report = AbuseReport.objects.get(user_id=user.id)
|
||||
self.check_report(report, 'Abuse Report for User %s' % user)
|
||||
self.check_report(report, f'Abuse Report for User {user.pk}')
|
||||
|
||||
def test_report_user_username(self):
|
||||
user = user_factory()
|
||||
|
@ -566,7 +567,7 @@ class UserAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||
report = AbuseReport.objects.get(user_id=user.id)
|
||||
self.check_report(report, 'Abuse Report for User %s' % user)
|
||||
self.check_report(report, f'Abuse Report for User {user.pk}')
|
||||
|
||||
def test_no_user_fails(self):
|
||||
response = self.client.post(self.url, data={'message': 'abuse!'})
|
||||
|
@ -620,7 +621,7 @@ class UserAbuseViewSetTestBase:
|
|||
|
||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||
report = AbuseReport.objects.get(user_id=user.id)
|
||||
self.check_report(report, 'Abuse Report for User %s' % user)
|
||||
self.check_report(report, f'Abuse Report for User {user.pk}')
|
||||
assert report.country_code == 'YY'
|
||||
|
||||
|
||||
|
@ -663,6 +664,198 @@ class TestUserAbuseViewSetLoggedIn(UserAbuseViewSetTestBase, TestCase):
|
|||
assert response.status_code == 429
|
||||
|
||||
|
||||
class RatingAbuseViewSetTestBase:
|
||||
client_class = APITestClientSessionID
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse_ns('abusereportrating-list')
|
||||
|
||||
def check_reporter(self, report):
|
||||
raise NotImplementedError
|
||||
|
||||
def check_report(self, report, text):
|
||||
assert str(report) == text
|
||||
self.check_reporter(report)
|
||||
|
||||
def test_report_rating_id(self):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
assert AbuseReport.objects.filter(rating_id=target_rating.pk).exists()
|
||||
report = AbuseReport.objects.get(rating=target_rating)
|
||||
self.check_report(report, f'Abuse Report for Rating {target_rating.pk}')
|
||||
|
||||
def test_no_rating_fails(self):
|
||||
response = self.client.post(
|
||||
self.url, data={'message': 'abuse!', 'reason': 'illegal'}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert json.loads(response.content) == {'rating': ['This field is required.']}
|
||||
|
||||
def test_reason_required(self):
|
||||
pass
|
||||
|
||||
def test_reason_only_ratings_ones(self):
|
||||
pass
|
||||
|
||||
def test_message_required_empty(self):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={'rating': str(target_rating.pk), 'message': '', 'reason': 'illegal'},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert json.loads(response.content) == {
|
||||
'message': ['This field may not be blank.']
|
||||
}
|
||||
|
||||
def test_message_required_missing(self):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
response = self.client.post(
|
||||
self.url, data={'rating': str(target_rating.pk), 'reason': 'illegal'}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert json.loads(response.content) == {'message': ['This field is required.']}
|
||||
|
||||
def test_throttle(self):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
for x in range(20):
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_FORWARDED_FOR=f'123.45.67.89, {get_random_ip()}',
|
||||
)
|
||||
assert response.status_code == 201, x
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_FORWARDED_FOR=f'123.45.67.89, {get_random_ip()}',
|
||||
)
|
||||
assert response.status_code == 429
|
||||
|
||||
def test_report_country_code(self):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_COUNTRY_CODE='YY',
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
assert AbuseReport.objects.filter(rating_id=target_rating.pk).exists()
|
||||
report = AbuseReport.objects.get(rating=target_rating)
|
||||
self.check_report(report, f'Abuse Report for Rating {target_rating.pk}')
|
||||
assert report.country_code == 'YY'
|
||||
|
||||
def _setup_reportable_reason(self, reason):
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={'rating': target_rating.pk, 'reason': reason, 'message': 'bad!'},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_FORWARDED_FOR=f'123.45.67.89, {get_random_ip()}',
|
||||
)
|
||||
assert response.status_code == 201, response.content
|
||||
|
||||
@mock.patch('olympia.abuse.tasks.report_to_cinder.delay')
|
||||
@override_switch('enable-cinder-reporting', active=True)
|
||||
def test_reportable_reason_calls_cinder_task(self, task_mock):
|
||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||
task_mock.assert_called()
|
||||
|
||||
@mock.patch('olympia.abuse.tasks.report_to_cinder.delay')
|
||||
@override_switch('enable-cinder-reporting', active=False)
|
||||
def test_reportable_reason_does_not_call_cinder_with_waffle_off(self, task_mock):
|
||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||
task_mock.assert_not_called()
|
||||
|
||||
|
||||
class TestRatingAbuseViewSetLoggedOut(RatingAbuseViewSetTestBase, TestCase):
|
||||
def check_reporter(self, report):
|
||||
assert not report.reporter
|
||||
|
||||
|
||||
class TestRatingAbuseViewSetLoggedIn(RatingAbuseViewSetTestBase, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = user_factory()
|
||||
self.client.login_api(self.user)
|
||||
|
||||
def check_reporter(self, report):
|
||||
assert report.reporter == self.user
|
||||
|
||||
def test_throttle_ip_for_authenticated_users(self):
|
||||
user = user_factory()
|
||||
self.client.login_api(user)
|
||||
target_rating = Rating.objects.create(
|
||||
addon=addon_factory(), user=user_factory(), body='Booh', rating=1
|
||||
)
|
||||
for x in range(20):
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_FORWARDED_FOR=f'123.45.67.89, {get_random_ip()}',
|
||||
)
|
||||
assert response.status_code == 201, x
|
||||
|
||||
# Different user, same IP: should still be blocked (> 20 / day).
|
||||
new_user = user_factory()
|
||||
self.client.login_api(new_user)
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={
|
||||
'rating': str(target_rating.pk),
|
||||
'message': 'abuse!',
|
||||
'reason': 'illegal',
|
||||
},
|
||||
REMOTE_ADDR='123.45.67.89',
|
||||
HTTP_X_FORWARDED_FOR=f'123.45.67.89, {get_random_ip()}',
|
||||
)
|
||||
assert response.status_code == 429
|
||||
|
||||
|
||||
class TestAppeal(TestCase):
|
||||
def setUp(self):
|
||||
self.addon = addon_factory()
|
||||
|
|
|
@ -24,6 +24,7 @@ from olympia.abuse.forms import AbuseAppealEmailForm, AbuseAppealForm
|
|||
from olympia.abuse.models import CinderReport
|
||||
from olympia.abuse.serializers import (
|
||||
AddonAbuseReportSerializer,
|
||||
RatingAbuseReportSerializer,
|
||||
UserAbuseReportSerializer,
|
||||
)
|
||||
from olympia.abuse.tasks import appeal_to_cinder
|
||||
|
@ -31,6 +32,7 @@ from olympia.accounts.utils import redirect_for_login
|
|||
from olympia.accounts.views import AccountViewSet
|
||||
from olympia.addons.views import AddonViewSet
|
||||
from olympia.api.throttling import GranularIPRateThrottle, GranularUserRateThrottle
|
||||
from olympia.ratings.views import RatingViewSet
|
||||
|
||||
from .cinder import Cinder
|
||||
|
||||
|
@ -48,35 +50,56 @@ class AbuseIPThrottle(GranularIPRateThrottle):
|
|||
scope = 'ip_abuse'
|
||||
|
||||
|
||||
class AddonAbuseViewSet(CreateModelMixin, GenericViewSet):
|
||||
class AbuseTargetMixin:
|
||||
target_viewset_class = None # Implement in child classes
|
||||
target_viewset_action = None # Implement in child classes or leave None
|
||||
target_parameter_name = None # Implement in child classes (must match serializer)
|
||||
|
||||
def get_target_viewset(self):
|
||||
if hasattr(self, 'target_viewset'):
|
||||
return self.target_viewset
|
||||
|
||||
if 'target_pk' not in self.kwargs:
|
||||
self.kwargs['target_pk'] = self.request.data.get(
|
||||
self.target_parameter_name
|
||||
) or self.request.GET.get(self.target_parameter_name)
|
||||
|
||||
self.target_viewset = self.target_viewset_class(
|
||||
request=self.request,
|
||||
permission_classes=[],
|
||||
kwargs={'pk': self.kwargs['target_pk']},
|
||||
action=self.target_viewset_action,
|
||||
)
|
||||
return self.target_viewset
|
||||
|
||||
# This method is used by the serializer.
|
||||
def get_target_object(self):
|
||||
if hasattr(self, 'target_object'):
|
||||
return self.target_object
|
||||
|
||||
self.target_object = self.get_target_viewset().get_object()
|
||||
return self.target_object
|
||||
|
||||
|
||||
class AddonAbuseViewSet(AbuseTargetMixin, CreateModelMixin, GenericViewSet):
|
||||
permission_classes = []
|
||||
serializer_class = AddonAbuseReportSerializer
|
||||
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
||||
target_viewset_class = AddonViewSet
|
||||
target_viewset_action = 'retrieve_from_related'
|
||||
target_parameter_name = 'addon'
|
||||
|
||||
def get_addon_viewset(self):
|
||||
if hasattr(self, 'addon_viewset'):
|
||||
return self.addon_viewset
|
||||
|
||||
if 'addon_pk' not in self.kwargs:
|
||||
self.kwargs['addon_pk'] = self.request.data.get(
|
||||
'addon'
|
||||
) or self.request.GET.get('addon')
|
||||
self.addon_viewset = AddonViewSet(
|
||||
request=self.request,
|
||||
permission_classes=[],
|
||||
kwargs={'pk': self.kwargs['addon_pk']},
|
||||
action='retrieve_from_related',
|
||||
)
|
||||
return self.addon_viewset
|
||||
|
||||
def get_addon_object(self):
|
||||
if hasattr(self, 'addon_object'):
|
||||
return self.addon_object
|
||||
|
||||
self.addon_object = self.get_addon_viewset().get_object()
|
||||
if self.addon_object and not self.addon_object.is_public():
|
||||
def get_target_object(self):
|
||||
super().get_target_object()
|
||||
# It's possible to submit reports against non-public add-ons via guid,
|
||||
# but we can't reveal their slug/pk and can't accept their slug/pk as
|
||||
# input either. We raise a 404 if get_target_object() is called for a
|
||||
# non-public add-on: the view avoids calling it when a guid is passed
|
||||
# (see get_guid()) and the serializer will handle the Http404 when
|
||||
# returning the internal value (see get_addon() in serializer).
|
||||
if self.target_object and not self.target_object.is_public():
|
||||
raise Http404
|
||||
return self.addon_object
|
||||
return self.target_object
|
||||
|
||||
def get_guid(self):
|
||||
"""
|
||||
|
@ -91,36 +114,34 @@ class AddonAbuseViewSet(CreateModelMixin, GenericViewSet):
|
|||
look like a guid and there is no public add-on with a matching slug or
|
||||
pk.
|
||||
"""
|
||||
if self.get_addon_viewset().get_lookup_field(self.kwargs['addon_pk']) == 'guid':
|
||||
guid = self.kwargs['addon_pk']
|
||||
if (
|
||||
self.get_target_viewset().get_lookup_field(self.kwargs['target_pk'])
|
||||
== 'guid'
|
||||
):
|
||||
guid = self.kwargs['target_pk']
|
||||
else:
|
||||
# At this point the parameter is a slug or pk. For backwards-compatibility
|
||||
# we accept that, but ultimately record only the guid.
|
||||
self.get_addon_object()
|
||||
if self.addon_object:
|
||||
guid = self.addon_object.guid
|
||||
self.get_target_object()
|
||||
if self.target_object:
|
||||
guid = self.target_object.guid
|
||||
return guid
|
||||
|
||||
|
||||
class UserAbuseViewSet(CreateModelMixin, GenericViewSet):
|
||||
class UserAbuseViewSet(AbuseTargetMixin, CreateModelMixin, GenericViewSet):
|
||||
permission_classes = []
|
||||
serializer_class = UserAbuseReportSerializer
|
||||
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
||||
target_viewset_class = AccountViewSet
|
||||
target_parameter_name = 'user'
|
||||
|
||||
def get_user_object(self):
|
||||
if hasattr(self, 'user_object'):
|
||||
return self.user_object
|
||||
|
||||
if 'user_pk' not in self.kwargs:
|
||||
self.kwargs['user_pk'] = self.request.data.get(
|
||||
'user'
|
||||
) or self.request.GET.get('user')
|
||||
|
||||
return AccountViewSet(
|
||||
request=self.request,
|
||||
permission_classes=[],
|
||||
kwargs={'pk': self.kwargs['user_pk']},
|
||||
).get_object()
|
||||
class RatingAbuseViewSet(AbuseTargetMixin, CreateModelMixin, GenericViewSet):
|
||||
permission_classes = []
|
||||
serializer_class = RatingAbuseReportSerializer
|
||||
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
||||
target_viewset_class = RatingViewSet
|
||||
target_parameter_name = 'rating'
|
||||
|
||||
|
||||
class CinderInboundPermission:
|
||||
|
@ -222,9 +243,10 @@ def appeal(request, *, decision_id, **kwargs):
|
|||
allowed_users = []
|
||||
if hasattr(abuse_report.target, 'authors'):
|
||||
allowed_users = abuse_report.target.authors.all()
|
||||
# FIXME: when we implement collections in abuse reports
|
||||
# elif hasattr(abuse_report.target, 'author'):
|
||||
# allowed_users = [abuse_report.target.author]
|
||||
elif hasattr(abuse_report.target, 'author'):
|
||||
allowed_users = [abuse_report.target.author]
|
||||
elif hasattr(abuse_report.target, 'user'):
|
||||
allowed_users = [abuse_report.target.user]
|
||||
valid_user_or_email_provided = request.user in allowed_users
|
||||
|
||||
if not valid_user_or_email_provided and not appeal_email_form:
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.6 on 2023-10-27 10:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import olympia.ratings.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('ratings', '0008_alter_deniedratingword_options_alter_ratingflag_flag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='deniedratingword',
|
||||
name='word',
|
||||
field=models.CharField(
|
||||
help_text='Can only contain alphanumeric characters ("\\w", exc. "_"). If contains a "." it will be interpreted as a domain name instead, and can contain any character',
|
||||
max_length=255,
|
||||
unique=True,
|
||||
validators=[olympia.ratings.models.word_validator],
|
||||
),
|
||||
),
|
||||
]
|
|
@ -29,8 +29,6 @@ urlpatterns = [
|
|||
# Home.
|
||||
re_path(r'^$', frontend_view, name='home'),
|
||||
# Abuse.
|
||||
# FIXME: modify nginx config to route /<locale>/abuse/ to addons-server
|
||||
# This will need to not clash with new frontend reporting pages !
|
||||
re_path(r'abuse/', include('olympia.abuse.urls')),
|
||||
# Add-ons.
|
||||
re_path(r'', include('olympia.addons.urls')),
|
||||
|
@ -109,7 +107,7 @@ urlpatterns = [
|
|||
),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
|
||||
# Remove leading and trailing slashes so the regex matches.
|
||||
|
|
Загрузка…
Ссылка в новой задаче