From 33d1f879cf0ba928c475ddc4175c1a35bf15e83f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 9 Sep 2019 11:10:25 +0200 Subject: [PATCH] Report UI (end-user) --- api/funkwhale_api/favorites/serializers.py | 10 +- api/funkwhale_api/favorites/views.py | 4 +- api/funkwhale_api/history/serializers.py | 9 +- api/funkwhale_api/history/views.py | 4 +- api/funkwhale_api/instance/nodeinfo.py | 8 + api/funkwhale_api/moderation/models.py | 2 +- api/funkwhale_api/playlists/serializers.py | 8 + api/funkwhale_api/playlists/views.py | 2 +- api/tests/favorites/test_favorites.py | 18 +- api/tests/favorites/test_serializers.py | 20 +++ api/tests/history/test_serializers.py | 20 +++ api/tests/instance/test_nodeinfo.py | 56 ++++++ api/tests/playlists/test_serializers.py | 24 +++ front/src/App.vue | 3 + front/src/components/audio/PlayButton.vue | 17 +- front/src/components/audio/track/Widget.vue | 7 +- front/src/components/library/AlbumBase.vue | 11 ++ front/src/components/library/ArtistBase.vue | 14 +- front/src/components/library/TrackBase.vue | 11 ++ front/src/components/mixins/Report.vue | 75 ++++++++ .../moderation/ReportCategoryDropdown.vue | 22 ++- .../src/components/moderation/ReportModal.vue | 169 ++++++++++++++++++ front/src/components/playlists/Card.vue | 14 +- front/src/store/moderation.js | 23 +++ 24 files changed, 519 insertions(+), 32 deletions(-) create mode 100644 api/tests/favorites/test_serializers.py create mode 100644 api/tests/history/test_serializers.py create mode 100644 front/src/components/mixins/Report.vue create mode 100644 front/src/components/moderation/ReportModal.vue diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index 66e10a1b4..dd28dcd07 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from funkwhale_api.activity import serializers as activity_serializers +from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer @@ -27,10 +28,17 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer): class UserTrackFavoriteSerializer(serializers.ModelSerializer): track = TrackSerializer(read_only=True) user = UserBasicSerializer(read_only=True) + actor = serializers.SerializerMethodField() class Meta: model = models.TrackFavorite - fields = ("id", "user", "track", "creation_date") + fields = ("id", "user", "track", "creation_date", "actor") + actor = serializers.SerializerMethodField() + + def get_actor(self, obj): + actor = obj.user.actor + if actor: + return federation_serializers.APIActorSerializer(actor).data class UserTrackFavoriteWriteSerializer(serializers.ModelSerializer): diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index dce285d85..7d1991bc6 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -22,7 +22,7 @@ class TrackFavoriteViewSet( filterset_class = filters.TrackFavoriteFilter serializer_class = serializers.UserTrackFavoriteSerializer - queryset = models.TrackFavorite.objects.all().select_related("user") + queryset = models.TrackFavorite.objects.all().select_related("user__actor") permission_classes = [ oauth_permissions.ScopePermission, permissions.OwnerPermission, @@ -54,7 +54,7 @@ class TrackFavoriteViewSet( ) tracks = Track.objects.with_playable_uploads( music_utils.get_actor_from_request(self.request) - ).select_related("artist", "album__artist") + ).select_related("artist", "album__artist", "attributed_to") queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks)) return queryset diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index 2254aee8c..c894ec59a 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from funkwhale_api.activity import serializers as activity_serializers +from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer @@ -27,16 +28,22 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer): class ListeningSerializer(serializers.ModelSerializer): track = TrackSerializer(read_only=True) user = UserBasicSerializer(read_only=True) + actor = serializers.SerializerMethodField() class Meta: model = models.Listening - fields = ("id", "user", "track", "creation_date") + fields = ("id", "user", "track", "creation_date", "actor") def create(self, validated_data): validated_data["user"] = self.context["user"] return super().create(validated_data) + def get_actor(self, obj): + actor = obj.user.actor + if actor: + return federation_serializers.APIActorSerializer(actor).data + class ListeningWriteSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 30219629a..6cdbc8a80 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -19,7 +19,7 @@ class ListeningViewSet( ): serializer_class = serializers.ListeningSerializer - queryset = models.Listening.objects.all().select_related("user") + queryset = models.Listening.objects.all().select_related("user__actor") permission_classes = [ oauth_permissions.ScopePermission, @@ -47,7 +47,7 @@ class ListeningViewSet( ) tracks = Track.objects.with_playable_uploads( music_utils.get_actor_from_request(self.request) - ).select_related("artist", "album__artist") + ).select_related("artist", "album__artist", "attributed_to") return queryset.prefetch_related(Prefetch("track", queryset=tracks)) def get_serializer_context(self): diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py index 178a8c1ab..ecdca9e40 100644 --- a/api/funkwhale_api/instance/nodeinfo.py +++ b/api/funkwhale_api/instance/nodeinfo.py @@ -3,6 +3,7 @@ import memoize.djangocache import funkwhale_api from funkwhale_api.common import preferences from funkwhale_api.federation import actors, models as federation_models +from funkwhale_api.moderation import models as moderation_models from funkwhale_api.music import utils as music_utils from . import stats @@ -15,6 +16,9 @@ def get(): share_stats = preferences.get("instance__nodeinfo_stats_enabled") allow_list_enabled = preferences.get("moderation__allow_list_enabled") allow_list_public = preferences.get("moderation__allow_list_public") + unauthenticated_report_types = preferences.get( + "moderation__unauthenticated_report_types" + ) if allow_list_enabled and allow_list_public: allowed_domains = list( federation_models.Domain.objects.filter(allowed=True) @@ -47,6 +51,10 @@ def get(): }, "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS, "allowList": {"enabled": allow_list_enabled, "domains": allowed_domains}, + "reportTypes": [ + {"type": t, "label": l, "anonymous": t in unauthenticated_report_types} + for t, l in moderation_models.REPORT_TYPES + ], }, } if share_stats: diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py index 5a4081b7b..c2b91760d 100644 --- a/api/funkwhale_api/moderation/models.py +++ b/api/funkwhale_api/moderation/models.py @@ -115,7 +115,7 @@ REPORT_TYPES = [ 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) + summary = models.TextField(null=True, blank=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) diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index ccdf82f4b..dc61950dd 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -2,6 +2,7 @@ from django.db import transaction from rest_framework import serializers from funkwhale_api.common import preferences +from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.music.models import Track from funkwhale_api.music.serializers import TrackSerializer from funkwhale_api.users.serializers import UserBasicSerializer @@ -79,6 +80,7 @@ class PlaylistSerializer(serializers.ModelSerializer): album_covers = serializers.SerializerMethodField(read_only=True) user = UserBasicSerializer(read_only=True) is_playable = serializers.SerializerMethodField() + actor = serializers.SerializerMethodField() class Meta: model = models.Playlist @@ -93,9 +95,15 @@ class PlaylistSerializer(serializers.ModelSerializer): "album_covers", "duration", "is_playable", + "actor", ) read_only_fields = ["id", "modification_date", "creation_date"] + def get_actor(self, obj): + actor = obj.user.actor + if actor: + return federation_serializers.APIActorSerializer(actor).data + def get_is_playable(self, obj): try: return bool(obj.playable_plts) diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 861dc8175..6f9ea23ce 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -23,7 +23,7 @@ class PlaylistViewSet( serializer_class = serializers.PlaylistSerializer queryset = ( models.Playlist.objects.all() - .select_related("user") + .select_related("user__actor") .annotate(tracks_count=Count("playlist_tracks")) .with_covers() .with_duration() diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py index 190c79184..b81006386 100644 --- a/api/tests/favorites/test_favorites.py +++ b/api/tests/favorites/test_favorites.py @@ -4,8 +4,7 @@ import pytest from django.urls import reverse from funkwhale_api.favorites.models import TrackFavorite -from funkwhale_api.music import serializers as music_serializers -from funkwhale_api.users import serializers as users_serializers +from funkwhale_api.favorites import serializers def test_user_can_add_favorite(factories): @@ -20,22 +19,15 @@ def test_user_can_add_favorite(factories): def test_user_can_get_his_favorites( api_request, factories, logged_in_api_client, client ): - r = api_request.get("/") + request = api_request.get("/") favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) factories["favorites.TrackFavorite"]() url = reverse("api:v1:favorites:tracks-list") response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk}) expected = [ - { - "user": users_serializers.UserBasicSerializer( - favorite.user, context={"request": r} - ).data, - "track": music_serializers.TrackSerializer( - favorite.track, context={"request": r} - ).data, - "id": favorite.id, - "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"), - } + serializers.UserTrackFavoriteSerializer( + favorite, context={"request": request} + ).data ] assert response.status_code == 200 assert response.data["results"] == expected diff --git a/api/tests/favorites/test_serializers.py b/api/tests/favorites/test_serializers.py new file mode 100644 index 000000000..16823caa3 --- /dev/null +++ b/api/tests/favorites/test_serializers.py @@ -0,0 +1,20 @@ +from funkwhale_api.federation import serializers as federation_serializers +from funkwhale_api.favorites import serializers +from funkwhale_api.music import serializers as music_serializers +from funkwhale_api.users import serializers as users_serializers + + +def test_track_favorite_serializer(factories, to_api_date): + favorite = factories["favorites.TrackFavorite"]() + actor = favorite.user.create_actor() + + expected = { + "id": favorite.pk, + "creation_date": to_api_date(favorite.creation_date), + "track": music_serializers.TrackSerializer(favorite.track).data, + "actor": federation_serializers.APIActorSerializer(actor).data, + "user": users_serializers.UserBasicSerializer(favorite.user).data, + } + serializer = serializers.UserTrackFavoriteSerializer(favorite) + + assert serializer.data == expected diff --git a/api/tests/history/test_serializers.py b/api/tests/history/test_serializers.py new file mode 100644 index 000000000..170b44d6b --- /dev/null +++ b/api/tests/history/test_serializers.py @@ -0,0 +1,20 @@ +from funkwhale_api.federation import serializers as federation_serializers +from funkwhale_api.history import serializers +from funkwhale_api.music import serializers as music_serializers +from funkwhale_api.users import serializers as users_serializers + + +def test_listening_serializer(factories, to_api_date): + listening = factories["history.Listening"]() + actor = listening.user.create_actor() + + expected = { + "id": listening.pk, + "creation_date": to_api_date(listening.creation_date), + "track": music_serializers.TrackSerializer(listening.track).data, + "actor": federation_serializers.APIActorSerializer(actor).data, + "user": users_serializers.UserBasicSerializer(listening.user).data, + } + serializer = serializers.ListeningSerializer(listening) + + assert serializer.data == expected diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py index 211dbaa54..cdb9ad93a 100644 --- a/api/tests/instance/test_nodeinfo.py +++ b/api/tests/instance/test_nodeinfo.py @@ -8,6 +8,12 @@ from funkwhale_api.music import utils as music_utils def test_nodeinfo_dump(preferences, mocker): preferences["instance__nodeinfo_stats_enabled"] = True + preferences["moderation__unauthenticated_report_types"] = [ + "takedown_request", + "other", + "other_category_that_doesnt_exist", + ] + stats = { "users": {"total": 1, "active_halfyear": 12, "active_month": 13}, "tracks": 2, @@ -51,6 +57,29 @@ def test_nodeinfo_dump(preferences, mocker): }, "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS, "allowList": {"enabled": False, "domains": None}, + "reportTypes": [ + { + "type": "takedown_request", + "label": "Takedown request", + "anonymous": True, + }, + { + "type": "invalid_metadata", + "label": "Invalid metadata", + "anonymous": False, + }, + { + "type": "illegal_content", + "label": "Illegal content", + "anonymous": False, + }, + { + "type": "offensive_content", + "label": "Offensive content", + "anonymous": False, + }, + {"type": "other", "label": "Other", "anonymous": True}, + ], }, } assert nodeinfo.get() == expected @@ -58,6 +87,10 @@ def test_nodeinfo_dump(preferences, mocker): def test_nodeinfo_dump_stats_disabled(preferences, mocker): preferences["instance__nodeinfo_stats_enabled"] = False + preferences["moderation__unauthenticated_report_types"] = [ + "takedown_request", + "other", + ] expected = { "version": "2.0", @@ -83,6 +116,29 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker): }, "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS, "allowList": {"enabled": False, "domains": None}, + "reportTypes": [ + { + "type": "takedown_request", + "label": "Takedown request", + "anonymous": True, + }, + { + "type": "invalid_metadata", + "label": "Invalid metadata", + "anonymous": False, + }, + { + "type": "illegal_content", + "label": "Illegal content", + "anonymous": False, + }, + { + "type": "offensive_content", + "label": "Offensive content", + "anonymous": False, + }, + {"type": "other", "label": "Other", "anonymous": True}, + ], }, } assert nodeinfo.get() == expected diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py index 250094729..f84df4bb2 100644 --- a/api/tests/playlists/test_serializers.py +++ b/api/tests/playlists/test_serializers.py @@ -1,4 +1,6 @@ +from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.playlists import models, serializers +from funkwhale_api.users import serializers as users_serializers def test_cannot_max_500_tracks_per_playlist(factories, preferences): @@ -124,3 +126,25 @@ def test_playlist_serializer_include_duration(factories, api_request): serializer = serializers.PlaylistSerializer(qs.get()) assert serializer.data["duration"] == 45 + + +def test_playlist_serializer(factories, to_api_date): + playlist = factories["playlists.Playlist"]() + actor = playlist.user.create_actor() + + expected = { + "id": playlist.pk, + "name": playlist.name, + "privacy_level": playlist.privacy_level, + "is_playable": None, + "creation_date": to_api_date(playlist.creation_date), + "modification_date": to_api_date(playlist.modification_date), + "actor": federation_serializers.APIActorSerializer(actor).data, + "user": users_serializers.UserBasicSerializer(playlist.user).data, + "duration": 0, + "tracks_count": 0, + "album_covers": [], + } + serializer = serializers.PlaylistSerializer(playlist) + + assert serializer.data == expected diff --git a/front/src/App.vue b/front/src/App.vue index e401d475e..ad64cb0b5 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -21,6 +21,7 @@ > + @@ -41,6 +42,7 @@ import moment from 'moment' import locales from './locales' import PlaylistModal from '@/components/playlists/PlaylistModal' import FilterModal from '@/components/moderation/FilterModal' +import ReportModal from '@/components/moderation/ReportModal' import ShortcutsModal from '@/components/ShortcutsModal' import SetInstanceModal from '@/components/SetInstanceModal' @@ -50,6 +52,7 @@ export default { Sidebar, AppFooter, FilterModal, + ReportModal, PlaylistModal, ShortcutsModal, GlobalEvents, diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index 8e490b4f4..cbc1da119 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -27,9 +27,17 @@ +
+