"[EPIC] Report option on everything - reports models
This commit is contained in:
parent
079671ef7a
commit
a6cf2ce019
22 changed files with 792 additions and 21 deletions
|
|
@ -1,6 +1,10 @@
|
|||
from dynamic_preferences import types
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from funkwhale_api.common import preferences as common_preferences
|
||||
|
||||
from . import models
|
||||
|
||||
moderation = types.Section("moderation")
|
||||
|
||||
|
||||
|
|
@ -24,3 +28,15 @@ class AllowListPublic(types.BooleanPreference):
|
|||
"make your moderation policy public."
|
||||
)
|
||||
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(
|
||||
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"))
|
||||
|
|
|
|||
100
api/funkwhale_api/moderation/migrations/0003_report.py
Normal file
100
api/funkwhale_api/moderation/migrations/0003_report.py
Normal file
|
|
@ -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 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.urls import reverse
|
||||
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):
|
||||
def active(self):
|
||||
|
|
@ -92,3 +100,63 @@ class UserFilter(models.Model):
|
|||
def target(self):
|
||||
if 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 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.playlists import models as playlists_models
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
|
|
@ -43,3 +51,179 @@ class UserFilterSerializer(serializers.ModelSerializer):
|
|||
data["target_artist"] = target["obj"]
|
||||
|
||||
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.register(r"content-filters", views.UserFilterViewSet, "content-filters")
|
||||
router.register(r"reports", views.ReportsViewSet, "reports")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
|
|
|||
|
|
@ -39,3 +39,25 @@ class UserFilterViewSet(
|
|||
|
||||
def perform_create(self, serializer):
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue