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 install_date: The add-on install date.
|
||||||
:<json string|null operating_system: The client's operating system.
|
:<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 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 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 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.name: The name of the user who submitted the report.
|
||||||
|
@ -191,9 +193,9 @@ to if necessary.
|
||||||
privileged Privileged
|
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
|
Value Description
|
||||||
|
@ -205,6 +207,10 @@ to if necessary.
|
||||||
policy Hateful, violent, or illegal content
|
policy Hateful, violent, or illegal content
|
||||||
deceptive Doesn't match description
|
deceptive Doesn't match description
|
||||||
unwanted Wasn't wanted / impossible to get rid of
|
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
|
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 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 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 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 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.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.url: The link to the profile page for of the user reported.
|
||||||
:>json string user.username: The username of the user reported.
|
:>json string user.username: The username of the user reported.
|
||||||
:>json string message: The body/content of the abuse report.
|
:>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 rest_framework.routers import SimpleRouter
|
||||||
|
|
||||||
from .views import AddonAbuseViewSet, UserAbuseViewSet, cinder_webhook
|
from .views import (
|
||||||
|
AddonAbuseViewSet,
|
||||||
|
RatingAbuseViewSet,
|
||||||
|
UserAbuseViewSet,
|
||||||
|
cinder_webhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
reporting = SimpleRouter()
|
reporting = SimpleRouter()
|
||||||
reporting.register(r'addon', AddonAbuseViewSet, basename='abusereportaddon')
|
reporting.register(r'addon', AddonAbuseViewSet, basename='abusereportaddon')
|
||||||
|
reporting.register(r'rating', RatingAbuseViewSet, basename='abusereportrating')
|
||||||
reporting.register(r'user', UserAbuseViewSet, basename='abusereportuser')
|
reporting.register(r'user', UserAbuseViewSet, basename='abusereportuser')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -109,11 +109,14 @@ class CinderUser(Cinder):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_context(self):
|
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 {
|
return {
|
||||||
'entities': [addon.get_entity_data() for addon in addons],
|
'entities': [
|
||||||
|
cinder_addon.get_entity_data() for cinder_addon in cinder_addons
|
||||||
|
],
|
||||||
'relationships': [
|
'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):
|
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 {
|
return {
|
||||||
'entities': [author.get_entity_data() for author in authors],
|
'entities': [cinder_user.get_entity_data() for cinder_user in cinder_users],
|
||||||
'relationships': [
|
'relationships': [
|
||||||
author.get_relationship_data(self, 'amo_author_of')
|
cinder_user.get_relationship_data(self, 'amo_author_of')
|
||||||
for author in authors
|
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.addons.models import Addon
|
||||||
from olympia.amo.models import BaseQuerySet, ManagerBase, ModelBase
|
from olympia.amo.models import BaseQuerySet, ManagerBase, ModelBase
|
||||||
from olympia.api.utils import APIChoices, APIChoicesWithNone
|
from olympia.api.utils import APIChoices, APIChoicesWithNone
|
||||||
|
from olympia.ratings.models import Rating
|
||||||
from olympia.users.models import UserProfile
|
from olympia.users.models import UserProfile
|
||||||
|
|
||||||
from .cinder import CinderAddon, CinderUnauthenticatedReporter, CinderUser
|
from .cinder import CinderAddon, CinderRating, CinderUnauthenticatedReporter, CinderUser
|
||||||
|
|
||||||
|
|
||||||
class AbuseReportQuerySet(BaseQuerySet):
|
class AbuseReportQuerySet(BaseQuerySet):
|
||||||
|
@ -107,11 +108,13 @@ class AbuseReport(ModelBase):
|
||||||
),
|
),
|
||||||
('OTHER', 127, 'Other'),
|
('OTHER', 127, 'Other'),
|
||||||
)
|
)
|
||||||
REPORTABLE_REASONS = (
|
REASONS.add_subset(
|
||||||
REASONS.HATEFUL_VIOLENT_DECEPTIVE,
|
'RATING_REASONS', ('HATEFUL_VIOLENT_DECEPTIVE', 'ILLEGAL', 'OTHER')
|
||||||
REASONS.ILLEGAL,
|
)
|
||||||
REASONS.POLICY_VIOLATION,
|
# Those reasons will be reported to Cinder.
|
||||||
REASONS.OTHER,
|
REASONS.add_subset(
|
||||||
|
'REPORTABLE_REASONS',
|
||||||
|
('HATEFUL_VIOLENT_DECEPTIVE', 'ILLEGAL', 'POLICY_VIOLATION', 'OTHER'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://searchfox.org
|
# https://searchfox.org
|
||||||
|
@ -204,13 +207,17 @@ class AbuseReport(ModelBase):
|
||||||
reporter_email = models.CharField(max_length=255, default=None, null=True)
|
reporter_email = models.CharField(max_length=255, default=None, null=True)
|
||||||
reporter_name = 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)
|
country_code = models.CharField(max_length=2, default=None, null=True)
|
||||||
# An abuse report can be for an addon or a user.
|
# An abuse report can be for an addon, a user or a rating.
|
||||||
# If user is set then guid should be null.
|
# - If user is set then guid and rating should be null.
|
||||||
# If user is null then guid should be set.
|
# - 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)
|
guid = models.CharField(max_length=255, null=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL
|
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)
|
message = models.TextField(blank=True)
|
||||||
|
|
||||||
state = models.PositiveSmallIntegerField(
|
state = models.PositiveSmallIntegerField(
|
||||||
|
@ -295,16 +302,23 @@ class AbuseReport(ModelBase):
|
||||||
]
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
name='just_one_of_guid_and_user_must_be_set',
|
name='just_one_of_guid_user_rating_must_be_set',
|
||||||
check=(
|
check=(
|
||||||
models.Q(
|
models.Q(
|
||||||
~models.Q(guid=''),
|
~models.Q(guid=''),
|
||||||
guid__isnull=False,
|
guid__isnull=False,
|
||||||
user__isnull=True,
|
user__isnull=True,
|
||||||
|
rating__isnull=True,
|
||||||
)
|
)
|
||||||
| models.Q(
|
| models.Q(
|
||||||
guid__isnull=True,
|
guid__isnull=True,
|
||||||
user__isnull=False,
|
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):
|
def type(self):
|
||||||
if self.guid:
|
if self.guid:
|
||||||
type_ = 'Addon'
|
type_ = 'Addon'
|
||||||
else:
|
elif self.user_id:
|
||||||
type_ = 'User'
|
type_ = 'User'
|
||||||
|
elif self.rating_id:
|
||||||
|
type_ = 'Rating'
|
||||||
|
else:
|
||||||
|
type_ = 'Unknown'
|
||||||
return type_
|
return type_
|
||||||
|
|
||||||
def __str__(self):
|
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}'
|
return f'Abuse Report for {self.type} {name}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -336,8 +354,10 @@ class AbuseReport(ModelBase):
|
||||||
|
|
||||||
if self.guid:
|
if self.guid:
|
||||||
return Addon.unfiltered.filter(guid=self.guid).first()
|
return Addon.unfiltered.filter(guid=self.guid).first()
|
||||||
elif self.user:
|
elif self.user_id:
|
||||||
return self.user
|
return self.user
|
||||||
|
elif self.rating_id:
|
||||||
|
return self.rating
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -385,7 +405,8 @@ class CinderReport(ModelBase):
|
||||||
return CinderAddon(target)
|
return CinderAddon(target)
|
||||||
elif isinstance(target, UserProfile):
|
elif isinstance(target, UserProfile):
|
||||||
return CinderUser(target)
|
return CinderUser(target)
|
||||||
# TODO: More helpers here
|
elif isinstance(target, Rating):
|
||||||
|
return CinderRating(target)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_cinder_reporter(self):
|
def get_cinder_reporter(self):
|
||||||
|
|
|
@ -38,6 +38,16 @@ class BaseAbuseReportSerializer(AMOModelSerializer):
|
||||||
output['reporter'] = request.user
|
output['reporter'] = request.user
|
||||||
return output
|
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):
|
class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
|
@ -194,7 +204,7 @@ class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||||
}
|
}
|
||||||
if view := self.context.get('view'):
|
if view := self.context.get('view'):
|
||||||
try:
|
try:
|
||||||
addon = view.get_addon_object()
|
addon = view.get_target_object()
|
||||||
output['id'] = addon.pk
|
output['id'] = addon.pk
|
||||||
output['slug'] = addon.slug
|
output['slug'] = addon.slug
|
||||||
except Http404:
|
except Http404:
|
||||||
|
@ -204,16 +214,6 @@ class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||||
pass
|
pass
|
||||||
return output
|
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):
|
class UserAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||||
user = BaseUserSerializer(required=False) # We validate it ourselves.
|
user = BaseUserSerializer(required=False) # We validate it ourselves.
|
||||||
|
@ -228,9 +228,36 @@ class UserAbuseReportSerializer(BaseAbuseReportSerializer):
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
view = self.context.get('view')
|
view = self.context.get('view')
|
||||||
self.validate_target(data, 'user')
|
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
|
# Pop 'user' before passing it to super(), we already have the
|
||||||
# output value and did the validation above.
|
# output value and did the validation above.
|
||||||
data.pop('user')
|
data.pop('user')
|
||||||
output.update(super().to_internal_value(data))
|
output.update(super().to_internal_value(data))
|
||||||
return output
|
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
|
import responses
|
||||||
|
|
||||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
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:
|
class BaseTestCinderCase:
|
||||||
|
cinder_class = None # Override in child classes
|
||||||
|
|
||||||
|
def _create_dummy_target(self, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def _test_report(self, cinder_instance):
|
def _test_report(self, cinder_instance):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
|
@ -54,9 +65,12 @@ class BaseTestCinderCase:
|
||||||
with self.assertRaises(ConnectionError):
|
with self.assertRaises(ConnectionError):
|
||||||
cinder_instance.report(report_text='reason', category=None, reporter=None)
|
cinder_instance.report(report_text='reason', category=None, reporter=None)
|
||||||
|
|
||||||
def test_report(self):
|
def test_build_report_payload(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def test_report(self):
|
||||||
|
self._test_report(self.cinder_class(self._create_dummy_target()))
|
||||||
|
|
||||||
def _test_appeal(self, appealer):
|
def _test_appeal(self, appealer):
|
||||||
fake_decision_id = 'decision-id-to-appeal-666'
|
fake_decision_id = 'decision-id-to-appeal-666'
|
||||||
cinder_instance = self.cinder_class(self._create_dummy_target())
|
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):
|
class TestCinderUser(BaseTestCinderCase, TestCase):
|
||||||
cinder_class = CinderUser
|
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.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
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
|
from ..models import AbuseReport, CinderReport
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,6 +240,36 @@ class TestAbuse(TestCase):
|
||||||
report.update(guid=None, user=user)
|
report.update(guid=None, user=user)
|
||||||
assert report.target == 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):
|
class TestAbuseManager(TestCase):
|
||||||
def test_deleted(self):
|
def test_deleted(self):
|
||||||
|
@ -293,6 +325,12 @@ class TestCinderReport(TestCase):
|
||||||
assert isinstance(helper, CinderUser)
|
assert isinstance(helper, CinderUser)
|
||||||
assert helper.user == user
|
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):
|
def test_report(self):
|
||||||
cinder_report = CinderReport.objects.create(
|
cinder_report = CinderReport.objects.create(
|
||||||
abuse_report=AbuseReport.objects.create(
|
abuse_report=AbuseReport.objects.create(
|
||||||
|
|
|
@ -10,10 +10,12 @@ from olympia import amo
|
||||||
from olympia.abuse.models import AbuseReport
|
from olympia.abuse.models import AbuseReport
|
||||||
from olympia.abuse.serializers import (
|
from olympia.abuse.serializers import (
|
||||||
AddonAbuseReportSerializer,
|
AddonAbuseReportSerializer,
|
||||||
|
RatingAbuseReportSerializer,
|
||||||
UserAbuseReportSerializer,
|
UserAbuseReportSerializer,
|
||||||
)
|
)
|
||||||
from olympia.accounts.serializers import BaseUserSerializer
|
from olympia.accounts.serializers import BaseUserSerializer
|
||||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||||
|
from olympia.ratings.models import Rating
|
||||||
|
|
||||||
|
|
||||||
class TestAddonAbuseReportSerializer(TestCase):
|
class TestAddonAbuseReportSerializer(TestCase):
|
||||||
|
@ -26,8 +28,8 @@ class TestAddonAbuseReportSerializer(TestCase):
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
view = Mock()
|
view = Mock()
|
||||||
view.get_guid.return_value = addon.guid
|
view.get_guid.return_value = addon.guid
|
||||||
view.get_addon_object.return_value.slug = addon.slug
|
view.get_target_object.return_value.slug = addon.slug
|
||||||
view.get_addon_object.return_value.pk = addon.pk
|
view.get_target_object.return_value.pk = addon.pk
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -124,7 +126,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
view = Mock()
|
view = Mock()
|
||||||
view.get_guid.return_value = '@someguid'
|
view.get_guid.return_value = '@someguid'
|
||||||
view.get_addon_object.return_value = None
|
view.get_target_object.return_value = None
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -186,7 +188,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
view = Mock()
|
view = Mock()
|
||||||
view.get_guid.return_value = '@someguid'
|
view.get_guid.return_value = '@someguid'
|
||||||
view.get_addon_object.return_value = None
|
view.get_target_object.return_value = None
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -221,7 +223,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
view = Mock()
|
view = Mock()
|
||||||
view.get_guid.return_value = '@someguid'
|
view.get_guid.return_value = '@someguid'
|
||||||
view.get_addon_object.return_value = None
|
view.get_target_object.return_value = None
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -239,7 +241,7 @@ class TestAddonAbuseReportSerializer(TestCase):
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
view = Mock()
|
view = Mock()
|
||||||
view.get_guid.return_value = '@someguid'
|
view.get_guid.return_value = '@someguid'
|
||||||
view.get_addon_object.return_value = None
|
view.get_target_object.return_value = None
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -266,3 +268,37 @@ class TestUserAbuseReportSerializer(TestCase):
|
||||||
'user': serialized_user,
|
'user': serialized_user,
|
||||||
'message': 'bad stuff',
|
'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,
|
reverse_ns,
|
||||||
user_factory,
|
user_factory,
|
||||||
)
|
)
|
||||||
|
from olympia.ratings.models import Rating
|
||||||
|
|
||||||
|
|
||||||
class AddonAbuseViewSetTestBase:
|
class AddonAbuseViewSetTestBase:
|
||||||
|
@ -45,7 +46,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
# It was a public add-on, so we found its guid.
|
# It was a public add-on, so we found its guid.
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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!'
|
assert report.message == 'abuse!'
|
||||||
|
|
||||||
def test_report_addon_by_slug(self):
|
def test_report_addon_by_slug(self):
|
||||||
|
@ -61,7 +62,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
# It was a public add-on, so we found its guid.
|
# It was a public add-on, so we found its guid.
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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):
|
def test_report_addon_by_guid(self):
|
||||||
addon = addon_factory(guid='@badman')
|
addon = addon_factory(guid='@badman')
|
||||||
|
@ -75,7 +76,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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!'
|
assert report.message == 'abuse!'
|
||||||
|
|
||||||
def test_report_addon_by_id_not_public(self):
|
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
|
# simply inexistant (see test below) since we're not linking them
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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!'
|
assert report.message == 'abuse!'
|
||||||
|
|
||||||
def test_report_addon_guid_not_on_amo(self):
|
def test_report_addon_guid_not_on_amo(self):
|
||||||
|
@ -159,7 +160,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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!'
|
assert report.message == 'abuse!'
|
||||||
|
|
||||||
def test_no_addon_fails(self):
|
def test_no_addon_fails(self):
|
||||||
|
@ -195,7 +196,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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 == ''
|
assert report.message == ''
|
||||||
|
|
||||||
def test_message_can_be_blank_if_reason_is_provided(self):
|
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()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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 == ''
|
assert report.message == ''
|
||||||
|
|
||||||
def test_message_length_limited(self):
|
def test_message_length_limited(self):
|
||||||
|
@ -427,7 +428,7 @@ class AddonAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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_method == (AbuseReport.ADDON_INSTALL_METHODS.OTHER)
|
||||||
assert report.addon_install_source == (AbuseReport.ADDON_INSTALL_SOURCES.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()
|
assert AbuseReport.objects.filter(guid=addon.guid).exists()
|
||||||
report = AbuseReport.objects.get(guid=addon.guid)
|
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'
|
assert report.country_code == 'YY'
|
||||||
|
|
||||||
def test_abuse_report_with_invalid_data(self):
|
def test_abuse_report_with_invalid_data(self):
|
||||||
|
@ -471,19 +472,19 @@ class AddonAbuseViewSetTestBase:
|
||||||
)
|
)
|
||||||
assert response.status_code == 201, response.content
|
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)
|
@override_switch('enable-cinder-reporting', active=True)
|
||||||
def test_reportable_reason_calls_cinder_task(self, task_mock):
|
def test_reportable_reason_calls_cinder_task(self, task_mock):
|
||||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||||
task_mock.assert_called()
|
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)
|
@override_switch('enable-cinder-reporting', active=False)
|
||||||
def test_reportable_reason_does_not_call_cinder_with_waffle_off(self, task_mock):
|
def test_reportable_reason_does_not_call_cinder_with_waffle_off(self, task_mock):
|
||||||
self._setup_reportable_reason('hateful_violent_deceptive')
|
self._setup_reportable_reason('hateful_violent_deceptive')
|
||||||
task_mock.assert_not_called()
|
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)
|
@override_switch('enable-cinder-reporting', active=True)
|
||||||
def test_not_reportable_reason_does_not_call_cinder_task(self, task_mock):
|
def test_not_reportable_reason_does_not_call_cinder_task(self, task_mock):
|
||||||
self._setup_reportable_reason('feedback_spam')
|
self._setup_reportable_reason('feedback_spam')
|
||||||
|
@ -553,7 +554,7 @@ class UserAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||||
report = AbuseReport.objects.get(user_id=user.id)
|
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):
|
def test_report_user_username(self):
|
||||||
user = user_factory()
|
user = user_factory()
|
||||||
|
@ -566,7 +567,7 @@ class UserAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||||
report = AbuseReport.objects.get(user_id=user.id)
|
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):
|
def test_no_user_fails(self):
|
||||||
response = self.client.post(self.url, data={'message': 'abuse!'})
|
response = self.client.post(self.url, data={'message': 'abuse!'})
|
||||||
|
@ -620,7 +621,7 @@ class UserAbuseViewSetTestBase:
|
||||||
|
|
||||||
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
assert AbuseReport.objects.filter(user_id=user.id).exists()
|
||||||
report = AbuseReport.objects.get(user_id=user.id)
|
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'
|
assert report.country_code == 'YY'
|
||||||
|
|
||||||
|
|
||||||
|
@ -663,6 +664,198 @@ class TestUserAbuseViewSetLoggedIn(UserAbuseViewSetTestBase, TestCase):
|
||||||
assert response.status_code == 429
|
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):
|
class TestAppeal(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.addon = addon_factory()
|
self.addon = addon_factory()
|
||||||
|
|
|
@ -24,6 +24,7 @@ from olympia.abuse.forms import AbuseAppealEmailForm, AbuseAppealForm
|
||||||
from olympia.abuse.models import CinderReport
|
from olympia.abuse.models import CinderReport
|
||||||
from olympia.abuse.serializers import (
|
from olympia.abuse.serializers import (
|
||||||
AddonAbuseReportSerializer,
|
AddonAbuseReportSerializer,
|
||||||
|
RatingAbuseReportSerializer,
|
||||||
UserAbuseReportSerializer,
|
UserAbuseReportSerializer,
|
||||||
)
|
)
|
||||||
from olympia.abuse.tasks import appeal_to_cinder
|
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.accounts.views import AccountViewSet
|
||||||
from olympia.addons.views import AddonViewSet
|
from olympia.addons.views import AddonViewSet
|
||||||
from olympia.api.throttling import GranularIPRateThrottle, GranularUserRateThrottle
|
from olympia.api.throttling import GranularIPRateThrottle, GranularUserRateThrottle
|
||||||
|
from olympia.ratings.views import RatingViewSet
|
||||||
|
|
||||||
from .cinder import Cinder
|
from .cinder import Cinder
|
||||||
|
|
||||||
|
@ -48,35 +50,56 @@ class AbuseIPThrottle(GranularIPRateThrottle):
|
||||||
scope = 'ip_abuse'
|
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 = []
|
permission_classes = []
|
||||||
serializer_class = AddonAbuseReportSerializer
|
serializer_class = AddonAbuseReportSerializer
|
||||||
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
||||||
|
target_viewset_class = AddonViewSet
|
||||||
|
target_viewset_action = 'retrieve_from_related'
|
||||||
|
target_parameter_name = 'addon'
|
||||||
|
|
||||||
def get_addon_viewset(self):
|
def get_target_object(self):
|
||||||
if hasattr(self, 'addon_viewset'):
|
super().get_target_object()
|
||||||
return self.addon_viewset
|
# 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
|
||||||
if 'addon_pk' not in self.kwargs:
|
# input either. We raise a 404 if get_target_object() is called for a
|
||||||
self.kwargs['addon_pk'] = self.request.data.get(
|
# non-public add-on: the view avoids calling it when a guid is passed
|
||||||
'addon'
|
# (see get_guid()) and the serializer will handle the Http404 when
|
||||||
) or self.request.GET.get('addon')
|
# returning the internal value (see get_addon() in serializer).
|
||||||
self.addon_viewset = AddonViewSet(
|
if self.target_object and not self.target_object.is_public():
|
||||||
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():
|
|
||||||
raise Http404
|
raise Http404
|
||||||
return self.addon_object
|
return self.target_object
|
||||||
|
|
||||||
def get_guid(self):
|
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
|
look like a guid and there is no public add-on with a matching slug or
|
||||||
pk.
|
pk.
|
||||||
"""
|
"""
|
||||||
if self.get_addon_viewset().get_lookup_field(self.kwargs['addon_pk']) == 'guid':
|
if (
|
||||||
guid = self.kwargs['addon_pk']
|
self.get_target_viewset().get_lookup_field(self.kwargs['target_pk'])
|
||||||
|
== 'guid'
|
||||||
|
):
|
||||||
|
guid = self.kwargs['target_pk']
|
||||||
else:
|
else:
|
||||||
# At this point the parameter is a slug or pk. For backwards-compatibility
|
# At this point the parameter is a slug or pk. For backwards-compatibility
|
||||||
# we accept that, but ultimately record only the guid.
|
# we accept that, but ultimately record only the guid.
|
||||||
self.get_addon_object()
|
self.get_target_object()
|
||||||
if self.addon_object:
|
if self.target_object:
|
||||||
guid = self.addon_object.guid
|
guid = self.target_object.guid
|
||||||
return guid
|
return guid
|
||||||
|
|
||||||
|
|
||||||
class UserAbuseViewSet(CreateModelMixin, GenericViewSet):
|
class UserAbuseViewSet(AbuseTargetMixin, CreateModelMixin, GenericViewSet):
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
serializer_class = UserAbuseReportSerializer
|
serializer_class = UserAbuseReportSerializer
|
||||||
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
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:
|
class RatingAbuseViewSet(AbuseTargetMixin, CreateModelMixin, GenericViewSet):
|
||||||
self.kwargs['user_pk'] = self.request.data.get(
|
permission_classes = []
|
||||||
'user'
|
serializer_class = RatingAbuseReportSerializer
|
||||||
) or self.request.GET.get('user')
|
throttle_classes = (AbuseUserThrottle, AbuseIPThrottle)
|
||||||
|
target_viewset_class = RatingViewSet
|
||||||
return AccountViewSet(
|
target_parameter_name = 'rating'
|
||||||
request=self.request,
|
|
||||||
permission_classes=[],
|
|
||||||
kwargs={'pk': self.kwargs['user_pk']},
|
|
||||||
).get_object()
|
|
||||||
|
|
||||||
|
|
||||||
class CinderInboundPermission:
|
class CinderInboundPermission:
|
||||||
|
@ -222,9 +243,10 @@ def appeal(request, *, decision_id, **kwargs):
|
||||||
allowed_users = []
|
allowed_users = []
|
||||||
if hasattr(abuse_report.target, 'authors'):
|
if hasattr(abuse_report.target, 'authors'):
|
||||||
allowed_users = abuse_report.target.authors.all()
|
allowed_users = abuse_report.target.authors.all()
|
||||||
# FIXME: when we implement collections in abuse reports
|
elif hasattr(abuse_report.target, 'author'):
|
||||||
# elif hasattr(abuse_report.target, 'author'):
|
allowed_users = [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
|
valid_user_or_email_provided = request.user in allowed_users
|
||||||
|
|
||||||
if not valid_user_or_email_provided and not appeal_email_form:
|
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.
|
# Home.
|
||||||
re_path(r'^$', frontend_view, name='home'),
|
re_path(r'^$', frontend_view, name='home'),
|
||||||
# Abuse.
|
# 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')),
|
re_path(r'abuse/', include('olympia.abuse.urls')),
|
||||||
# Add-ons.
|
# Add-ons.
|
||||||
re_path(r'', include('olympia.addons.urls')),
|
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
|
import debug_toolbar
|
||||||
|
|
||||||
# Remove leading and trailing slashes so the regex matches.
|
# Remove leading and trailing slashes so the regex matches.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче