From b659eec4df53c0cf66f0b263946026c356c381a9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 26 Aug 2019 14:37:48 +0200 Subject: [PATCH 01/36] See #890: initial admin serializer for reports --- api/funkwhale_api/manage/serializers.py | 40 ++++++++++++++++++++ api/funkwhale_api/moderation/factories.py | 12 ++++++ api/funkwhale_api/moderation/serializers.py | 41 +++++++++++---------- api/tests/manage/test_serializers.py | 28 ++++++++++++++ 4 files changed, 102 insertions(+), 19 deletions(-) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 8d4b01b05..38a9cd57a 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -8,6 +8,7 @@ from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import fields as federation_fields from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.moderation import models as moderation_models +from funkwhale_api.moderation import serializers as moderation_serializers from funkwhale_api.music import models as music_models from funkwhale_api.music import serializers as music_serializers from funkwhale_api.tags import models as tags_models @@ -629,3 +630,42 @@ class ManageTagActionSerializer(common_serializers.ActionSerializer): @transaction.atomic def handle_delete(self, objects): return objects.delete() + + +class ManageReportSerializer(serializers.ModelSerializer): + assigned_to = ManageBaseActorSerializer() + target_owner = ManageBaseActorSerializer() + submitter = ManageBaseActorSerializer() + target = moderation_serializers.TARGET_FIELD + + class Meta: + model = moderation_models.Report + fields = [ + "id", + "uuid", + "fid", + "creation_date", + "handled_date", + "summary", + "type", + "target", + "target_state", + "is_handled", + "assigned_to", + "target_owner", + "submitter", + "submitter_email", + ] + read_only_fields = [ + "id", + "uuid", + "fid", + "submitter", + "submitter_email", + "creation_date", + "handled_date", + "target", + "target_state", + "target_owner", + "summary", + ] diff --git a/api/funkwhale_api/moderation/factories.py b/api/funkwhale_api/moderation/factories.py index 4bf7ce584..3db5d2060 100644 --- a/api/funkwhale_api/moderation/factories.py +++ b/api/funkwhale_api/moderation/factories.py @@ -5,6 +5,8 @@ from funkwhale_api.federation import factories as federation_factories from funkwhale_api.music import factories as music_factories from funkwhale_api.users import factories as users_factories +from . import serializers + @registry.register class InstancePolicyFactory(NoUpdateOnCreate, factory.DjangoModelFactory): @@ -51,3 +53,13 @@ class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory): class Params: anonymous = factory.Trait(actor=None, submitter_email=factory.Faker("email")) + assigned = factory.Trait( + assigned_to=factory.SubFactory(federation_factories.ActorFactory) + ) + + @factory.post_generation + def _set_target_owner(self, create, extracted, **kwargs): + if not self.target: + return + + self.target_owner = serializers.get_target_owner(self.target) diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py index c52a9e913..6e216f29e 100644 --- a/api/funkwhale_api/moderation/serializers.py +++ b/api/funkwhale_api/moderation/serializers.py @@ -160,26 +160,29 @@ def get_target_owner(target): return mapping[target.__class__](target) +TARGET_FIELD = 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 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, - }, - } - ) + target = TARGET_FIELD class Meta: model = models.Report diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 16fd9ce58..7f99dd901 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -521,3 +521,31 @@ def test_manage_tag_serializer(factories, to_api_date): s = serializers.ManageTagSerializer(tag) assert s.data == expected + + +def test_manage_report_serializer(factories, to_api_date): + artist = factories["music.Artist"](attributed=True) + report = factories["moderation.Report"]( + target=artist, target_state={"hello": "world"}, assigned=True + ) + expected = { + "id": report.id, + "uuid": str(report.uuid), + "fid": report.fid, + "creation_date": to_api_date(report.creation_date), + "handled_date": None, + "summary": report.summary, + "is_handled": report.is_handled, + "type": report.type, + "submitter_email": None, + "submitter": serializers.ManageBaseActorSerializer(report.submitter).data, + "assigned_to": serializers.ManageBaseActorSerializer(report.assigned_to).data, + "target": {"type": "artist", "id": artist.pk}, + "target_owner": serializers.ManageBaseActorSerializer( + artist.attributed_to + ).data, + "target_state": report.target_state, + } + s = serializers.ManageReportSerializer(report) + + assert s.data == expected From 177f06cf2a4957f37629a396f5195a72253286a7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 26 Aug 2019 14:47:01 +0200 Subject: [PATCH 02/36] See #890: Ensure report handled_date is populated automatically when handling the report --- api/funkwhale_api/moderation/models.py | 11 ++++++++++- api/tests/moderation/test_models.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 api/tests/moderation/test_models.py diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py index ccc891e79..74fa4fd1b 100644 --- a/api/funkwhale_api/moderation/models.py +++ b/api/funkwhale_api/moderation/models.py @@ -6,13 +6,14 @@ 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.models.signals import pre_save +from django.dispatch import receiver 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): return self.filter(is_active=True) @@ -160,3 +161,11 @@ class Report(federation_models.FederationMixin): self.fid = self.get_federation_id() return super().save(**kwargs) + + +@receiver(pre_save, sender=Report) +def set_handled_date(sender, instance, **kwargs): + if instance.is_handled is True and not instance.handled_date: + instance.handled_date = timezone.now() + elif not instance.is_handled: + instance.handled_date = None \ No newline at end of file diff --git a/api/tests/moderation/test_models.py b/api/tests/moderation/test_models.py new file mode 100644 index 000000000..8c1de981f --- /dev/null +++ b/api/tests/moderation/test_models.py @@ -0,0 +1,22 @@ +def test_setting_report_handled_to_true_sets_handled_date(factories, now): + target = factories["music.Artist"]() + report = factories["moderation.Report"](target=target) + + assert report.is_handled is False + assert report.handled_date is None + + report.is_handled = True + report.save() + + assert report.handled_date == now + + +def test_setting_report_handled_to_false_sets_handled_date_to_null(factories, now): + target = factories["music.Artist"]() + report = factories["moderation.Report"]( + target=target, is_handled=True, handled_date=now + ) + report.is_handled = False + report.save() + + assert report.handled_date is None From 815d9c02f81c8caf22de28ddc33d14e0b876a5e1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 26 Aug 2019 14:48:30 +0200 Subject: [PATCH 03/36] See #890: Added Oauth scope for managing instance reports --- api/funkwhale_api/users/models.py | 2 ++ api/funkwhale_api/users/oauth/scopes.py | 1 + 2 files changed, 3 insertions(+) diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 1e95aac2a..d19de05aa 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -46,6 +46,8 @@ PERMISSIONS_CONFIGURATION = { "write:instance:accounts", "read:instance:domains", "write:instance:domains", + "read:instance:reports", + "write:instance:reports", }, }, "library": { diff --git a/api/funkwhale_api/users/oauth/scopes.py b/api/funkwhale_api/users/oauth/scopes.py index 8cf91192c..e70f20256 100644 --- a/api/funkwhale_api/users/oauth/scopes.py +++ b/api/funkwhale_api/users/oauth/scopes.py @@ -34,6 +34,7 @@ BASE_SCOPES = [ Scope("instance:accounts", "Access instance federated accounts"), Scope("instance:domains", "Access instance domains"), Scope("instance:policies", "Access instance moderation policies"), + Scope("instance:reports", "Access instance moderation reports"), ] SCOPES = [ Scope("read", children=[s.copy("read") for s in BASE_SCOPES]), From 8f7ab82117e083f38c52067df04d89b5178b99ca Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 26 Aug 2019 15:27:21 +0200 Subject: [PATCH 04/36] See #890: initial API endpoint to handle management of reports --- api/funkwhale_api/manage/filters.py | 23 ++++++++++++ api/funkwhale_api/manage/urls.py | 1 + api/funkwhale_api/manage/views.py | 19 ++++++++++ api/funkwhale_api/moderation/factories.py | 2 +- api/funkwhale_api/moderation/models.py | 3 +- api/tests/manage/test_views.py | 44 +++++++++++++++++++++++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 6a6e7b99d..f4ebb1172 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -339,3 +339,26 @@ class ManageTagFilterSet(filters.FilterSet): class Meta: model = tags_models.Tag fields = ["q"] + + +class ManageReportFilterSet(filters.FilterSet): + q = fields.SmartSearchFilter( + config=search.SearchConfig( + search_fields={"summary": {"to": "summary"}}, + filter_fields={ + "uuid": {"to": "uuid"}, + "id": {"to": "id"}, + "is_handled": {"to": "is_handled"}, + "domain": {"to": "target_owner__domain_id"}, + "type": {"to": "type"}, + "submitter": get_actor_filter("submitter"), + "assigned_to": get_actor_filter("assigned_to"), + "target_owner": get_actor_filter("target_owner"), + "submitter_email": {"to": "submitter_email"}, + }, + ) + ) + + class Meta: + model = moderation_models.Report + fields = ["q", "is_handled", "type", "submitter_email"] diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index b830f0023..f97fd5e30 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -17,6 +17,7 @@ moderation_router = routers.OptionalSlashRouter() moderation_router.register( r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies" ) +moderation_router.register(r"reports", views.ManageReportViewSet, "reports") 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 ea3c33f80..70f1b3a27 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -459,6 +459,25 @@ class ManageInstancePolicyViewSet( serializer.save(actor=self.request.user.actor) +class ManageReportViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): + lookup_field = "uuid" + queryset = ( + moderation_models.Report.objects.all() + .order_by("-creation_date") + .select_related() + ) + serializer_class = serializers.ManageReportSerializer + filterset_class = filters.ManageReportFilterSet + required_scope = "instance:reports" + ordering_fields = ["id", "creation_date", "handled_date"] + + class ManageTagViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, diff --git a/api/funkwhale_api/moderation/factories.py b/api/funkwhale_api/moderation/factories.py index 3db5d2060..75cec7c7f 100644 --- a/api/funkwhale_api/moderation/factories.py +++ b/api/funkwhale_api/moderation/factories.py @@ -44,7 +44,7 @@ class UserFilterFactory(NoUpdateOnCreate, factory.DjangoModelFactory): @registry.register class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory): submitter = factory.SubFactory(federation_factories.ActorFactory) - target = None + target = factory.SubFactory(music_factories.ArtistFactory) summary = factory.Faker("paragraph") type = "other" diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py index 74fa4fd1b..9966cdb56 100644 --- a/api/funkwhale_api/moderation/models.py +++ b/api/funkwhale_api/moderation/models.py @@ -14,6 +14,7 @@ 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): return self.filter(is_active=True) @@ -168,4 +169,4 @@ def set_handled_date(sender, instance, **kwargs): if instance.is_handled is True and not instance.handled_date: instance.handled_date = timezone.now() elif not instance.is_handled: - instance.handled_date = None \ No newline at end of file + instance.handled_date = None diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index a164e4012..8bd414db2 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -417,3 +417,47 @@ def test_tag_delete(factories, superuser_api_client): response = superuser_api_client.delete(url) assert response.status_code == 204 + + +def test_report_detail(factories, superuser_api_client): + report = factories["moderation.Report"]() + url = reverse( + "api:v1:manage:moderation:reports-detail", kwargs={"uuid": report.uuid} + ) + response = superuser_api_client.get(url) + + assert response.status_code == 200 + assert response.data["summary"] == report.summary + + +def test_report_list(factories, superuser_api_client, settings): + report = factories["moderation.Report"]() + url = reverse("api:v1:manage:moderation:reports-list") + response = superuser_api_client.get(url) + + assert response.status_code == 200 + + assert response.data["count"] == 1 + assert response.data["results"][0]["summary"] == report.summary + + +def test_report_update(factories, superuser_api_client): + report = factories["moderation.Report"]() + url = reverse( + "api:v1:manage:moderation:reports-detail", kwargs={"uuid": report.uuid} + ) + response = superuser_api_client.patch(url, {"is_handled": True}) + + assert response.status_code == 200 + report.refresh_from_db() + assert report.is_handled is True + + +def test_report_delete(factories, superuser_api_client): + report = factories["moderation.Report"]() + url = reverse( + "api:v1:manage:moderation:reports-detail", kwargs={"uuid": report.uuid} + ) + response = superuser_api_client.delete(url) + + assert response.status_code == 204 From f20b27622fc0533224fa8cae6dc12f789af8adcd Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 27 Aug 2019 10:55:24 +0200 Subject: [PATCH 05/36] See #890: Display the number of unhandled reports in the sidebar --- front/src/components/Sidebar.vue | 4 ++++ front/src/store/auth.js | 7 ++++++- front/src/store/ui.js | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 03084a634..6f72a9c2f 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -95,6 +95,10 @@ class="item" :to="{name: 'manage.moderation.domains.list'}"> Moderation +
{{ $store.state.ui.notifications.pendingReviewReports }}
{ + commit('notifications', {type: 'pendingReviewReports', count: response.data.count}) + }) + }, websocketEvent ({state}, event) { let handlers = state.websocketEventsHandlers[event.type] console.log('Dispatching websocket event', event, handlers) From 00efe7e854137a3a87d87f21b0e2b004d12e8101 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 27 Aug 2019 13:44:26 +0200 Subject: [PATCH 06/36] Fix unrelated broken actor filtering --- api/funkwhale_api/manage/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index f4ebb1172..6ea3b8c99 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -26,7 +26,7 @@ class ActorField(forms.CharField): def get_actor_filter(actor_field): def handler(v): - federation_utils.get_actor_from_username_data_query(actor_field, v) + return federation_utils.get_actor_from_username_data_query(actor_field, v) return {"field": ActorField(), "handler": handler} From d7705593a9b97992f54038a503aa81af7f730f03 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 27 Aug 2019 13:49:54 +0200 Subject: [PATCH 07/36] See #890: added table to list reports --- api/funkwhale_api/common/filters.py | 14 +- api/funkwhale_api/manage/filters.py | 5 +- front/src/components/Sidebar.vue | 2 +- front/src/router/index.js | 16 +- .../views/admin/moderation/ReportsList.vue | 268 ++++++++++++++++++ 5 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 front/src/views/admin/moderation/ReportsList.vue diff --git a/api/funkwhale_api/common/filters.py b/api/funkwhale_api/common/filters.py index 364a1fba1..58244b67f 100644 --- a/api/funkwhale_api/common/filters.py +++ b/api/funkwhale_api/common/filters.py @@ -15,7 +15,7 @@ class NoneObject(object): NONE = NoneObject() -NULL_BOOLEAN_CHOICES = [ +BOOLEAN_CHOICES = [ (True, True), ("true", True), ("True", True), @@ -26,6 +26,8 @@ NULL_BOOLEAN_CHOICES = [ ("False", False), ("0", False), ("no", False), +] +NULL_BOOLEAN_CHOICES = BOOLEAN_CHOICES + [ ("None", NONE), ("none", NONE), ("Null", NONE), @@ -76,10 +78,18 @@ def clean_null_boolean_filter(v): return v +def clean_boolean_filter(v): + return CoerceChoiceField(choices=BOOLEAN_CHOICES).clean(v) + + def get_null_boolean_filter(name): return {"handler": lambda v: Q(**{name: clean_null_boolean_filter(v)})} +def get_boolean_filter(name): + return {"handler": lambda v: Q(**{name: clean_boolean_filter(v)})} + + class DummyTypedMultipleChoiceField(forms.TypedMultipleChoiceField): def valid_value(self, value): return True @@ -142,7 +152,7 @@ class MutationFilter(filters.FilterSet): "domain": {"to": "created_by__domain__name__iexact"}, "is_approved": get_null_boolean_filter("is_approved"), "target": {"handler": filter_target}, - "is_applied": {"to": "is_applied"}, + "is_applied": get_boolean_filter("is_applied"), }, ) ) diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 6ea3b8c99..59577cd39 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -5,6 +5,7 @@ import django_filters from django_filters import rest_framework as filters from funkwhale_api.common import fields +from funkwhale_api.common import filters as common_filters from funkwhale_api.common import search from funkwhale_api.federation import models as federation_models @@ -348,9 +349,9 @@ class ManageReportFilterSet(filters.FilterSet): filter_fields={ "uuid": {"to": "uuid"}, "id": {"to": "id"}, - "is_handled": {"to": "is_handled"}, + "resolved": common_filters.get_boolean_filter("is_handled"), "domain": {"to": "target_owner__domain_id"}, - "type": {"to": "type"}, + "category": {"to": "type"}, "submitter": get_actor_filter("submitter"), "assigned_to": get_actor_filter("assigned_to"), "target_owner": get_actor_filter("target_owner"), diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 6f72a9c2f..6d9e0c0ec 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -93,7 +93,7 @@ + :to="{name: 'manage.moderation.reports.list'}"> Moderation
+ import( + /* webpackChunkName: "admin" */ "@/views/admin/moderation/ReportsList" + ), + props: route => { + return { + defaultQuery: route.query.q, + updateUrl: true + } + } + }, ] }, { diff --git a/front/src/views/admin/moderation/ReportsList.vue b/front/src/views/admin/moderation/ReportsList.vue new file mode 100644 index 000000000..d5781e1b4 --- /dev/null +++ b/front/src/views/admin/moderation/ReportsList.vue @@ -0,0 +1,268 @@ + + + + + + From 9488094ff9c124c4df418d5a42d7e661e2736cc7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 27 Aug 2019 14:00:33 +0200 Subject: [PATCH 08/36] See #890: Added is_local attribute on admin account API representations --- api/funkwhale_api/manage/serializers.py | 7 +++++++ api/tests/manage/test_serializers.py | 1 + 2 files changed, 8 insertions(+) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 38a9cd57a..f5fe1a53b 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import transaction from rest_framework import serializers @@ -183,6 +184,8 @@ class ManageDomainActionSerializer(common_serializers.ActionSerializer): class ManageBaseActorSerializer(serializers.ModelSerializer): + is_local = serializers.SerializerMethodField() + class Meta: model = federation_models.Actor fields = [ @@ -201,9 +204,13 @@ class ManageBaseActorSerializer(serializers.ModelSerializer): "outbox_url", "shared_inbox_url", "manually_approves_followers", + "is_local", ] read_only_fields = ["creation_date", "instance_policy"] + def get_is_local(self, o): + return o.domain_id == settings.FEDERATION_HOSTNAME + class ManageActorSerializer(ManageBaseActorSerializer): uploads_count = serializers.SerializerMethodField() diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 7f99dd901..4056c1819 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -87,6 +87,7 @@ def test_manage_actor_serializer(factories, now, to_api_date): "full_username": actor.full_username, "user": None, "instance_policy": None, + "is_local": False, } s = serializers.ManageActorSerializer(actor) From 1912a7f6d26eefd334615d080f8431ec089fb66c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Aug 2019 10:46:39 +0200 Subject: [PATCH 09/36] See #890: link to unresolved reports by default --- front/src/components/Sidebar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 6d9e0c0ec..81b498034 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -93,7 +93,7 @@ + :to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"> Moderation
Date: Wed, 28 Aug 2019 11:31:57 +0200 Subject: [PATCH 10/36] Added HTML sanitizer --- front/package.json | 3 ++- front/src/sanitize.js | 44 ++++++++++++++++++++++++++++++++ front/yarn.lock | 58 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 front/src/sanitize.js diff --git a/front/package.json b/front/package.json index 2c8f041f0..f00a0681c 100644 --- a/front/package.json +++ b/front/package.json @@ -15,14 +15,15 @@ "dateformat": "^3.0.3", "diff": "^4.0.1", "django-channels": "^1.1.6", + "fomantic-ui-css": "^2.7", "howler": "^2.0.14", "js-logger": "^1.4.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.10", "masonry-layout": "^4.2.2", "moment": "^2.22.2", - "fomantic-ui-css": "^2.7", "qs": "^6.7.0", + "sanitize-html": "^1.20.1", "showdown": "^1.8.6", "vue": "^2.5.17", "vue-gettext": "^2.1.0", diff --git a/front/src/sanitize.js b/front/src/sanitize.js new file mode 100644 index 000000000..a4b4e5c8e --- /dev/null +++ b/front/src/sanitize.js @@ -0,0 +1,44 @@ +import sanitizeHtml from "sanitize-html" + +const allowedTags = [ + "h3", + "h4", + "h5", + "h6", + "blockquote", + "p", + "a", + "ul", + "ol", + "nl", + "li", + "b", + "i", + "strong", + "em", + "strike", + "code", + "hr", + "br", + "div", + "table", + "thead", + "caption", + "tbody", + "tr", + "th", + "td", + "pre", + "iframe" +] +const allowedAttributes = { + a: ["href", "name", "target"], + // We don't currently allow img itself by default, but this + // would make sense if we did. You could add srcset here, + // and if you do the URL is checked for safety + img: ["src"] +} + +export default function sanitize(input) { + return sanitizeHtml(input, {allowedAttributes, allowedAttributes}) +} diff --git a/front/yarn.lock b/front/yarn.lock index 68e128385..fe239302c 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1547,7 +1547,7 @@ array-union@^1.0.1, array-union@^1.0.2: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1: +array-uniq@^1.0.1, array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= @@ -4485,7 +4485,7 @@ html-webpack-plugin@^3.2.0: toposort "^1.0.0" util.promisify "1.0.0" -htmlparser2@^3.3.0, htmlparser2@^3.8.2, htmlparser2@^3.9.1: +htmlparser2@^3.10.0, htmlparser2@^3.3.0, htmlparser2@^3.8.2, htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -5401,16 +5401,36 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.defaultsdeep@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81" integrity sha1-vsECT4WxvZbL6kBbI8FK1kQ6b4E= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -5426,6 +5446,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.mergewith@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -7806,6 +7831,22 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.1.tgz#f6effdf55dd398807171215a62bfc21811bacf85" + integrity sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA== + dependencies: + chalk "^2.4.1" + htmlparser2 "^3.10.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.mergewith "^4.6.1" + postcss "^7.0.5" + srcset "^1.0.0" + xtend "^4.0.1" + sass-graph@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" @@ -8243,6 +8284,14 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8= + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -9472,6 +9521,11 @@ xtend@^4.0.0, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= +xtend@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" From fbb814bca8885f8be4b7bc7b37c72817d26480f1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Aug 2019 11:32:08 +0200 Subject: [PATCH 11/36] See #890: added django admin module for reports --- api/funkwhale_api/moderation/admin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/funkwhale_api/moderation/admin.py b/api/funkwhale_api/moderation/admin.py index 9f8340030..4017ab86d 100644 --- a/api/funkwhale_api/moderation/admin.py +++ b/api/funkwhale_api/moderation/admin.py @@ -30,6 +30,28 @@ class InstancePolicyAdmin(admin.ModelAdmin): list_select_related = True + +@admin.register(models.Report) +class ReportAdmin(admin.ModelAdmin): + list_display = [ + "uuid", + "submitter", + "type", + "assigned_to", + "is_handled", + "creation_date", + "handled_date", + ] + list_filter = [ + "type", + "is_handled", + ] + search_fields = [ + "summary", + ] + list_select_related = True + + @admin.register(models.UserFilter) class UserFilterAdmin(admin.ModelAdmin): list_display = ["uuid", "user", "target_artist", "creation_date"] From ef3c37584aa78d082eb2940f42d36ba0a1e1eb69 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Aug 2019 11:32:33 +0200 Subject: [PATCH 12/36] fixup! Added HTML sanitizer --- front/src/sanitize.js | 1 - 1 file changed, 1 deletion(-) diff --git a/front/src/sanitize.js b/front/src/sanitize.js index a4b4e5c8e..4e42d5a4b 100644 --- a/front/src/sanitize.js +++ b/front/src/sanitize.js @@ -29,7 +29,6 @@ const allowedTags = [ "th", "td", "pre", - "iframe" ] const allowedAttributes = { a: ["href", "name", "target"], From f48f74dcb34b85ac2d139d9bce066d56df52c028 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Aug 2019 17:12:54 +0200 Subject: [PATCH 13/36] See #890: store domain name and local status of reported objects --- api/funkwhale_api/moderation/serializers.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py index 6e216f29e..46b90e7a9 100644 --- a/api/funkwhale_api/moderation/serializers.py +++ b/api/funkwhale_api/moderation/serializers.py @@ -1,5 +1,7 @@ -import persisting_theory +import urllib.parse +from django.conf import settings +import persisting_theory from rest_framework import serializers from funkwhale_api.common import fields as common_fields @@ -117,7 +119,7 @@ class TrackStateSerializer(serializers.ModelSerializer): class LibraryStateSerializer(serializers.ModelSerializer): class Meta: model = music_models.Library - fields = ["id", "fid", "name", "description", "creation_date", "privacy_level"] + fields = ["id", "uuid", "fid", "name", "description", "creation_date", "privacy_level"] @state_serializers.register(name="playlists.Playlist") @@ -135,6 +137,7 @@ class ActorStateSerializer(serializers.ModelSerializer): "fid", "name", "preferred_username", + "full_username", "summary", "domain", "type", @@ -228,5 +231,14 @@ class ReportSerializer(serializers.ModelSerializer): validated_data["target_state"] = target_state_serializer( validated_data["target"] ).data + if "fid" in validated_data["target_state"]: + validated_data["target_state"]["domain"] = urllib.parse.urlparse( + validated_data["target_state"]["fid"] + ).hostname + + validated_data["target_state"]["is_local"] = ( + validated_data["target_state"].get("domain", settings.FEDERATION_HOSTNAME) + == settings.FEDERATION_HOSTNAME + ) validated_data["target_owner"] = get_target_owner(validated_data["target"]) return super().create(validated_data) From 6e82780e0d90bd4a1edecbc470acad730220ef95 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Aug 2019 17:13:26 +0200 Subject: [PATCH 14/36] See #890: improved report card design, to include relevant context --- front/src/components/common/ExpandableDiv.vue | 32 ++ front/src/components/globals.js | 3 + front/src/components/library/EditCard.vue | 3 + .../manage/moderation/ReportCard.vue | 281 ++++++++++++++++++ front/src/entities.js | 201 +++++++++++++ front/src/style/_main.scss | 8 + front/src/views/admin/moderation/Base.vue | 3 + .../views/admin/moderation/ReportsList.vue | 94 +++--- 8 files changed, 584 insertions(+), 41 deletions(-) create mode 100644 front/src/components/common/ExpandableDiv.vue create mode 100644 front/src/components/manage/moderation/ReportCard.vue create mode 100644 front/src/entities.js diff --git a/front/src/components/common/ExpandableDiv.vue b/front/src/components/common/ExpandableDiv.vue new file mode 100644 index 000000000..653286ad2 --- /dev/null +++ b/front/src/components/common/ExpandableDiv.vue @@ -0,0 +1,32 @@ + + diff --git a/front/src/components/globals.js b/front/src/components/globals.js index 711b227ae..4131c5d15 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -48,4 +48,7 @@ import EmptyState from '@/components/common/EmptyState' Vue.component('empty-state', EmptyState) +import ExpandableDiv from '@/components/common/ExpandableDiv' + +Vue.component('expandable-div', ExpandableDiv) export default {} diff --git a/front/src/components/library/EditCard.vue b/front/src/components/library/EditCard.vue index bb8676cee..fc5efea55 100644 --- a/front/src/components/library/EditCard.vue +++ b/front/src/components/library/EditCard.vue @@ -158,6 +158,9 @@ export default { }, updatedFields () { + if (!this.obj.target) { + return [] + } let payload = this.obj.payload let previousState = this.previousState let fields = Object.keys(payload) diff --git a/front/src/components/manage/moderation/ReportCard.vue b/front/src/components/manage/moderation/ReportCard.vue new file mode 100644 index 000000000..bbf475b88 --- /dev/null +++ b/front/src/components/manage/moderation/ReportCard.vue @@ -0,0 +1,281 @@ + + + diff --git a/front/src/entities.js b/front/src/entities.js new file mode 100644 index 000000000..48d39df77 --- /dev/null +++ b/front/src/entities.js @@ -0,0 +1,201 @@ +function getTagsValueRepr (val) { + if (!val) { + return '' + } + return val.slice().sort().join('\n') +} + +export default { + getConfigs () { + return { + artist: { + label: this.$pgettext('*/*/*', 'Artist'), + icon: 'users', + urls: { + getAdminDetail: (obj) => { return {name: 'manage.library.artists.detail', params: {id: obj.id}}} + }, + moderatedFields: [ + { + id: 'name', + label: this.$pgettext('*/*/*/Noun', 'Name'), + getValue: (obj) => { return obj.name } + }, + { + id: 'creation_date', + label: this.$pgettext('*/*/*/Noun', 'Creation date'), + getValue: (obj) => { return obj.creation_date } + }, + { + id: 'tags', + type: 'tags', + label: this.$pgettext('*/*/*/Noun', 'Tags'), + getValue: (obj) => { return obj.tags }, + getValueRepr: getTagsValueRepr + }, + { + id: 'mbid', + label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), + getValue: (obj) => { return obj.mbid } + }, + ] + }, + album: { + label: this.$pgettext('*/*/*', 'Album'), + icon: 'play', + urls: { + getAdminDetail: (obj) => { return {name: 'manage.library.albums.detail', params: {id: obj.id}}} + }, + moderatedFields: [ + { + id: 'title', + label: this.$pgettext('*/*/*/Noun', 'Title'), + getValue: (obj) => { return obj.title } + }, + { + id: 'creation_date', + label: this.$pgettext('*/*/*/Noun', 'Creation date'), + getValue: (obj) => { return obj.creation_date } + }, + { + id: 'release_date', + label: this.$pgettext('Content/*/*/Noun', 'Release date'), + getValue: (obj) => { return obj.release_date } + }, + { + id: 'tags', + type: 'tags', + required: true, + label: this.$pgettext('*/*/*/Noun', 'Tags'), + getValue: (obj) => { return obj.tags }, + getValueRepr: getTagsValueRepr + }, + { + id: 'mbid', + label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), + getValue: (obj) => { return obj.mbid } + }, + ] + }, + track: { + label: this.$pgettext('*/*/*', 'Track'), + icon: 'music', + urls: { + getAdminDetail: (obj) => { return {name: 'manage.library.tracks.detail', params: {id: obj.id}}} + }, + moderatedFields: [ + { + id: 'title', + label: this.$pgettext('*/*/*/Noun', 'Title'), + getValue: (obj) => { return obj.title } + }, + { + id: 'position', + label: this.$pgettext('*/*/*/Short, Noun', 'Position'), + getValue: (obj) => { return obj.position } + }, + { + id: 'copyright', + label: this.$pgettext('Content/Track/*/Noun', 'Copyright'), + getValue: (obj) => { return obj.copyright } + }, + { + id: 'license', + label: this.$pgettext('Content/*/*/Noun', 'License'), + getValue: (obj) => { return obj.license }, + }, + { + id: 'tags', + label: this.$pgettext('*/*/*/Noun', 'Tags'), + getValue: (obj) => { return obj.tags }, + getValueRepr: getTagsValueRepr + }, + { + id: 'mbid', + label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), + getValue: (obj) => { return obj.mbid } + }, + ] + }, + library: { + label: this.$pgettext('*/*/*', 'Library'), + icon: 'book', + urls: { + getAdminDetail: (obj) => { return {name: 'manage.library.libraries.detail', params: {id: obj.uuid}}} + }, + moderatedFields: [ + { + id: 'name', + label: this.$pgettext('*/*/*/Noun', 'Name'), + getValue: (obj) => { return obj.name } + }, + { + id: 'description', + label: this.$pgettext('*/*/*/Noun', 'Description'), + getValue: (obj) => { return obj.position } + }, + { + id: 'privacy_level', + label: this.$pgettext('*/*/*', 'Visibility'), + getValue: (obj) => { return obj.privacy_level } + }, + ] + }, + playlist: { + label: this.$pgettext('*/*/*', 'Playlist'), + icon: 'list', + urls: { + // getAdminDetail: (obj) => { return {name: 'manage.playlists.detail', params: {id: obj.id}}} + }, + moderatedFields: [ + { + id: 'name', + label: this.$pgettext('*/*/*/Noun', 'Name'), + getValue: (obj) => { return obj.name } + }, + { + id: 'privacy_level', + label: this.$pgettext('*/*/*', 'Visibility'), + getValue: (obj) => { return obj.privacy_level } + }, + ] + }, + account: { + label: this.$pgettext('*/*/*', 'Account'), + icon: 'user', + urls: { + getAdminDetail: (obj) => { return {name: 'manage.moderation.accounts.detail', params: {id: `${obj.preferred_username}@${obj.domain}`}}} + }, + moderatedFields: [ + { + id: 'name', + label: this.$pgettext('*/*/*/Noun', 'Name'), + getValue: (obj) => { return obj.name } + }, + { + id: 'summary', + label: this.$pgettext('*/*/*/Noun', 'Bio'), + getValue: (obj) => { return obj.summary } + }, + ] + }, + } + }, + + getConfig () { + return this.configs[this.objectType] + }, + getFieldConfig (configs, type, fieldId) { + let c = configs[type] + return c.fields.filter((f) => { + return f.id == fieldId + })[0] + }, + getCurrentStateForObj (obj, config) { + let s = {} + config.fields.forEach(f => { + s[f.id] = {value: f.getValue(obj)} + }) + return s + }, + +} diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 3687b6e22..cd863cb17 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -368,5 +368,13 @@ input + .help { margin-top: 0.5em; } +.expandable { + &:not(.expanded) { + overflow: hidden; + max-height: 15vh; + background: linear-gradient(top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 90%, rgba(0, 0, 0, 0.3) 100%); + } +} + @import "./themes/_light.scss"; @import "./themes/_dark.scss"; diff --git a/front/src/views/admin/moderation/Base.vue b/front/src/views/admin/moderation/Base.vue index d1c3ae29f..04753cd36 100644 --- a/front/src/views/admin/moderation/Base.vue +++ b/front/src/views/admin/moderation/Base.vue @@ -1,6 +1,9 @@ @@ -139,14 +74,12 @@ import ReportCard from '@/components/manage/moderation/ReportCard' import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown' import {normalizeQuery, parseTokens} from '@/search' import SmartSearchMixin from '@/components/mixins/SmartSearch' -import ActionTable from '@/components/common/ActionTable' export default { mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], components: { Pagination, - ActionTable, ReportCard, ReportCategoryDropdown, }, From 92a1914ef8fd46a55bd188f3aad30d2693f6f93c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 2 Sep 2019 16:34:39 +0200 Subject: [PATCH 35/36] See #890: fixed failing tests --- api/tests/manage/test_serializers.py | 1 + api/tests/users/oauth/test_scopes.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 126ade6bc..d8a2ee8f9 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -546,6 +546,7 @@ def test_manage_report_serializer(factories, to_api_date): artist.attributed_to ).data, "target_state": report.target_state, + "notes": [], } s = serializers.ManageReportSerializer(report) diff --git a/api/tests/users/oauth/test_scopes.py b/api/tests/users/oauth/test_scopes.py index 384e6ee8f..4943a8a1e 100644 --- a/api/tests/users/oauth/test_scopes.py +++ b/api/tests/users/oauth/test_scopes.py @@ -50,6 +50,10 @@ from funkwhale_api.users.oauth import scopes "write:instance:edits", "read:instance:libraries", "write:instance:libraries", + "read:instance:notes", + "write:instance:notes", + "read:instance:reports", + "write:instance:reports", }, ), ( @@ -91,6 +95,10 @@ from funkwhale_api.users.oauth import scopes "write:instance:users", "read:instance:invitations", "write:instance:invitations", + "read:instance:notes", + "write:instance:notes", + "read:instance:reports", + "write:instance:reports", }, ), ( @@ -126,6 +134,10 @@ from funkwhale_api.users.oauth import scopes "write:instance:accounts", "read:instance:domains", "write:instance:domains", + "read:instance:notes", + "write:instance:notes", + "read:instance:reports", + "write:instance:reports", }, ), ( From 0600819b380fca5b036b884c1e627c4ede37bb84 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Sep 2019 10:10:32 +0200 Subject: [PATCH 36/36] See #890: review tweaks --- api/funkwhale_api/manage/serializers.py | 1 - api/tests/moderation/test_serializers.py | 54 ++++++++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 21612ab78..48ea1dc50 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -695,7 +695,6 @@ class ManageReportSerializer(serializers.ModelSerializer): "target_state", "target_owner", "summary", - # "notes", ] def get_notes(self, o): diff --git a/api/tests/moderation/test_serializers.py b/api/tests/moderation/test_serializers.py index f20f27b2b..041a8f274 100644 --- a/api/tests/moderation/test_serializers.py +++ b/api/tests/moderation/test_serializers.py @@ -45,7 +45,6 @@ def test_user_filter_serializer_save(factories): ("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", @@ -54,7 +53,7 @@ def test_user_filter_serializer_save(factories): ), ], ) -def test_report_serializer_save( +def test_report_federated_entity_serializer_save( factory_name, target_type, id_field, state_serializer, factories, mocker, settings ): target = factories[factory_name]() @@ -80,13 +79,50 @@ def test_report_serializer_save( expected_state["_target"] = json.loads( json.dumps(target_data, cls=DjangoJSONEncoder) ) - if hasattr(target, "fid"): - expected_state["domain"] = urllib.parse.urlparse(target.fid).hostname - expected_state["is_local"] = ( - expected_state["domain"] == settings.FEDERATION_HOSTNAME - ) - else: - expected_state["is_local"] = True + expected_state["domain"] = urllib.parse.urlparse(target.fid).hostname + expected_state["is_local"] = ( + expected_state["domain"] == settings.FEDERATION_HOSTNAME + ) + + assert report.target == target + assert report.type == payload["type"] + assert report.summary == payload["summary"] + assert report.target_state == expected_state + assert report.target_owner == target_owner + get_target_owner.assert_called_once_with(target) + + +@pytest.mark.parametrize( + "factory_name, target_type, id_field, state_serializer", + [("playlists.Playlist", "playlist", "id", serializers.PlaylistStateSerializer)], +) +def test_report_local_entity_serializer_save( + factory_name, target_type, id_field, state_serializer, factories, mocker, settings +): + 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() + + expected_state = state_serializer(target).data + expected_state["_target"] = json.loads( + json.dumps(target_data, cls=DjangoJSONEncoder) + ) + expected_state["is_local"] = True assert report.target == target assert report.type == payload["type"] assert report.summary == payload["summary"]