"[EPIC] Report option on everything - reports models

This commit is contained in:
Eliot Berriot 2019-08-22 11:30:30 +02:00
commit a6cf2ce019
22 changed files with 792 additions and 21 deletions

View file

@ -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}

View file

@ -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"))

View 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},
)
]

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)