From ab3bc96783dfc732495747a896635bf304697292 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 29 Aug 2019 11:45:41 +0200 Subject: [PATCH] See #890: added moderation note model, serializers and views --- api/funkwhale_api/manage/filters.py | 16 +++++++ api/funkwhale_api/manage/serializers.py | 25 +++++++++++ api/funkwhale_api/manage/urls.py | 1 + api/funkwhale_api/manage/views.py | 21 +++++++++ api/funkwhale_api/moderation/admin.py | 10 +---- api/funkwhale_api/moderation/factories.py | 10 +++++ .../moderation/migrations/0004_note.py | 30 +++++++++++++ api/funkwhale_api/moderation/models.py | 15 +++++++ api/funkwhale_api/moderation/serializers.py | 10 ++++- api/funkwhale_api/users/models.py | 2 + api/funkwhale_api/users/oauth/scopes.py | 1 + api/tests/manage/test_serializers.py | 17 +++++++ api/tests/manage/test_views.py | 44 +++++++++++++++++++ 13 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 api/funkwhale_api/moderation/migrations/0004_note.py diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 59577cd39..6cc69ea58 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -363,3 +363,19 @@ class ManageReportFilterSet(filters.FilterSet): class Meta: model = moderation_models.Report fields = ["q", "is_handled", "type", "submitter_email"] + + +class ManageNoteFilterSet(filters.FilterSet): + q = fields.SmartSearchFilter( + config=search.SearchConfig( + search_fields={"summary": {"to": "summary"}}, + filter_fields={ + "uuid": {"to": "uuid"}, + "author": get_actor_filter("author"), + }, + ) + ) + + class Meta: + model = moderation_models.Note + fields = ["q"] diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index f5fe1a53b..0bd364183 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -3,6 +3,7 @@ from django.db import transaction from rest_framework import serializers +from funkwhale_api.common import fields as common_fields from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import utils as common_utils from funkwhale_api.federation import models as federation_models @@ -676,3 +677,27 @@ class ManageReportSerializer(serializers.ModelSerializer): "target_owner", "summary", ] + + +class ManageNoteSerializer(serializers.ModelSerializer): + author = ManageBaseActorSerializer(required=False) + target = common_fields.GenericRelation( + { + "report": { + "queryset": moderation_models.Report.objects.all(), + "id_attr": "uuid", + "id_field": serializers.UUIDField(), + }, + "account": { + "queryset": federation_models.Actor.objects.all(), + "id_attr": "full_username", + "id_field": serializers.EmailField(), + "get_query": moderation_serializers.get_actor_query, + }, + } + ) + + class Meta: + model = moderation_models.Note + fields = ["id", "uuid", "creation_date", "summary", "author", "target"] + read_only_fields = ["uuid", "creation_date", "author"] diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index f97fd5e30..36997b24a 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -18,6 +18,7 @@ moderation_router.register( r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies" ) moderation_router.register(r"reports", views.ManageReportViewSet, "reports") +moderation_router.register(r"notes", views.ManageNoteViewSet, "notes") users_router = routers.OptionalSlashRouter() users_router.register(r"users", views.ManageUserViewSet, "users") diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 70f1b3a27..4c8f6655e 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -478,6 +478,27 @@ class ManageReportViewSet( ordering_fields = ["id", "creation_date", "handled_date"] +class ManageNoteViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.CreateModelMixin, + viewsets.GenericViewSet, +): + lookup_field = "uuid" + queryset = ( + moderation_models.Note.objects.all().order_by("-creation_date").select_related() + ) + serializer_class = serializers.ManageNoteSerializer + filterset_class = filters.ManageNoteFilterSet + required_scope = "instance:notes" + ordering_fields = ["id", "creation_date"] + + def perform_create(self, serializer): + author = self.request.user.actor + return serializer.save(author=author) + + class ManageTagViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, diff --git a/api/funkwhale_api/moderation/admin.py b/api/funkwhale_api/moderation/admin.py index 4017ab86d..1a44ea283 100644 --- a/api/funkwhale_api/moderation/admin.py +++ b/api/funkwhale_api/moderation/admin.py @@ -30,7 +30,6 @@ class InstancePolicyAdmin(admin.ModelAdmin): list_select_related = True - @admin.register(models.Report) class ReportAdmin(admin.ModelAdmin): list_display = [ @@ -42,13 +41,8 @@ class ReportAdmin(admin.ModelAdmin): "creation_date", "handled_date", ] - list_filter = [ - "type", - "is_handled", - ] - search_fields = [ - "summary", - ] + list_filter = ["type", "is_handled"] + search_fields = ["summary"] list_select_related = True diff --git a/api/funkwhale_api/moderation/factories.py b/api/funkwhale_api/moderation/factories.py index 75cec7c7f..b426a6cea 100644 --- a/api/funkwhale_api/moderation/factories.py +++ b/api/funkwhale_api/moderation/factories.py @@ -41,6 +41,16 @@ class UserFilterFactory(NoUpdateOnCreate, factory.DjangoModelFactory): ) +@registry.register +class NoteFactory(NoUpdateOnCreate, factory.DjangoModelFactory): + author = factory.SubFactory(federation_factories.ActorFactory) + target = None + summary = factory.Faker("paragraph") + + class Meta: + model = "moderation.Note" + + @registry.register class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory): submitter = factory.SubFactory(federation_factories.ActorFactory) diff --git a/api/funkwhale_api/moderation/migrations/0004_note.py b/api/funkwhale_api/moderation/migrations/0004_note.py new file mode 100644 index 000000000..8e7454b69 --- /dev/null +++ b/api/funkwhale_api/moderation/migrations/0004_note.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.4 on 2019-08-29 09:08 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0020_auto_20190730_0846'), + ('contenttypes', '0002_remove_content_type_name'), + ('moderation', '0003_report'), + ] + + operations = [ + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('summary', models.TextField(max_length=50000)), + ('target_id', models.IntegerField(null=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_notes', to='federation.Actor')), + ('target_content_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py index 9966cdb56..1aebfddf6 100644 --- a/api/funkwhale_api/moderation/models.py +++ b/api/funkwhale_api/moderation/models.py @@ -164,6 +164,21 @@ class Report(federation_models.FederationMixin): return super().save(**kwargs) +class Note(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, unique=True) + creation_date = models.DateTimeField(default=timezone.now) + summary = models.TextField(max_length=50000) + author = models.ForeignKey( + "federation.Actor", related_name="moderation_notes", on_delete=models.CASCADE + ) + + 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") + + @receiver(pre_save, sender=Report) def set_handled_date(sender, instance, **kwargs): if instance.is_handled is True and not instance.handled_date: diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py index 46b90e7a9..d6a954a15 100644 --- a/api/funkwhale_api/moderation/serializers.py +++ b/api/funkwhale_api/moderation/serializers.py @@ -119,7 +119,15 @@ class TrackStateSerializer(serializers.ModelSerializer): class LibraryStateSerializer(serializers.ModelSerializer): class Meta: model = music_models.Library - fields = ["id", "uuid", "fid", "name", "description", "creation_date", "privacy_level"] + fields = [ + "id", + "uuid", + "fid", + "name", + "description", + "creation_date", + "privacy_level", + ] @state_serializers.register(name="playlists.Playlist") diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index d19de05aa..e0958bc82 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -48,6 +48,8 @@ PERMISSIONS_CONFIGURATION = { "write:instance:domains", "read:instance:reports", "write:instance:reports", + "read:instance:notes", + "write:instance:notes", }, }, "library": { diff --git a/api/funkwhale_api/users/oauth/scopes.py b/api/funkwhale_api/users/oauth/scopes.py index e70f20256..88b928c50 100644 --- a/api/funkwhale_api/users/oauth/scopes.py +++ b/api/funkwhale_api/users/oauth/scopes.py @@ -35,6 +35,7 @@ BASE_SCOPES = [ Scope("instance:domains", "Access instance domains"), Scope("instance:policies", "Access instance moderation policies"), Scope("instance:reports", "Access instance moderation reports"), + Scope("instance:notes", "Access instance moderation notes"), ] SCOPES = [ Scope("read", children=[s.copy("read") for s in BASE_SCOPES]), diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 4056c1819..126ade6bc 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -550,3 +550,20 @@ def test_manage_report_serializer(factories, to_api_date): s = serializers.ManageReportSerializer(report) assert s.data == expected + + +def test_manage_note_serializer(factories, to_api_date): + actor = factories["federation.Actor"]() + note = factories["moderation.Note"](target=actor) + + expected = { + "id": note.id, + "uuid": str(note.uuid), + "summary": note.summary, + "creation_date": to_api_date(note.creation_date), + "author": serializers.ManageBaseActorSerializer(note.author).data, + "target": {"type": "account", "full_username": actor.full_username}, + } + s = serializers.ManageNoteSerializer(note) + + assert s.data == expected diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index 8bd414db2..f426dd76e 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -391,6 +391,50 @@ def test_upload_delete(factories, superuser_api_client): assert response.status_code == 204 +def test_note_create(factories, superuser_api_client): + actor = superuser_api_client.user.create_actor() + target = factories["federation.Actor"]() + data = { + "summary": "Hello", + "target": {"type": "account", "full_username": target.full_username}, + } + url = reverse("api:v1:manage:moderation:notes-list") + response = superuser_api_client.post(url, data, format="json") + assert response.status_code == 201 + + note = actor.moderation_notes.latest("id") + assert note.target == target + assert response.data == serializers.ManageNoteSerializer(note).data + + +def test_note_list(factories, superuser_api_client, settings): + note = factories["moderation.Note"]() + url = reverse("api:v1:manage:moderation:notes-list") + response = superuser_api_client.get(url) + + assert response.status_code == 200 + + assert response.data["count"] == 1 + assert response.data["results"][0] == serializers.ManageNoteSerializer(note).data + + +def test_note_delete(factories, superuser_api_client): + note = factories["moderation.Note"]() + url = reverse("api:v1:manage:moderation:notes-detail", kwargs={"uuid": note.uuid}) + response = superuser_api_client.delete(url) + + assert response.status_code == 204 + + +def test_note_detail(factories, superuser_api_client): + note = factories["moderation.Note"]() + url = reverse("api:v1:manage:moderation:notes-detail", kwargs={"uuid": note.uuid}) + response = superuser_api_client.get(url) + + assert response.status_code == 200 + assert response.data == serializers.ManageNoteSerializer(note).data + + def test_tag_detail(factories, superuser_api_client): tag = factories["tags.Tag"]() url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name})