"[EPIC] Report option on everything - reports models
This commit is contained in:
parent
079671ef7a
commit
a6cf2ce019
|
@ -1,7 +1,10 @@
|
||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from . import search
|
from . import search
|
||||||
|
|
||||||
PRIVACY_LEVEL_CHOICES = [
|
PRIVACY_LEVEL_CHOICES = [
|
||||||
|
@ -52,3 +55,58 @@ class SmartSearchFilter(django_filters.CharFilter):
|
||||||
except (forms.ValidationError):
|
except (forms.ValidationError):
|
||||||
return qs.none()
|
return qs.none()
|
||||||
return search.apply(qs, cleaned)
|
return search.apply(qs, cleaned)
|
||||||
|
|
||||||
|
|
||||||
|
class GenericRelation(serializers.JSONField):
|
||||||
|
def __init__(self, choices, *args, **kwargs):
|
||||||
|
self.choices = choices
|
||||||
|
self.encoder = kwargs.setdefault("encoder", DjangoJSONEncoder)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
type = None
|
||||||
|
id = None
|
||||||
|
for key, choice in self.choices.items():
|
||||||
|
if isinstance(value, choice["queryset"].model):
|
||||||
|
type = key
|
||||||
|
id = getattr(value, choice.get("id_attr", "id"))
|
||||||
|
break
|
||||||
|
|
||||||
|
if type:
|
||||||
|
return {"type": type, "id": id}
|
||||||
|
|
||||||
|
def to_internal_value(self, v):
|
||||||
|
v = super().to_internal_value(v)
|
||||||
|
|
||||||
|
if not v or not isinstance(v, dict):
|
||||||
|
raise serializers.ValidationError("Invalid data")
|
||||||
|
|
||||||
|
try:
|
||||||
|
type = v["type"]
|
||||||
|
field = serializers.ChoiceField(choices=list(self.choices.keys()))
|
||||||
|
type = field.to_internal_value(type)
|
||||||
|
except (TypeError, KeyError, serializers.ValidationError):
|
||||||
|
raise serializers.ValidationError("Invalid type")
|
||||||
|
|
||||||
|
conf = self.choices[type]
|
||||||
|
id_attr = conf.get("id_attr", "id")
|
||||||
|
id_field = conf.get("id_field", serializers.IntegerField(min_value=1))
|
||||||
|
queryset = conf["queryset"]
|
||||||
|
try:
|
||||||
|
id_value = v[id_attr]
|
||||||
|
id_value = id_field.to_internal_value(id_value)
|
||||||
|
except (TypeError, KeyError, serializers.ValidationError):
|
||||||
|
raise serializers.ValidationError("Invalid {}".format(id_attr))
|
||||||
|
|
||||||
|
query_getter = conf.get(
|
||||||
|
"get_query", lambda attr, value: models.Q(**{attr: value})
|
||||||
|
)
|
||||||
|
query = query_getter(id_attr, id_value)
|
||||||
|
try:
|
||||||
|
obj = queryset.get(query)
|
||||||
|
except queryset.model.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("Object not found")
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
|
@ -154,7 +154,7 @@ def order_for_search(qs, field):
|
||||||
|
|
||||||
def recursive_getattr(obj, key, permissive=False):
|
def recursive_getattr(obj, key, permissive=False):
|
||||||
"""
|
"""
|
||||||
Given a dictionary such as {'user': {'name': 'Bob'}} and
|
Given a dictionary such as {'user': {'name': 'Bob'}} or and object and
|
||||||
a dotted string such as user.name, returns 'Bob'.
|
a dotted string such as user.name, returns 'Bob'.
|
||||||
|
|
||||||
If the value is not present, returns None
|
If the value is not present, returns None
|
||||||
|
@ -162,7 +162,10 @@ def recursive_getattr(obj, key, permissive=False):
|
||||||
v = obj
|
v = obj
|
||||||
for k in key.split("."):
|
for k in key.split("."):
|
||||||
try:
|
try:
|
||||||
v = v.get(k)
|
if hasattr(v, "get"):
|
||||||
|
v = v.get(k)
|
||||||
|
else:
|
||||||
|
v = getattr(v, k)
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
if not permissive:
|
if not permissive:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2.3 on 2019-07-30 08:46
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
import django.core.serializers.json
|
||||||
|
from django.db import migrations
|
||||||
|
import funkwhale_api.federation.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0019_auto_20190611_0851'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='activity',
|
||||||
|
name='payload',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fetch',
|
||||||
|
name='detail',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='librarytrack',
|
||||||
|
name='metadata',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,6 +9,7 @@ music_router = routers.SimpleRouter(trailing_slash=False)
|
||||||
router.register(r"federation/shared", views.SharedViewSet, "shared")
|
router.register(r"federation/shared", views.SharedViewSet, "shared")
|
||||||
router.register(r"federation/actors", views.ActorViewSet, "actors")
|
router.register(r"federation/actors", views.ActorViewSet, "actors")
|
||||||
router.register(r"federation/edits", views.EditViewSet, "edits")
|
router.register(r"federation/edits", views.EditViewSet, "edits")
|
||||||
|
router.register(r"federation/reports", views.ReportViewSet, "reports")
|
||||||
router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
||||||
|
|
||||||
music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries")
|
music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries")
|
||||||
|
|
|
@ -128,3 +128,32 @@ def is_local(url):
|
||||||
return url.startswith("http://{}/".format(d)) or url.startswith(
|
return url.startswith("http://{}/".format(d)) or url.startswith(
|
||||||
"https://{}/".format(d)
|
"https://{}/".format(d)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_data_from_username(username):
|
||||||
|
|
||||||
|
parts = username.split("@")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"username": parts[0],
|
||||||
|
"domain": parts[1] if len(parts) > 1 else settings.FEDERATION_HOSTNAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_from_username_data_query(field, data):
|
||||||
|
if not data:
|
||||||
|
return Q(**{field: None})
|
||||||
|
if field:
|
||||||
|
return Q(
|
||||||
|
**{
|
||||||
|
"{}__preferred_username__iexact".format(field): data["username"],
|
||||||
|
"{}__domain__name__iexact".format(field): data["domain"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Q(
|
||||||
|
**{
|
||||||
|
"preferred_username__iexact": data["username"],
|
||||||
|
"domain__name__iexact": data["domain"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from rest_framework import exceptions, mixins, permissions, response, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
from funkwhale_api.moderation import models as moderation_models
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.music import utils as music_utils
|
from funkwhale_api.music import utils as music_utils
|
||||||
|
|
||||||
|
@ -86,6 +87,15 @@ class EditViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericVi
|
||||||
# serializer_class = serializers.ActorSerializer
|
# serializer_class = serializers.ActorSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ReportViewSet(
|
||||||
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
|
):
|
||||||
|
lookup_field = "uuid"
|
||||||
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
|
queryset = moderation_models.Report.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class WellKnownViewSet(viewsets.GenericViewSet):
|
class WellKnownViewSet(viewsets.GenericViewSet):
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
|
@ -22,24 +21,12 @@ class ActorField(forms.CharField):
|
||||||
if not value:
|
if not value:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
parts = value.split("@")
|
return federation_utils.get_actor_data_from_username(value)
|
||||||
|
|
||||||
return {
|
|
||||||
"username": parts[0],
|
|
||||||
"domain": parts[1] if len(parts) > 1 else settings.FEDERATION_HOSTNAME,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_actor_filter(actor_field):
|
def get_actor_filter(actor_field):
|
||||||
def handler(v):
|
def handler(v):
|
||||||
if not v:
|
federation_utils.get_actor_from_username_data_query(actor_field, v)
|
||||||
return Q(**{actor_field: None})
|
|
||||||
return Q(
|
|
||||||
**{
|
|
||||||
"{}__preferred_username__iexact".format(actor_field): v["username"],
|
|
||||||
"{}__domain__name__iexact".format(actor_field): v["domain"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"field": ActorField(), "handler": handler}
|
return {"field": ActorField(), "handler": handler}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
from dynamic_preferences import types
|
from dynamic_preferences import types
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
from funkwhale_api.common import preferences as common_preferences
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
moderation = types.Section("moderation")
|
moderation = types.Section("moderation")
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,3 +28,15 @@ class AllowListPublic(types.BooleanPreference):
|
||||||
"make your moderation policy public."
|
"make your moderation policy public."
|
||||||
)
|
)
|
||||||
default = False
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class UnauthenticatedReportTypes(common_preferences.StringListPreference):
|
||||||
|
show_in_api = True
|
||||||
|
section = moderation
|
||||||
|
name = "unauthenticated_report_types"
|
||||||
|
default = ["takedown_request", "illegal_content"]
|
||||||
|
verbose_name = "Accountless report categories"
|
||||||
|
help_text = "A list of categories for which external users (without an account) can submit a report"
|
||||||
|
choices = models.REPORT_TYPES
|
||||||
|
field_kwargs = {"choices": choices, "required": False}
|
||||||
|
|
|
@ -37,3 +37,17 @@ class UserFilterFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||||
for_artist = factory.Trait(
|
for_artist = factory.Trait(
|
||||||
target_artist=factory.SubFactory(music_factories.ArtistFactory)
|
target_artist=factory.SubFactory(music_factories.ArtistFactory)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||||
|
submitter = factory.SubFactory(federation_factories.ActorFactory)
|
||||||
|
target = None
|
||||||
|
summary = factory.Faker("paragraph")
|
||||||
|
type = "other"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = "moderation.Report"
|
||||||
|
|
||||||
|
class Params:
|
||||||
|
anonymous = factory.Trait(actor=None, submitter_email=factory.Faker("email"))
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Generated by Django 2.2.3 on 2019-08-01 08:34
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("federation", "0020_auto_20190730_0846"),
|
||||||
|
("moderation", "0002_auto_20190213_0927"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Report",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("fid", models.URLField(db_index=True, max_length=500, unique=True)),
|
||||||
|
("url", models.URLField(blank=True, max_length=500, null=True)),
|
||||||
|
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||||
|
(
|
||||||
|
"creation_date",
|
||||||
|
models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
("summary", models.TextField(max_length=50000, null=True)),
|
||||||
|
("handled_date", models.DateTimeField(null=True)),
|
||||||
|
("is_handled", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("takedown_request", "Takedown request"),
|
||||||
|
("invalid_metadata", "Invalid metadata"),
|
||||||
|
("illegal_content", "Illegal content"),
|
||||||
|
("offensive_content", "Offensive content"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
max_length=40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("submitter_email", models.EmailField(max_length=254, null=True)),
|
||||||
|
("target_id", models.IntegerField(null=True)),
|
||||||
|
(
|
||||||
|
"target_state",
|
||||||
|
django.contrib.postgres.fields.jsonb.JSONField(null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"submitter",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="reports",
|
||||||
|
to="federation.Actor",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assigned_to",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="assigned_reports",
|
||||||
|
to="federation.Actor",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"target_content_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.ContentType",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"target_owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="federation.Actor",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False},
|
||||||
|
)
|
||||||
|
]
|
|
@ -1,9 +1,17 @@
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from funkwhale_api.federation import models as federation_models
|
||||||
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
|
||||||
|
|
||||||
class InstancePolicyQuerySet(models.QuerySet):
|
class InstancePolicyQuerySet(models.QuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
|
@ -92,3 +100,63 @@ class UserFilter(models.Model):
|
||||||
def target(self):
|
def target(self):
|
||||||
if self.target_artist:
|
if self.target_artist:
|
||||||
return {"type": "artist", "obj": self.target_artist}
|
return {"type": "artist", "obj": self.target_artist}
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_TYPES = [
|
||||||
|
("takedown_request", "Takedown request"),
|
||||||
|
("invalid_metadata", "Invalid metadata"),
|
||||||
|
("illegal_content", "Illegal content"),
|
||||||
|
("offensive_content", "Offensive content"),
|
||||||
|
("other", "Other"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Report(federation_models.FederationMixin):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
summary = models.TextField(null=True, max_length=50000)
|
||||||
|
handled_date = models.DateTimeField(null=True)
|
||||||
|
is_handled = models.BooleanField(default=False)
|
||||||
|
type = models.CharField(max_length=40, choices=REPORT_TYPES)
|
||||||
|
submitter_email = models.EmailField(null=True)
|
||||||
|
submitter = models.ForeignKey(
|
||||||
|
"federation.Actor",
|
||||||
|
related_name="reports",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assigned_to = models.ForeignKey(
|
||||||
|
"federation.Actor",
|
||||||
|
related_name="assigned_reports",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
target_id = models.IntegerField(null=True)
|
||||||
|
target_content_type = models.ForeignKey(
|
||||||
|
ContentType, null=True, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
target = GenericForeignKey("target_content_type", "target_id")
|
||||||
|
target_owner = models.ForeignKey(
|
||||||
|
"federation.Actor", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
|
)
|
||||||
|
# frozen state of the target being reported, to ensure we still have info in the event of a
|
||||||
|
# delete
|
||||||
|
target_state = JSONField(null=True)
|
||||||
|
|
||||||
|
def get_federation_id(self):
|
||||||
|
if self.fid:
|
||||||
|
return self.fid
|
||||||
|
|
||||||
|
return federation_utils.full_url(
|
||||||
|
reverse("federation:reports-detail", kwargs={"uuid": self.uuid})
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if not self.pk and not self.fid:
|
||||||
|
self.fid = self.get_federation_id()
|
||||||
|
|
||||||
|
return super().save(**kwargs)
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
import persisting_theory
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from funkwhale_api.common import fields as common_fields
|
||||||
|
from funkwhale_api.common import preferences
|
||||||
|
from funkwhale_api.federation import models as federation_models
|
||||||
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
|
from funkwhale_api.playlists import models as playlists_models
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,3 +51,179 @@ class UserFilterSerializer(serializers.ModelSerializer):
|
||||||
data["target_artist"] = target["obj"]
|
data["target_artist"] = target["obj"]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
state_serializers = persisting_theory.Registry()
|
||||||
|
|
||||||
|
|
||||||
|
TAGS_FIELD = serializers.ListField(source="get_tags")
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="music.Artist")
|
||||||
|
class ArtistStateSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TAGS_FIELD
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Artist
|
||||||
|
fields = ["id", "name", "mbid", "fid", "creation_date", "uuid", "tags"]
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="music.Album")
|
||||||
|
class AlbumStateSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TAGS_FIELD
|
||||||
|
artist = ArtistStateSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Album
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"mbid",
|
||||||
|
"fid",
|
||||||
|
"creation_date",
|
||||||
|
"uuid",
|
||||||
|
"artist",
|
||||||
|
"release_date",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="music.Track")
|
||||||
|
class TrackStateSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TAGS_FIELD
|
||||||
|
artist = ArtistStateSerializer()
|
||||||
|
album = AlbumStateSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Track
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"mbid",
|
||||||
|
"fid",
|
||||||
|
"creation_date",
|
||||||
|
"uuid",
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"disc_number",
|
||||||
|
"position",
|
||||||
|
"license",
|
||||||
|
"copyright",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="music.Library")
|
||||||
|
class LibraryStateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Library
|
||||||
|
fields = ["id", "fid", "name", "description", "creation_date", "privacy_level"]
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="playlists.Playlist")
|
||||||
|
class PlaylistStateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = playlists_models.Playlist
|
||||||
|
fields = ["id", "name", "creation_date", "privacy_level"]
|
||||||
|
|
||||||
|
|
||||||
|
@state_serializers.register(name="federation.Actor")
|
||||||
|
class ActorStateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = federation_models.Actor
|
||||||
|
fields = [
|
||||||
|
"fid",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"summary",
|
||||||
|
"domain",
|
||||||
|
"type",
|
||||||
|
"creation_date",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_query(attr, value):
|
||||||
|
data = federation_utils.get_actor_data_from_username(value)
|
||||||
|
return federation_utils.get_actor_from_username_data_query(None, data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_target_owner(target):
|
||||||
|
mapping = {
|
||||||
|
music_models.Artist: lambda t: t.attributed_to,
|
||||||
|
music_models.Album: lambda t: t.attributed_to,
|
||||||
|
music_models.Track: lambda t: t.attributed_to,
|
||||||
|
music_models.Library: lambda t: t.actor,
|
||||||
|
playlists_models.Playlist: lambda t: t.user.actor,
|
||||||
|
federation_models.Actor: lambda t: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping[target.__class__](target)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportSerializer(serializers.ModelSerializer):
|
||||||
|
target = common_fields.GenericRelation(
|
||||||
|
{
|
||||||
|
"artist": {"queryset": music_models.Artist.objects.all()},
|
||||||
|
"album": {"queryset": music_models.Album.objects.all()},
|
||||||
|
"track": {"queryset": music_models.Track.objects.all()},
|
||||||
|
"library": {
|
||||||
|
"queryset": music_models.Library.objects.all(),
|
||||||
|
"id_attr": "uuid",
|
||||||
|
"id_field": serializers.UUIDField(),
|
||||||
|
},
|
||||||
|
"playlist": {"queryset": playlists_models.Playlist.objects.all()},
|
||||||
|
"account": {
|
||||||
|
"queryset": federation_models.Actor.objects.all(),
|
||||||
|
"id_attr": "full_username",
|
||||||
|
"id_field": serializers.EmailField(),
|
||||||
|
"get_query": get_actor_query,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Report
|
||||||
|
fields = [
|
||||||
|
"uuid",
|
||||||
|
"summary",
|
||||||
|
"creation_date",
|
||||||
|
"handled_date",
|
||||||
|
"is_handled",
|
||||||
|
"submitter_email",
|
||||||
|
"target",
|
||||||
|
"type",
|
||||||
|
]
|
||||||
|
read_only_fields = ["uuid", "is_handled", "creation_date", "handled_date"]
|
||||||
|
|
||||||
|
def validate(self, validated_data):
|
||||||
|
validated_data = super().validate(validated_data)
|
||||||
|
submitter = self.context.get("submitter")
|
||||||
|
if submitter:
|
||||||
|
# we have an authenticated actor so no need to check further
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
unauthenticated_report_types = preferences.get(
|
||||||
|
"moderation__unauthenticated_report_types"
|
||||||
|
)
|
||||||
|
if validated_data["type"] not in unauthenticated_report_types:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"You need an account to submit this report"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not validated_data.get("submitter_email"):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"You need to provide an email address to submit this report"
|
||||||
|
)
|
||||||
|
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
target_state_serializer = state_serializers[
|
||||||
|
validated_data["target"]._meta.label
|
||||||
|
]
|
||||||
|
|
||||||
|
validated_data["target_state"] = target_state_serializer(
|
||||||
|
validated_data["target"]
|
||||||
|
).data
|
||||||
|
validated_data["target_owner"] = get_target_owner(validated_data["target"])
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
|
@ -4,5 +4,6 @@ from . import views
|
||||||
|
|
||||||
router = routers.OptionalSlashRouter()
|
router = routers.OptionalSlashRouter()
|
||||||
router.register(r"content-filters", views.UserFilterViewSet, "content-filters")
|
router.register(r"content-filters", views.UserFilterViewSet, "content-filters")
|
||||||
|
router.register(r"reports", views.ReportsViewSet, "reports")
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
|
@ -39,3 +39,25 @@ class UserFilterViewSet(
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(user=self.request.user)
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportsViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||||
|
lookup_field = "uuid"
|
||||||
|
queryset = models.Report.objects.all().order_by("-creation_date")
|
||||||
|
serializer_class = serializers.ReportSerializer
|
||||||
|
required_scope = "reports"
|
||||||
|
ordering_fields = ("creation_date",)
|
||||||
|
anonymous_policy = "setting"
|
||||||
|
anonymous_scopes = {"write:reports"}
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
context["submitter"] = self.request.user.actor
|
||||||
|
return context
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
submitter = None
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
submitter = self.request.user.actor
|
||||||
|
serializer.save(submitter=submitter)
|
||||||
|
|
|
@ -127,6 +127,9 @@ class APIModelMixin(models.Model):
|
||||||
parsed = urllib.parse.urlparse(self.fid)
|
parsed = urllib.parse.urlparse(self.fid)
|
||||||
return parsed.hostname
|
return parsed.hostname
|
||||||
|
|
||||||
|
def get_tags(self):
|
||||||
|
return list(sorted(self.tagged_items.values_list("tag__name", flat=True)))
|
||||||
|
|
||||||
|
|
||||||
class License(models.Model):
|
class License(models.Model):
|
||||||
code = models.CharField(primary_key=True, max_length=100)
|
code = models.CharField(primary_key=True, max_length=100)
|
||||||
|
|
|
@ -96,8 +96,9 @@ class ScopePermission(permissions.BasePermission):
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# we use default anonymous scopes
|
user_scopes = (
|
||||||
user_scopes = scopes.ANONYMOUS_SCOPES
|
getattr(view, "anonymous_scopes", set()) | scopes.ANONYMOUS_SCOPES
|
||||||
|
)
|
||||||
return should_allow(
|
return should_allow(
|
||||||
required_scope=required_scope, request_scopes=user_scopes
|
required_scope=required_scope, request_scopes=user_scopes
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ BASE_SCOPES = [
|
||||||
Scope("playlists", "Access playlists"),
|
Scope("playlists", "Access playlists"),
|
||||||
Scope("notifications", "Access personal notifications"),
|
Scope("notifications", "Access personal notifications"),
|
||||||
Scope("security", "Access security settings"),
|
Scope("security", "Access security settings"),
|
||||||
|
Scope("reports", "Access reports"),
|
||||||
# Privileged scopes that require specific user permissions
|
# Privileged scopes that require specific user permissions
|
||||||
Scope("instance:settings", "Access instance settings"),
|
Scope("instance:settings", "Access instance settings"),
|
||||||
Scope("instance:users", "Access local user accounts"),
|
Scope("instance:users", "Access local user accounts"),
|
||||||
|
@ -72,6 +73,8 @@ COMMON_SCOPES = ANONYMOUS_SCOPES | {
|
||||||
"write:edits",
|
"write:edits",
|
||||||
"read:filters",
|
"read:filters",
|
||||||
"write:filters",
|
"write:filters",
|
||||||
|
"read:reports",
|
||||||
|
"write:reports",
|
||||||
"write:listenings",
|
"write:listenings",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,50 @@ from funkwhale_api.users.factories import UserFactory
|
||||||
def test_privacy_level_query(user, expected):
|
def test_privacy_level_query(user, expected):
|
||||||
query = fields.privacy_level_query(user)
|
query = fields.privacy_level_query(user)
|
||||||
assert query == expected
|
assert query == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_relation_field(factories):
|
||||||
|
obj = factories["users.User"]()
|
||||||
|
f = fields.GenericRelation(
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"queryset": obj.__class__.objects.all(),
|
||||||
|
"id_attr": "username",
|
||||||
|
"id_field": fields.serializers.CharField(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {"type": "user", "username": obj.username}
|
||||||
|
|
||||||
|
assert f.to_internal_value(data) == obj
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"payload, expected_error",
|
||||||
|
[
|
||||||
|
({}, r".*Invalid data.*"),
|
||||||
|
(1, r".*Invalid data.*"),
|
||||||
|
(False, r".*Invalid data.*"),
|
||||||
|
("test", r".*Invalid data.*"),
|
||||||
|
({"missing": "type"}, r".*Invalid type.*"),
|
||||||
|
({"type": "noop"}, r".*Invalid type.*"),
|
||||||
|
({"type": "user"}, r".*Invalid username.*"),
|
||||||
|
({"type": "user", "username": {}}, r".*Invalid username.*"),
|
||||||
|
({"type": "user", "username": "not_found"}, r".*Object not found.*"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_generic_relation_field_validation_error(payload, expected_error, factories):
|
||||||
|
obj = factories["users.User"]()
|
||||||
|
f = fields.GenericRelation(
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"queryset": obj.__class__.objects.all(),
|
||||||
|
"id_attr": "username",
|
||||||
|
"id_field": fields.serializers.CharField(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(fields.serializers.ValidationError, match=expected_error):
|
||||||
|
f.to_internal_value(payload)
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from funkwhale_api.common import utils as common_utils
|
||||||
|
from funkwhale_api.federation import models as federation_models
|
||||||
from funkwhale_api.moderation import serializers
|
from funkwhale_api.moderation import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,3 +32,145 @@ def test_user_filter_serializer_save(factories):
|
||||||
content_filter = serializer.save(user=user)
|
content_filter = serializer.save(user=user)
|
||||||
|
|
||||||
assert content_filter.target_artist == artist
|
assert content_filter.target_artist == artist
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"factory_name, target_type, id_field, state_serializer",
|
||||||
|
[
|
||||||
|
("music.Artist", "artist", "id", serializers.ArtistStateSerializer),
|
||||||
|
("music.Album", "album", "id", serializers.AlbumStateSerializer),
|
||||||
|
("music.Track", "track", "id", serializers.TrackStateSerializer),
|
||||||
|
("music.Library", "library", "uuid", serializers.LibraryStateSerializer),
|
||||||
|
("playlists.Playlist", "playlist", "id", serializers.PlaylistStateSerializer),
|
||||||
|
(
|
||||||
|
"federation.Actor",
|
||||||
|
"account",
|
||||||
|
"full_username",
|
||||||
|
serializers.ActorStateSerializer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_report_serializer_save(
|
||||||
|
factory_name, target_type, id_field, state_serializer, factories, mocker
|
||||||
|
):
|
||||||
|
target = factories[factory_name]()
|
||||||
|
target_owner = factories["federation.Actor"]()
|
||||||
|
submitter = factories["federation.Actor"]()
|
||||||
|
target_data = {"type": target_type, id_field: getattr(target, id_field)}
|
||||||
|
payload = {
|
||||||
|
"summary": "Report content",
|
||||||
|
"type": "illegal_content",
|
||||||
|
"target": target_data,
|
||||||
|
}
|
||||||
|
serializer = serializers.ReportSerializer(
|
||||||
|
data=payload, context={"submitter": submitter}
|
||||||
|
)
|
||||||
|
get_target_owner = mocker.patch.object(
|
||||||
|
serializers, "get_target_owner", return_value=target_owner
|
||||||
|
)
|
||||||
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
|
report = serializer.save()
|
||||||
|
|
||||||
|
assert report.target == target
|
||||||
|
assert report.type == payload["type"]
|
||||||
|
assert report.summary == payload["summary"]
|
||||||
|
assert report.target_state == state_serializer(target).data
|
||||||
|
assert report.target_owner == target_owner
|
||||||
|
get_target_owner.assert_called_once_with(target)
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_serializer_save_anonymous(factories, mocker):
|
||||||
|
target = factories["music.Artist"]()
|
||||||
|
payload = {
|
||||||
|
"summary": "Report content",
|
||||||
|
"type": "illegal_content",
|
||||||
|
"target": {"type": "artist", "id": target.pk},
|
||||||
|
"submitter_email": "test@submitter.example",
|
||||||
|
}
|
||||||
|
serializer = serializers.ReportSerializer(data=payload)
|
||||||
|
|
||||||
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
|
report = serializer.save()
|
||||||
|
|
||||||
|
assert report.target == target
|
||||||
|
assert report.type == payload["type"]
|
||||||
|
assert report.summary == payload["summary"]
|
||||||
|
assert report.submitter_email == payload["submitter_email"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"factory_name, factory_kwargs, owner_field",
|
||||||
|
[
|
||||||
|
("music.Artist", {"attributed": True}, "attributed_to"),
|
||||||
|
("music.Album", {"attributed": True}, "attributed_to"),
|
||||||
|
("music.Track", {"attributed": True}, "attributed_to"),
|
||||||
|
("music.Library", {}, "actor"),
|
||||||
|
("playlists.Playlist", {"user__with_actor": True}, "user.actor"),
|
||||||
|
("federation.Actor", {}, "self"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_target_owner(factory_name, factory_kwargs, owner_field, factories):
|
||||||
|
target = factories[factory_name](**factory_kwargs)
|
||||||
|
if owner_field == "self":
|
||||||
|
expected_owner = target
|
||||||
|
else:
|
||||||
|
expected_owner = common_utils.recursive_getattr(target, owner_field)
|
||||||
|
|
||||||
|
assert isinstance(expected_owner, federation_models.Actor)
|
||||||
|
assert serializers.get_target_owner(target) == expected_owner
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_serializer_repr(factories, to_api_date):
|
||||||
|
target = factories["music.Artist"]()
|
||||||
|
report = factories["moderation.Report"](target=target)
|
||||||
|
expected = {
|
||||||
|
"uuid": str(report.uuid),
|
||||||
|
"summary": report.summary,
|
||||||
|
"type": report.type,
|
||||||
|
"target": {"type": "artist", "id": target.pk},
|
||||||
|
"creation_date": to_api_date(report.creation_date),
|
||||||
|
"handled_date": None,
|
||||||
|
"is_handled": False,
|
||||||
|
"submitter_email": None,
|
||||||
|
}
|
||||||
|
serializer = serializers.ReportSerializer(report)
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"preference, context, payload, is_valid",
|
||||||
|
[
|
||||||
|
# anonymous reports not enabled for the category
|
||||||
|
(
|
||||||
|
["illegal_content"],
|
||||||
|
{},
|
||||||
|
{"type": "other", "submitter_email": "hello@example.test"},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
# anonymous reports enabled for the category, but invalid email
|
||||||
|
(["other"], {}, {"type": "other", "submitter_email": "hello@"}, False),
|
||||||
|
# anonymous reports enabled for the category, no email
|
||||||
|
(["other"], {}, {"type": "other"}, False),
|
||||||
|
# anonymous reports enabled for the category, actor object is empty
|
||||||
|
(["other"], {"submitter": None}, {"type": "other"}, False),
|
||||||
|
# valid examples
|
||||||
|
(
|
||||||
|
["other"],
|
||||||
|
{},
|
||||||
|
{"type": "other", "submitter_email": "hello@example.test"},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_report_serializer_save_unauthenticated_validation(
|
||||||
|
preference, context, payload, is_valid, factories, preferences
|
||||||
|
):
|
||||||
|
preferences["moderation__unauthenticated_report_types"] = preference
|
||||||
|
target = factories["music.Artist"]()
|
||||||
|
target_data = {"type": "artist", "id": target.id}
|
||||||
|
payload["summary"] = "Test"
|
||||||
|
payload["target"] = target_data
|
||||||
|
serializer = serializers.ReportSerializer(data=payload, context=context)
|
||||||
|
assert serializer.is_valid() is is_valid
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.moderation import models
|
||||||
|
|
||||||
|
|
||||||
def test_restrict_to_own_filters(factories, logged_in_api_client):
|
def test_restrict_to_own_filters(factories, logged_in_api_client):
|
||||||
cf = factories["moderation.UserFilter"](
|
cf = factories["moderation.UserFilter"](
|
||||||
|
@ -22,3 +24,35 @@ def test_create_filter(factories, logged_in_api_client):
|
||||||
cf = logged_in_api_client.user.content_filters.latest("id")
|
cf = logged_in_api_client.user.content_filters.latest("id")
|
||||||
assert cf.target_artist == artist
|
assert cf.target_artist == artist
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_report_logged_in(factories, logged_in_api_client):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
target = factories["music.Artist"]()
|
||||||
|
url = reverse("api:v1:moderation:reports-list")
|
||||||
|
data = {
|
||||||
|
"target": {"type": "artist", "id": target.pk},
|
||||||
|
"summary": "Test report",
|
||||||
|
"type": "other",
|
||||||
|
}
|
||||||
|
response = logged_in_api_client.post(url, data, format="json")
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
report = actor.reports.latest("id")
|
||||||
|
assert report.target == target
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_report_anonymous(factories, api_client, no_api_auth):
|
||||||
|
target = factories["music.Artist"]()
|
||||||
|
url = reverse("api:v1:moderation:reports-list")
|
||||||
|
data = {
|
||||||
|
"target": {"type": "artist", "id": target.pk},
|
||||||
|
"summary": "Test report",
|
||||||
|
"type": "illegal_content",
|
||||||
|
"submitter_email": "test@example.test",
|
||||||
|
}
|
||||||
|
response = api_client.post(url, data, format="json")
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
report = models.Report.objects.latest("id")
|
||||||
|
assert report.submitter_email == data["submitter_email"]
|
||||||
|
|
|
@ -59,7 +59,9 @@ def test_scope_permission_anonymous_policy(
|
||||||
policy, preference, expected, preferences, mocker, anonymous_user
|
policy, preference, expected, preferences, mocker, anonymous_user
|
||||||
):
|
):
|
||||||
preferences["common__api_authentication_required"] = preference
|
preferences["common__api_authentication_required"] = preference
|
||||||
view = mocker.Mock(required_scope="libraries", anonymous_policy=policy)
|
view = mocker.Mock(
|
||||||
|
required_scope="libraries", anonymous_policy=policy, anonymous_scopes=set()
|
||||||
|
)
|
||||||
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
|
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
|
||||||
|
|
||||||
p = permissions.ScopePermission()
|
p = permissions.ScopePermission()
|
||||||
|
@ -72,6 +74,7 @@ def test_scope_permission_dict_no_required(mocker, anonymous_user):
|
||||||
required_scope={"read": None, "write": "write:profile"},
|
required_scope={"read": None, "write": "write:profile"},
|
||||||
anonymous_policy=True,
|
anonymous_policy=True,
|
||||||
action="read",
|
action="read",
|
||||||
|
anonymous_scopes=set(),
|
||||||
)
|
)
|
||||||
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
|
request = mocker.Mock(method="GET", user=anonymous_user, actor=None)
|
||||||
|
|
||||||
|
@ -164,7 +167,9 @@ def test_scope_permission_token_anonymous_user_auth_not_required(
|
||||||
preferences["common__api_authentication_required"] = False
|
preferences["common__api_authentication_required"] = False
|
||||||
should_allow = mocker.patch.object(permissions, "should_allow")
|
should_allow = mocker.patch.object(permissions, "should_allow")
|
||||||
request = mocker.Mock(method="POST", user=anonymous_user, actor=None)
|
request = mocker.Mock(method="POST", user=anonymous_user, actor=None)
|
||||||
view = mocker.Mock(required_scope="profile", anonymous_policy="setting")
|
view = mocker.Mock(
|
||||||
|
required_scope="profile", anonymous_policy="setting", anonymous_scopes=set()
|
||||||
|
)
|
||||||
|
|
||||||
p = permissions.ScopePermission()
|
p = permissions.ScopePermission()
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ from funkwhale_api.users.oauth import scopes
|
||||||
"write:edits",
|
"write:edits",
|
||||||
"read:filters",
|
"read:filters",
|
||||||
"write:filters",
|
"write:filters",
|
||||||
|
"read:reports",
|
||||||
|
"write:reports",
|
||||||
"read:listenings",
|
"read:listenings",
|
||||||
"write:listenings",
|
"write:listenings",
|
||||||
"read:security",
|
"read:security",
|
||||||
|
@ -71,6 +73,8 @@ from funkwhale_api.users.oauth import scopes
|
||||||
"write:edits",
|
"write:edits",
|
||||||
"read:filters",
|
"read:filters",
|
||||||
"write:filters",
|
"write:filters",
|
||||||
|
"read:reports",
|
||||||
|
"write:reports",
|
||||||
"read:listenings",
|
"read:listenings",
|
||||||
"write:listenings",
|
"write:listenings",
|
||||||
"read:security",
|
"read:security",
|
||||||
|
@ -110,6 +114,8 @@ from funkwhale_api.users.oauth import scopes
|
||||||
"write:edits",
|
"write:edits",
|
||||||
"read:filters",
|
"read:filters",
|
||||||
"write:filters",
|
"write:filters",
|
||||||
|
"read:reports",
|
||||||
|
"write:reports",
|
||||||
"read:listenings",
|
"read:listenings",
|
||||||
"write:listenings",
|
"write:listenings",
|
||||||
"read:security",
|
"read:security",
|
||||||
|
@ -143,6 +149,8 @@ from funkwhale_api.users.oauth import scopes
|
||||||
"write:edits",
|
"write:edits",
|
||||||
"read:filters",
|
"read:filters",
|
||||||
"write:filters",
|
"write:filters",
|
||||||
|
"read:reports",
|
||||||
|
"write:reports",
|
||||||
"read:listenings",
|
"read:listenings",
|
||||||
"write:listenings",
|
"write:listenings",
|
||||||
"read:security",
|
"read:security",
|
||||||
|
|
Loading…
Reference in New Issue