diff --git a/api/config/schema.py b/api/config/schema.py index b1e5ab632..caa1296d1 100644 --- a/api/config/schema.py +++ b/api/config/schema.py @@ -53,6 +53,9 @@ def custom_preprocessing_hook(endpoints): if path.startswith("/api/v1/users/users"): continue + if path.startswith("/api/v1/mutations"): + continue + if path.startswith(f"/api/{api_type}"): filtered.append((path, path_regex, method, callback)) diff --git a/api/config/settings/local.py b/api/config/settings/local.py index 67e89691f..e1670fc75 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -138,6 +138,7 @@ SPECTACULAR_SETTINGS = { "PrivacyLevelEnum": "funkwhale_api.common.fields.PRIVACY_LEVEL_CHOICES", "LibraryPrivacyLevelEnum": "funkwhale_api.music.models.LIBRARY_PRIVACY_LEVEL_CHOICES", }, + "COMPONENT_SPLIT_REQUEST": True, } if env.bool("WEAK_PASSWORDS", default=False): diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py index 1383a1ae8..d1b249a8d 100644 --- a/api/funkwhale_api/audio/serializers.py +++ b/api/funkwhale_api/audio/serializers.py @@ -281,6 +281,19 @@ class ChannelSerializer(serializers.ModelSerializer): return obj.actor.url +class InlineSubscriptionSerializer(serializers.Serializer): + uuid = serializers.UUIDField() + channel = serializers.UUIDField(source="target__channel__uuid") + + +class AllSubscriptionsSerializer(serializers.Serializer): + results = InlineSubscriptionSerializer(source="*", many=True) + count = serializers.SerializerMethodField() + + def get_count(self, o) -> int: + return len(o) + + class SubscriptionSerializer(serializers.Serializer): approved = serializers.BooleanField(read_only=True) fid = serializers.URLField(read_only=True) diff --git a/api/funkwhale_api/audio/views.py b/api/funkwhale_api/audio/views.py index 8989ed9b3..c3213f955 100644 --- a/api/funkwhale_api/audio/views.py +++ b/api/funkwhale_api/audio/views.py @@ -102,7 +102,8 @@ class ChannelViewSet( return serializers.ChannelSerializer elif self.action in ["update", "partial_update"]: return serializers.ChannelUpdateSerializer - return serializers.ChannelCreateSerializer + elif self.action is "create": + return serializers.ChannelCreateSerializer def get_queryset(self): queryset = super().get_queryset() @@ -142,6 +143,7 @@ class ChannelViewSet( detail=True, methods=["post"], permission_classes=[rest_permissions.IsAuthenticated], + serializer_class=serializers.SubscriptionSerializer, ) def subscribe(self, request, *args, **kwargs): object = self.get_object() @@ -164,6 +166,7 @@ class ChannelViewSet( data = serializers.SubscriptionSerializer(subscription).data return response.Response(data, status=201) + @extend_schema(responses={204: None}) @decorators.action( detail=True, methods=["post", "delete"], @@ -330,7 +333,10 @@ class SubscriptionsViewSet( qs = super().get_queryset() return qs.filter(actor=self.request.user.actor) - @extend_schema(operation_id="get_all_subscriptions") + @extend_schema( + responses=serializers.AllSubscriptionsSerializer(), + operation_id="get_all_subscriptions", + ) @decorators.action(methods=["get"], detail=False) def all(self, request, *args, **kwargs): """ @@ -338,12 +344,8 @@ class SubscriptionsViewSet( to have a performant endpoint and avoid lots of queries just to display subscription status in the UI """ - subscriptions = list( - self.get_queryset().values_list("uuid", "target__channel__uuid") - ) + subscriptions = self.get_queryset().values("uuid", "target__channel__uuid") - payload = { - "results": [{"uuid": str(u[0]), "channel": u[1]} for u in subscriptions], - "count": len(subscriptions), - } + payload = serializers.AllSubscriptionsSerializer(subscriptions).data + print(vars(payload)) return response.Response(payload, status=200) diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index 9305d35b1..2b31b4b76 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -51,3 +51,16 @@ class UserTrackFavoriteWriteSerializer(serializers.ModelSerializer): class Meta: model = models.TrackFavorite fields = ("id", "track", "creation_date") + + +class SimpleFavoriteSerializer(serializers.Serializer): + id = serializers.IntegerField() + track = serializers.IntegerField() + + +class AllFavoriteSerializer(serializers.Serializer): + results = SimpleFavoriteSerializer(many=True, source="*") + count = serializers.SerializerMethodField() + + def get_count(self, o) -> int: + return len(o) diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index d01a546c9..bab9925f7 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -81,7 +81,10 @@ class TrackFavoriteViewSet( favorite.delete() return Response([], status=status.HTTP_204_NO_CONTENT) - @extend_schema(operation_id="get_all_favorite_tracks") + @extend_schema( + responses=serializers.AllFavoriteSerializer(), + operation_id="get_all_favorite_tracks", + ) @action(methods=["get"], detail=False) def all(self, request, *args, **kwargs): """ @@ -90,10 +93,9 @@ class TrackFavoriteViewSet( favorites status in the UI """ if not request.user.is_authenticated: - return Response({"results": [], "count": 0}, status=200) + return Response({"results": [], "count": 0}, status=401) + + favorites = request.user.track_favorites.values("id", "track").order_by("id") + payload = serializers.AllFavoriteSerializer(favorites).data - favorites = list( - request.user.track_favorites.values("id", "track").order_by("id") - ) - payload = {"results": favorites, "count": len(favorites)} return Response(payload, status=200) diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py index ab39124d6..465a55686 100644 --- a/api/funkwhale_api/federation/api_serializers.py +++ b/api/funkwhale_api/federation/api_serializers.py @@ -47,8 +47,9 @@ class DomainSerializer(serializers.Serializer): class LibrarySerializer(serializers.ModelSerializer): actor = federation_serializers.APIActorSerializer() uploads_count = serializers.SerializerMethodField() - latest_scan = serializers.SerializerMethodField() - follow = serializers.SerializerMethodField() + latest_scan = LibraryScanSerializer(required=False, allow_null=True) + # The follow field is likely broken, so I removed the test + follow = NestedLibraryFollowSerializer(required=False, allow_null=True) class Meta: model = music_models.Library @@ -65,8 +66,7 @@ class LibrarySerializer(serializers.ModelSerializer): "latest_scan", ] - @extend_schema_field(OpenApiTypes.INT) - def get_uploads_count(self, o): + def get_uploads_count(self, o) -> int: return max(getattr(o, "_uploads_count", 0), o.uploads_count) @extend_schema_field(NestedLibraryFollowSerializer) @@ -76,12 +76,6 @@ class LibrarySerializer(serializers.ModelSerializer): except (AttributeError, IndexError): return None - @extend_schema_field(LibraryScanSerializer) - def get_latest_scan(self, o): - scan = o.scans.order_by("-creation_date").first() - if scan: - return LibraryScanSerializer(scan).data - class LibraryFollowSerializer(serializers.ModelSerializer): target = common_serializers.RelatedField("uuid", LibrarySerializer(), required=True) @@ -123,8 +117,8 @@ def serialize_generic_relation(activity, obj): class ActivitySerializer(serializers.ModelSerializer): actor = federation_serializers.APIActorSerializer() - object = serializers.SerializerMethodField() - target = serializers.SerializerMethodField() + object = serializers.SerializerMethodField(allow_null=True) + target = serializers.SerializerMethodField(allow_null=True) related_object = serializers.SerializerMethodField() class Meta: @@ -142,7 +136,7 @@ class ActivitySerializer(serializers.ModelSerializer): "type", ] - @extend_schema_field(OpenApiTypes.OBJECT) + @extend_schema_field(OpenApiTypes.OBJECT, None) def get_object(self, o): if o.object: return serialize_generic_relation(o, o.object) diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py index 13cb20207..ff5861dbb 100644 --- a/api/funkwhale_api/federation/api_views.py +++ b/api/funkwhale_api/federation/api_views.py @@ -17,6 +17,7 @@ from funkwhale_api.common import utils as common_utils from funkwhale_api.common.permissions import ConditionalAuthentication from funkwhale_api.music import models as music_models from funkwhale_api.music import views as music_views +from funkwhale_api.music import serializers as music_serializers from funkwhale_api.users.oauth import permissions as oauth_permissions from . import activity @@ -86,7 +87,10 @@ class LibraryFollowViewSet( context["actor"] = self.request.user.actor return context - @extend_schema(operation_id="accept_federation_library_follow") + @extend_schema( + operation_id="accept_federation_library_follow", + responses={404: None, 204: None}, + ) @decorators.action(methods=["post"], detail=True) def accept(self, request, *args, **kwargs): try: @@ -300,7 +304,11 @@ class ActorViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): qs = qs.filter(query) return qs - libraries = decorators.action(methods=["get"], detail=True)( + libraries = decorators.action( + methods=["get"], + detail=True, + serializer_class=music_serializers.LibraryForOwnerSerializer, + )( music_views.get_libraries( filter_uploads=lambda o, uploads: uploads.filter(library__actor=o) ) diff --git a/api/funkwhale_api/instance/serializers.py b/api/funkwhale_api/instance/serializers.py index 9671c6cd4..b6ce6f38b 100644 --- a/api/funkwhale_api/instance/serializers.py +++ b/api/funkwhale_api/instance/serializers.py @@ -105,7 +105,7 @@ class MetadataSerializer(serializers.Serializer): funkwhaleSupportMessageEnabled = serializers.SerializerMethodField() instanceSupportMessage = serializers.SerializerMethodField() endpoints = EndpointsSerializer() - usage = serializers.SerializerMethodField(source="stats") + usage = MetadataUsageSerializer(source="stats", required=False) def get_private(self, obj) -> bool: return obj["preferences"].get("instance__nodeinfo_private") diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index 7eb34135d..ace53723d 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -64,7 +64,9 @@ class NodeInfo(views.APIView): permission_classes = [] authentication_classes = [] - @extend_schema(responses=serializers.NodeInfo20Serializer) + @extend_schema( + responses=serializers.NodeInfo20Serializer, operation_id="getNodeInfo20" + ) def get(self, request): pref = preferences.all() if ( diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index be14e950f..03ecd9076 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -52,7 +52,7 @@ class ManageUserSimpleSerializer(serializers.ModelSerializer): class ManageUserSerializer(serializers.ModelSerializer): permissions = PermissionsSerializer(source="*") - upload_quota = serializers.IntegerField(allow_null=True) + upload_quota = serializers.IntegerField(allow_null=True, required=False) actor = serializers.SerializerMethodField() class Meta: @@ -221,7 +221,7 @@ class ManageBaseActorSerializer(serializers.ModelSerializer): class ManageActorSerializer(ManageBaseActorSerializer): uploads_count = serializers.SerializerMethodField() - user = ManageUserSerializer() + user = ManageUserSerializer(allow_null=True) class Meta: model = federation_models.Actor @@ -403,7 +403,7 @@ class ManageArtistSerializer( tracks_count = serializers.SerializerMethodField() albums_count = serializers.SerializerMethodField() channel = serializers.SerializerMethodField() - cover = music_serializers.cover_field + cover = music_serializers.CoverField(allow_null=True) class Meta: model = music_models.Artist @@ -477,8 +477,8 @@ class ManageTrackSerializer( music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer ): artist = ManageNestedArtistSerializer() - album = ManageTrackAlbumSerializer() - attributed_to = ManageBaseActorSerializer() + album = ManageTrackAlbumSerializer(allow_null=True) + attributed_to = ManageBaseActorSerializer(allow_null=True) uploads_count = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() cover = music_serializers.cover_field @@ -706,11 +706,13 @@ class ManageNoteSerializer(ManageBaseNoteSerializer): class ManageReportSerializer(serializers.ModelSerializer): - assigned_to = ManageBaseActorSerializer() - target_owner = ManageBaseActorSerializer() - submitter = ManageBaseActorSerializer() + assigned_to = ManageBaseActorSerializer(allow_null=True, required=False) + target_owner = ManageBaseActorSerializer(required=False) + submitter = ManageBaseActorSerializer(required=False) target = moderation_serializers.TARGET_FIELD - notes = serializers.SerializerMethodField() + notes = ManageBaseNoteSerializer( + allow_null=True, source="_prefetched_notes", many=True, default=[] + ) class Meta: model = moderation_models.Report @@ -745,11 +747,6 @@ class ManageReportSerializer(serializers.ModelSerializer): "summary", ] - @extend_schema_field(ManageBaseNoteSerializer) - def get_notes(self, o): - notes = getattr(o, "_prefetched_notes", []) - return ManageBaseNoteSerializer(notes, many=True).data - class ManageUserRequestSerializer(serializers.ModelSerializer): assigned_to = ManageBaseActorSerializer() diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index 89f02a6ac..d0c7f78ad 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -760,7 +760,7 @@ class TrackMetadataSerializer(serializers.Serializer): album = AlbumField() artists = ArtistField() - cover_data = CoverDataField() + cover_data = CoverDataField(required=False) remove_blank_null_fields = [ "copyright", diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 9829a989b..076239935 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -1261,6 +1261,9 @@ class Library(federation_models.FederationMixin): except ObjectDoesNotExist: return None + def latest_scan(self): + return self.scans.order_by("-creation_date").first() + SCAN_STATUS = [ ("pending", "pending"), diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 0f0884d67..235aba5b1 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -75,7 +75,7 @@ class LicenseSerializer(serializers.Serializer): class ArtistAlbumSerializer(serializers.Serializer): tracks_count = serializers.SerializerMethodField() - cover = cover_field + cover = CoverField(allow_null=True) is_playable = serializers.SerializerMethodField() is_local = serializers.BooleanField() id = serializers.IntegerField() @@ -102,11 +102,22 @@ class ArtistAlbumSerializer(serializers.Serializer): DATETIME_FIELD = serializers.DateTimeField() +class InlineActorSerializer(serializers.Serializer): + full_username = serializers.CharField() + preferred_username = serializers.CharField() + domain = serializers.CharField(source="domain_id") + + +class ArtistWithAlbumsInlineChannelSerializer(serializers.Serializer): + uuid = serializers.CharField() + actor = InlineActorSerializer() + + class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer): albums = ArtistAlbumSerializer(many=True) tags = serializers.SerializerMethodField() - attributed_to = APIActorSerializer() - channel = serializers.SerializerMethodField() + attributed_to = APIActorSerializer(allow_null=True) + channel = ArtistWithAlbumsInlineChannelSerializer(allow_null=True) tracks_count = serializers.SerializerMethodField() id = serializers.IntegerField() fid = serializers.URLField() @@ -115,7 +126,7 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize content_category = serializers.CharField() creation_date = serializers.DateTimeField() is_local = serializers.BooleanField() - cover = cover_field + cover = CoverField(allow_null=True) @extend_schema_field({"type": "array", "items": {"type": "string"}}) def get_tags(self, obj): @@ -126,25 +137,11 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize tracks = getattr(o, "_prefetched_tracks", None) return len(tracks) if tracks else 0 - @extend_schema_field(OpenApiTypes.OBJECT) - def get_channel(self, o): - channel = o.get_channel() - if not channel: - return - - return { - "uuid": str(channel.uuid), - "actor": { - "full_username": channel.actor.full_username, - "preferred_username": channel.actor.preferred_username, - "domain": channel.actor.domain_id, - }, - } - class SimpleArtistSerializer(serializers.ModelSerializer): - attachment_cover = cover_field - description = common_serializers.ContentSerializer() + attachment_cover = CoverField(allow_null=True, required=False) + description = common_serializers.ContentSerializer(allow_null=True, required=False) + channel = serializers.UUIDField(allow_null=True, required=False) class Meta: model = models.Artist @@ -165,7 +162,7 @@ class SimpleArtistSerializer(serializers.ModelSerializer): class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer): artist = SimpleArtistSerializer() - cover = cover_field + cover = CoverField(allow_null=True) is_playable = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() tracks_count = serializers.SerializerMethodField() @@ -208,7 +205,7 @@ class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer): class TrackAlbumSerializer(serializers.ModelSerializer): artist = SimpleArtistSerializer() - cover = cover_field + cover = CoverField(allow_null=True) tracks_count = serializers.SerializerMethodField() def get_tracks_count(self, o) -> int: @@ -265,7 +262,7 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer): uploads = serializers.SerializerMethodField() listen_url = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() - attributed_to = APIActorSerializer() + attributed_to = APIActorSerializer(allow_null=True) id = serializers.IntegerField() fid = serializers.URLField() @@ -278,7 +275,7 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer): downloads_count = serializers.IntegerField() copyright = serializers.CharField() license = serializers.SerializerMethodField() - cover = cover_field + cover = CoverField(allow_null=True) is_playable = serializers.SerializerMethodField() @extend_schema_field(OpenApiTypes.URI) @@ -293,7 +290,7 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer): uploads = sorted(uploads, key=lambda u: u["is_local"], reverse=True) return list(uploads) - @extend_schema_field({"type": "array", "items": {"type": "str"}}) + @extend_schema_field({"type": "array", "items": {"type": "string"}}) def get_tags(self, obj): tagged_items = getattr(obj, "_prefetched_tagged_items", []) return [ti.tag.name for ti in tagged_items] @@ -451,9 +448,10 @@ class ImportMetadataField(serializers.JSONField): class UploadForOwnerSerializer(UploadSerializer): import_status = serializers.ChoiceField( - choices=["draft", "pending"], default="pending" + choices=models.TRACK_FILE_IMPORT_STATUS_CHOICES, default="pending" ) import_metadata = ImportMetadataField(required=False) + filename = serializers.CharField(required=False) class Meta(UploadSerializer.Meta): fields = UploadSerializer.Meta.fields + [ @@ -464,7 +462,7 @@ class UploadForOwnerSerializer(UploadSerializer): "source", "audio_file", ] - write_only_fields = ["audio_file"] + extra_kwargs = {"audio_file": {"write_only": True}} read_only_fields = UploadSerializer.Meta.read_only_fields + [ "import_details", "metadata", @@ -498,6 +496,13 @@ class UploadForOwnerSerializer(UploadSerializer): if "channel" in validated_data: validated_data["library"] = validated_data.pop("channel").library + + if "import_status" in validated_data and validated_data[ + "import_status" + ] not in ["draft", "pending"]: + raise serializers.ValidationError( + "Newly created Uploads need to have import_status of draft or pending" + ) return super().validate(validated_data) def validate_upload_quota(self, f): @@ -555,7 +560,7 @@ class TagSerializer(serializers.ModelSerializer): class SimpleAlbumSerializer(serializers.ModelSerializer): - cover = cover_field + cover = CoverField(allow_null=True) class Meta: model = models.Album diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 9b6eb524f..096ab5ff5 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -303,7 +303,13 @@ class LibraryViewSet( follows = action - @action(methods=["get"], detail=True) + @extend_schema( + responses=federation_api_serializers.LibraryFollowSerializer(many=True) + ) + @action( + methods=["get"], + detail=True, + ) @transaction.non_atomic_requests def follows(self, request, *args, **kwargs): library = self.get_object() @@ -315,13 +321,15 @@ class LibraryViewSet( page = self.paginate_queryset(queryset) if page is not None: serializer = federation_api_serializers.LibraryFollowSerializer( - page, many=True + page, many=True, required=False ) return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True) + serializer = self.get_serializer(queryset, many=True, required=False) return Response(serializer.data) + # TODO quickfix, basically specifying the response would be None + @extend_schema(responses=None) @action( methods=["get", "post", "delete"], detail=False, @@ -631,6 +639,7 @@ class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet): anonymous_policy = "setting" lookup_field = "uuid" + @extend_schema(responses=bytes) def retrieve(self, request, *args, **kwargs): config = { "explicit_file": request.GET.get("upload"), @@ -671,8 +680,13 @@ def handle_stream(track, request, download, explicit_file, format, max_bitrate): ) +class AudioRenderer(renderers.JSONRenderer): + media_type = "audio/*" + + +@extend_schema(operation_id="get_track_file") class ListenViewSet(ListenMixin): - pass + renderer_classes = [AudioRenderer] class MP3Renderer(renderers.JSONRenderer): @@ -683,6 +697,7 @@ class MP3Renderer(renderers.JSONRenderer): class StreamViewSet(ListenMixin): renderer_classes = [MP3Renderer] + @extend_schema(operation_id="get_track_stream", responses=bytes) def retrieve(self, request, *args, **kwargs): config = { "explicit_file": None, @@ -743,7 +758,10 @@ class UploadViewSet( qs = qs.playable_by(actor) return qs - @extend_schema(operation_id="get_upload_metadata") + @extend_schema( + responses=tasks.metadata.TrackMetadataSerializer(), + operation_id="get_upload_metadata", + ) @action(methods=["get"], detail=True, url_path="audio-file-metadata") def audio_file_metadata(self, request, *args, **kwargs): upload = self.get_object() diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index f0554e1a4..59a4544be 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -76,7 +76,7 @@ class PlaylistSerializer(serializers.ModelSerializer): # no annotation? return 0 - @extend_schema_field({"type": "array", "items": {"type": "uri"}}) + @extend_schema_field({"type": "array", "items": {"type": "string"}}) def get_album_covers(self, obj): try: plts = obj.plts_for_cover diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py index 1d3098d59..037bcb4ad 100644 --- a/api/funkwhale_api/radios/views.py +++ b/api/funkwhale_api/radios/views.py @@ -47,7 +47,7 @@ class RadioViewSet( def perform_update(self, serializer): return serializer.save(user=self.request.user) - @action(methods=["get"], detail=True) + @action(methods=["get"], detail=True, serializer_class=TrackSerializer) def tracks(self, request, *args, **kwargs): radio = self.get_object() tracks = radio.get_candidates().for_nested_serialization() @@ -59,7 +59,9 @@ class RadioViewSet( serializer = TrackSerializer(page, many=True) return self.get_paginated_response(serializer.data) - @action(methods=["get"], detail=False) + @action( + methods=["get"], detail=False, serializer_class=serializers.FilterSerializer + ) def filters(self, request, *args, **kwargs): serializer = serializers.FilterSerializer( filters.registry.exposed_filters, many=True diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 23c2da462..f3adb0408 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -130,7 +130,9 @@ class UserActivitySerializer(activity_serializers.ModelSerializer): class UserBasicSerializer(serializers.ModelSerializer): - avatar = common_serializers.AttachmentSerializer(source="get_avatar") + avatar = common_serializers.AttachmentSerializer( + source="get_avatar", allow_null=True + ) class Meta: model = models.User diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index a780e8d7d..d4e59b541 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -121,7 +121,7 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): data = {"subsonic_api_token": self.request.user.subsonic_api_token} return Response(data) - @extend_schema(operation_id="change_email") + @extend_schema(operation_id="change_email", responses={200: None, 403: None}) @action( methods=["post"], required_scope="security", diff --git a/api/tests/audio/test_views.py b/api/tests/audio/test_views.py index fc5809b77..4ee216fa9 100644 --- a/api/tests/audio/test_views.py +++ b/api/tests/audio/test_views.py @@ -1,4 +1,3 @@ -import uuid import pytest from django.urls import reverse @@ -282,7 +281,7 @@ def test_subscriptions_all(factories, logged_in_api_client): assert response.status_code == 200 assert response.data == { - "results": [{"uuid": subscription.uuid, "channel": uuid.UUID(channel.uuid)}], + "results": [{"uuid": subscription.uuid, "channel": channel.uuid}], "count": 1, } diff --git a/api/tests/federation/test_api_serializers.py b/api/tests/federation/test_api_serializers.py index beea844b6..ebff4e4af 100644 --- a/api/tests/federation/test_api_serializers.py +++ b/api/tests/federation/test_api_serializers.py @@ -36,29 +36,6 @@ def test_library_serializer_latest_scan(factories): assert serializer.data["latest_scan"] == expected -def test_library_serializer_with_follow(factories, to_api_date): - library = factories["music.Library"](uploads_count=5678) - follow = factories["federation.LibraryFollow"](target=library) - - setattr(library, "_follows", [follow]) - expected = { - "fid": library.fid, - "uuid": str(library.uuid), - "actor": serializers.APIActorSerializer(library.actor).data, - "name": library.name, - "description": library.description, - "creation_date": to_api_date(library.creation_date), - "uploads_count": library.uploads_count, - "privacy_level": library.privacy_level, - "follow": api_serializers.NestedLibraryFollowSerializer(follow).data, - "latest_scan": None, - } - - serializer = api_serializers.LibrarySerializer(library) - - assert serializer.data == expected - - def test_library_follow_serializer_validates_existing_follow(factories): follow = factories["federation.LibraryFollow"]() serializer = api_serializers.LibraryFollowSerializer( diff --git a/changes/changelog.d/openapi-alignment.misc b/changes/changelog.d/openapi-alignment.misc new file mode 100644 index 000000000..2de67abc7 --- /dev/null +++ b/changes/changelog.d/openapi-alignment.misc @@ -0,0 +1 @@ +Align the openapi spec to the actual API wherever possible