diff --git a/api/config/schema.py b/api/config/schema.py index a78a96a89..556536909 100644 --- a/api/config/schema.py +++ b/api/config/schema.py @@ -1,4 +1,5 @@ from drf_spectacular.contrib.django_oauth_toolkit import OpenApiAuthenticationExtension +from drf_spectacular.plumbing import build_bearer_security_scheme_object import os @@ -28,11 +29,24 @@ class CustomOAuthExt(OpenApiAuthenticationExtension): return {"type": "oauth2", "flows": flows} +class CustomApplicationTokenExt(OpenApiAuthenticationExtension): + target_class = "funkwhale_api.common.authentication.ApplicationTokenAuthentication" + name = "ApplicationToken" + + def get_security_definition(self, auto_schema): + return build_bearer_security_scheme_object( + header_name="Authorization", + token_prefix="Bearer", + ) + + def custom_preprocessing_hook(endpoints): filtered = [] # your modifications to the list of operations that are exposed in the schema - api_type = os.environ["API_TYPE"] + api_type = os.environ.get("API_TYPE", "v1") for (path, path_regex, method, callback) in endpoints: + if path.startswith("/api/v1/providers"): + continue if path.startswith(f"/api/{api_type}"): filtered.append((path, path_regex, method, callback)) return filtered diff --git a/api/config/settings/local.py b/api/config/settings/local.py index de4002782..37eac1903 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -132,6 +132,12 @@ SPECTACULAR_SETTINGS = { "OAUTH2_AUTHORIZATION_URL": "/authorize", "OAUTH2_TOKEN_URL": "/api/v1/oauth/token/", "PREPROCESSING_HOOKS": ["config.schema.custom_preprocessing_hook"], + "ENUM_NAME_OVERRIDES": { + "FederationChoiceEnum": "funkwhale_api.federation.models.TYPE_CHOICES", + "ReportTypeEnum": "funkwhale_api.moderation.models.REPORT_TYPES", + "PrivacyLevelEnum": "funkwhale_api.common.fields.PRIVACY_LEVEL_CHOICES", + "LibraryPrivacyLevelEnum": "funkwhale_api.music.models.LIBRARY_PRIVACY_LEVEL_CHOICES", + }, } if env.bool("WEAK_PASSWORDS", default=False): diff --git a/api/funkwhale_api/audio/models.py b/api/funkwhale_api/audio/models.py index 2955bc3fa..9e1f7453d 100644 --- a/api/funkwhale_api/audio/models.py +++ b/api/funkwhale_api/audio/models.py @@ -81,7 +81,7 @@ class Channel(models.Model): return self.actor.fid @property - def is_local(self): + def is_local(self) -> bool: return self.actor.is_local @property diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py index 50e4fc01f..1383a1ae8 100644 --- a/api/funkwhale_api/audio/serializers.py +++ b/api/funkwhale_api/audio/serializers.py @@ -28,11 +28,14 @@ from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.federation import utils as federation_utils from funkwhale_api.moderation import mrf from funkwhale_api.music import models as music_models -from funkwhale_api.music import serializers as music_serializers +from funkwhale_api.music.serializers import SimpleArtistSerializer, COVER_WRITE_FIELD from funkwhale_api.tags import models as tags_models from funkwhale_api.tags import serializers as tags_serializers from funkwhale_api.users import serializers as users_serializers +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import categories from . import models @@ -84,7 +87,7 @@ class ChannelCreateSerializer(serializers.Serializer): choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES ) metadata = serializers.DictField(required=False) - cover = music_serializers.COVER_WRITE_FIELD + cover = COVER_WRITE_FIELD def validate(self, validated_data): existing_channels = self.context["actor"].owned_channels.count() @@ -163,7 +166,7 @@ class ChannelUpdateSerializer(serializers.Serializer): choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES ) metadata = serializers.DictField(required=False) - cover = music_serializers.COVER_WRITE_FIELD + cover = COVER_WRITE_FIELD def validate(self, validated_data): validated_data = super().validate(validated_data) @@ -234,7 +237,7 @@ class ChannelUpdateSerializer(serializers.Serializer): class ChannelSerializer(serializers.ModelSerializer): - artist = serializers.SerializerMethodField() + artist = SimpleArtistSerializer() actor = serializers.SerializerMethodField() downloads_count = serializers.SerializerMethodField() attributed_to = federation_serializers.APIActorSerializer() @@ -255,26 +258,25 @@ class ChannelSerializer(serializers.ModelSerializer): "downloads_count", ] - def get_artist(self, obj): - return music_serializers.serialize_artist_simple(obj.artist) - def to_representation(self, obj): data = super().to_representation(obj) if self.context.get("subscriptions_count"): data["subscriptions_count"] = self.get_subscriptions_count(obj) return data - def get_subscriptions_count(self, obj): + def get_subscriptions_count(self, obj) -> int: return obj.actor.received_follows.exclude(approved=False).count() - def get_downloads_count(self, obj): + def get_downloads_count(self, obj) -> int: return getattr(obj, "_downloads_count", None) or 0 + @extend_schema_field(federation_serializers.APIActorSerializer) def get_actor(self, obj): if obj.attributed_to == actors.get_service_actor(): return None return federation_serializers.APIActorSerializer(obj.actor).data + @extend_schema_field(OpenApiTypes.URI) def get_url(self, obj): return obj.actor.url diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index 7735c0f72..7dcafa084 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -10,6 +10,9 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import models from . import utils @@ -270,6 +273,7 @@ class APIMutationSerializer(serializers.ModelSerializer): "previous_state", ] + @extend_schema_field(OpenApiTypes.OBJECT) def get_target(self, obj): target = obj.target if not target: @@ -292,6 +296,7 @@ class AttachmentSerializer(serializers.Serializer): file = StripExifImageField(write_only=True) urls = serializers.SerializerMethodField() + @extend_schema_field(OpenApiTypes.OBJECT) def get_urls(self, o): urls = {} urls["source"] = o.url @@ -315,7 +320,7 @@ class ContentSerializer(serializers.Serializer): ) html = serializers.SerializerMethodField() - def get_html(self, o): + def get_html(self, o) -> str: return utils.render_html(o.text, o.content_type) diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index dd28dcd07..9305d35b1 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -2,9 +2,14 @@ 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.music.serializers import ( + TrackActivitySerializer, + TrackSerializer, +) from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer +from drf_spectacular.utils import extend_schema_field + from . import models @@ -35,6 +40,7 @@ class UserTrackFavoriteSerializer(serializers.ModelSerializer): fields = ("id", "user", "track", "creation_date", "actor") actor = serializers.SerializerMethodField() + @extend_schema_field(federation_serializers.APIActorSerializer) def get_actor(self, obj): actor = obj.user.actor if actor: diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py index 6ff1e1b2b..ab39124d6 100644 --- a/api/funkwhale_api/federation/api_serializers.py +++ b/api/funkwhale_api/federation/api_serializers.py @@ -13,6 +13,9 @@ from funkwhale_api.common import serializers as common_serializers from funkwhale_api.music import models as music_models from funkwhale_api.users import serializers as users_serializers +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import filters from . import models from . import serializers as federation_serializers @@ -62,15 +65,18 @@ class LibrarySerializer(serializers.ModelSerializer): "latest_scan", ] + @extend_schema_field(OpenApiTypes.INT) def get_uploads_count(self, o): return max(getattr(o, "_uploads_count", 0), o.uploads_count) + @extend_schema_field(NestedLibraryFollowSerializer) def get_follow(self, o): try: return NestedLibraryFollowSerializer(o._follows[0]).data 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: @@ -95,6 +101,7 @@ class LibraryFollowSerializer(serializers.ModelSerializer): raise serializers.ValidationError("You are already following this library") return v + @extend_schema_field(federation_serializers.APIActorSerializer) def get_actor(self, o): return federation_serializers.APIActorSerializer(o.actor).data @@ -135,14 +142,17 @@ class ActivitySerializer(serializers.ModelSerializer): "type", ] + @extend_schema_field(OpenApiTypes.OBJECT) def get_object(self, o): if o.object: return serialize_generic_relation(o, o.object) + @extend_schema_field(OpenApiTypes.OBJECT) def get_related_object(self, o): if o.related_object: return serialize_generic_relation(o, o.related_object) + @extend_schema_field(OpenApiTypes.OBJECT) def get_target(self, o): if o.target: return serialize_generic_relation(o, o.target) @@ -268,6 +278,7 @@ class FullActorSerializer(serializers.Serializer): summary = common_serializers.ContentSerializer(source="summary_obj") icon = common_serializers.AttachmentSerializer(source="attachment_icon") + @extend_schema_field(OpenApiTypes.BOOL) def get_is_channel(self, o): try: return bool(o.channel) diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index d9ea20180..125b61353 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -48,7 +48,7 @@ class FederationMixin(models.Model): abstract = True @property - def is_local(self): + def is_local(self) -> bool: return federation_utils.is_local(self.fid) @property @@ -172,7 +172,7 @@ class Domain(models.Model): return data @property - def is_local(self): + def is_local(self) -> bool: return self.name == settings.FEDERATION_HOSTNAME @@ -232,14 +232,14 @@ class Actor(models.Model): return "{}#main-key".format(self.fid) @property - def full_username(self): + def full_username(self) -> str: return "{}@{}".format(self.preferred_username, self.domain_id) def __str__(self): return "{}@{}".format(self.preferred_username, self.domain_id) @property - def is_local(self): + def is_local(self) -> bool: return self.domain_id == settings.FEDERATION_HOSTNAME def get_approved_followers(self): diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py index 9dce78246..766765dee 100644 --- a/api/funkwhale_api/federation/utils.py +++ b/api/funkwhale_api/federation/utils.py @@ -139,7 +139,7 @@ def local_qs(queryset, url_field="fid", include=True): return queryset.filter(query) -def is_local(url): +def is_local(url) -> bool: if not url: return True diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index c894ec59a..b275ff806 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -5,6 +5,8 @@ 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 +from drf_spectacular.utils import extend_schema_field + from . import models @@ -39,6 +41,7 @@ class ListeningSerializer(serializers.ModelSerializer): return super().create(validated_data) + @extend_schema_field(federation_serializers.APIActorSerializer) def get_actor(self, obj): actor = obj.user.actor if actor: diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index de7d7d63b..be14e950f 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -18,6 +18,9 @@ from funkwhale_api.music import serializers as music_serializers from funkwhale_api.tags import models as tags_models from funkwhale_api.users import models as users_models +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import filters @@ -90,6 +93,7 @@ class ManageUserSerializer(serializers.ModelSerializer): ) return instance + @extend_schema_field(OpenApiTypes.OBJECT) def get_actor(self, obj): if obj.actor: return ManageBaseActorSerializer(obj.actor).data @@ -151,10 +155,10 @@ class ManageDomainSerializer(serializers.ModelSerializer): "nodeinfo_fetch_date", ] - def get_actors_count(self, o): + def get_actors_count(self, o) -> int: return getattr(o, "actors_count", 0) - def get_outbox_activities_count(self, o): + def get_outbox_activities_count(self, o) -> int: return getattr(o, "outbox_activities_count", 0) @@ -211,7 +215,7 @@ class ManageBaseActorSerializer(serializers.ModelSerializer): ] read_only_fields = ["creation_date", "instance_policy"] - def get_is_local(self, o): + def get_is_local(self, o) -> bool: return o.domain_id == settings.FEDERATION_HOSTNAME @@ -228,7 +232,7 @@ class ManageActorSerializer(ManageBaseActorSerializer): ] read_only_fields = ["creation_date", "instance_policy"] - def get_uploads_count(self, o): + def get_uploads_count(self, o) -> int: return getattr(o, "uploads_count", 0) @@ -242,7 +246,7 @@ class ManageActorActionSerializer(common_serializers.ActionSerializer): common_utils.on_commit(federation_tasks.purge_actors.delay, ids=list(ids)) -class TargetSerializer(serializers.Serializer): +class ManageTargetSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=["domain", "actor"]) id = serializers.CharField() @@ -264,7 +268,7 @@ class TargetSerializer(serializers.Serializer): class ManageInstancePolicySerializer(serializers.ModelSerializer): - target = TargetSerializer() + target = ManageTargetSerializer() actor = federation_fields.ActorRelatedField(read_only=True) class Meta: @@ -353,6 +357,7 @@ class ManageBaseAlbumSerializer(serializers.ModelSerializer): "tracks_count", ] + @extend_schema_field(OpenApiTypes.INT) def get_tracks_count(self, o): return getattr(o, "_tracks_count", None) @@ -385,6 +390,7 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer): model = music_models.Album fields = ManageBaseAlbumSerializer.Meta.fields + ["tracks_count"] + @extend_schema_field(OpenApiTypes.INT) def get_tracks_count(self, obj): return getattr(obj, "tracks_count", None) @@ -411,16 +417,20 @@ class ManageArtistSerializer( "content_category", ] + @extend_schema_field(OpenApiTypes.INT) def get_tracks_count(self, obj): return getattr(obj, "_tracks_count", None) + @extend_schema_field(OpenApiTypes.INT) def get_albums_count(self, obj): return getattr(obj, "_albums_count", None) + @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] + @extend_schema_field(OpenApiTypes.STR) def get_channel(self, obj): if "channel" in obj._state.fields_cache and obj.get_channel(): return str(obj.channel.uuid) @@ -446,9 +456,10 @@ class ManageAlbumSerializer( "tracks_count", ] - def get_tracks_count(self, o): + def get_tracks_count(self, o) -> int: return len(o.tracks.all()) + @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] @@ -483,9 +494,11 @@ class ManageTrackSerializer( "cover", ] + @extend_schema_field(OpenApiTypes.INT) def get_uploads_count(self, obj): return getattr(obj, "uploads_count", None) + @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] @@ -570,9 +583,10 @@ class ManageLibrarySerializer(serializers.ModelSerializer): "creation_date", ] - def get_uploads_count(self, obj): - return getattr(obj, "_uploads_count", obj.uploads_count) + def get_uploads_count(self, obj) -> int: + return getattr(obj, "_uploads_count", int(obj.uploads_count)) + @extend_schema_field(OpenApiTypes.INT) def get_followers_count(self, obj): return getattr(obj, "followers_count", None) @@ -652,12 +666,15 @@ class ManageTagSerializer(ManageBaseAlbumSerializer): "artists_count", ] + @extend_schema_field(OpenApiTypes.INT) def get_tracks_count(self, obj): return getattr(obj, "_tracks_count", None) + @extend_schema_field(OpenApiTypes.INT) def get_albums_count(self, obj): return getattr(obj, "_albums_count", None) + @extend_schema_field(OpenApiTypes.INT) def get_artists_count(self, obj): return getattr(obj, "_artists_count", None) @@ -728,6 +745,7 @@ 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 @@ -761,6 +779,7 @@ class ManageUserRequestSerializer(serializers.ModelSerializer): "metadata", ] + @extend_schema_field(ManageBaseNoteSerializer) def get_notes(self, o): notes = getattr(o, "_prefetched_notes", []) return ManageBaseNoteSerializer(notes, many=True).data diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py index b062744f4..55daca3a4 100644 --- a/api/funkwhale_api/moderation/serializers.py +++ b/api/funkwhale_api/moderation/serializers.py @@ -24,7 +24,7 @@ class FilteredArtistSerializer(serializers.ModelSerializer): fields = ["id", "name"] -class TargetSerializer(serializers.Serializer): +class ModerationTargetSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=["artist"]) id = serializers.CharField() @@ -44,7 +44,7 @@ class TargetSerializer(serializers.Serializer): class UserFilterSerializer(serializers.ModelSerializer): - target = TargetSerializer() + target = ModerationTargetSerializer() class Meta: model = models.UserFilter diff --git a/api/funkwhale_api/music/migrations/0056_alter_artist_content_category.py b/api/funkwhale_api/music/migrations/0056_alter_artist_content_category.py new file mode 100644 index 000000000..5b80f1f71 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0056_alter_artist_content_category.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-07-19 13:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0055_auto_20220627_1915'), + ] + + operations = [ + migrations.AlterField( + model_name='artist', + name='content_category', + field=models.CharField(choices=[('music', 'music'), ('podcast', 'podcast'), ('other', 'other')], db_index=True, default='music', max_length=30), + ), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 82464e40a..9829a989b 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -137,7 +137,7 @@ class APIModelMixin(models.Model): return super().save(**kwargs) @property - def is_local(self): + def is_local(self) -> bool: return federation_utils.is_local(self.fid) @property @@ -251,7 +251,7 @@ class Artist(APIModelMixin): db_index=True, default="music", choices=ARTIST_CONTENT_CATEGORY_CHOICES, - null=True, + null=False, ) modification_date = models.DateTimeField(default=timezone.now, db_index=True) api = musicbrainz.api.artists @@ -652,7 +652,7 @@ class Track(APIModelMixin): ) @property - def listen_url(self): + def listen_url(self) -> str: # Not using reverse because this is slow return "/api/v1/listen/{}/".format(self.uuid) @@ -782,7 +782,7 @@ class Upload(models.Model): objects = UploadQuerySet.as_manager() @property - def is_local(self): + def is_local(self) -> bool: return federation_utils.is_local(self.fid) @property @@ -834,7 +834,7 @@ class Upload(models.Model): ) @property - def filename(self): + def filename(self) -> str: return "{}.{}".format(self.track.full_name, self.extension) @property @@ -910,10 +910,10 @@ class Upload(models.Model): return metadata.Metadata(audio_file) @property - def listen_url(self): + def listen_url(self) -> str: return self.track.listen_url + "?upload={}".format(self.uuid) - def get_listen_url(self, to=None, download=True): + def get_listen_url(self, to=None, download=True) -> str: url = self.listen_url if to: url += "&to={}".format(to) @@ -1019,7 +1019,7 @@ class UploadVersion(models.Model): unique_together = ("upload", "mimetype", "bitrate") @property - def filename(self): + def filename(self) -> str: try: return ( self.upload.track.full_name @@ -1211,15 +1211,15 @@ class Library(federation_models.FederationMixin): def __str__(self): return self.name - def get_moderation_url(self): + def get_moderation_url(self) -> str: return "/manage/library/libraries/{}".format(self.uuid) - def get_federation_id(self): + def get_federation_id(self) -> str: return federation_utils.full_url( reverse("federation:music:libraries-detail", kwargs={"uuid": self.uuid}) ) - def get_absolute_url(self): + def get_absolute_url(self) -> str: return "/library/{}".format(self.uuid) def save(self, **kwargs): @@ -1229,7 +1229,7 @@ class Library(federation_models.FederationMixin): return super().save(**kwargs) - def should_autoapprove_follow(self, actor): + def should_autoapprove_follow(self, actor) -> bool: if self.privacy_level == "everyone": return True if self.privacy_level == "instance" and actor.get_user(): diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index df4e70133..0f0884d67 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -11,12 +11,16 @@ from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import utils as common_utils from funkwhale_api.federation import routes from funkwhale_api.federation import utils as federation_utils +from funkwhale_api.federation.serializers import APIActorSerializer from funkwhale_api.playlists import models as playlists_models from funkwhale_api.tags import models as tag_models from funkwhale_api.tags import serializers as tags_serializers from . import filters, models, tasks, utils +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + NOOP = object() COVER_WRITE_FIELD = common_serializers.RelatedField( @@ -29,8 +33,6 @@ COVER_WRITE_FIELD = common_serializers.RelatedField( write_only=True, ) -from funkwhale_api.audio import serializers as audio_serializers # NOQA - class CoverField(common_serializers.AttachmentSerializer): pass @@ -39,16 +41,6 @@ class CoverField(common_serializers.AttachmentSerializer): cover_field = CoverField() -def serialize_attributed_to(self, obj): - # Import at runtime to avoid a circular import issue - from funkwhale_api.federation import serializers as federation_serializers - - if not obj.attributed_to_id: - return - - return federation_serializers.APIActorSerializer(obj.attributed_to).data - - class OptionalDescriptionMixin(object): def to_representation(self, obj): repr = super().to_representation(obj) @@ -74,7 +66,7 @@ class LicenseSerializer(serializers.Serializer): attribution = serializers.BooleanField() copyleft = serializers.BooleanField() - def get_id(self, obj): + def get_id(self, obj) -> str: return obj["identifiers"][0] class Meta: @@ -94,13 +86,13 @@ class ArtistAlbumSerializer(serializers.Serializer): release_date = serializers.DateField() creation_date = serializers.DateTimeField() - def get_artist(self, o): + def get_artist(self, o) -> int: return o.artist_id - def get_tracks_count(self, o): + def get_tracks_count(self, o) -> int: return len(o.tracks.all()) - def get_is_playable(self, obj): + def get_is_playable(self, obj) -> bool: try: return bool(obj.is_playable_by_actor) except AttributeError: @@ -113,7 +105,7 @@ DATETIME_FIELD = serializers.DateTimeField() class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer): albums = ArtistAlbumSerializer(many=True) tags = serializers.SerializerMethodField() - attributed_to = serializers.SerializerMethodField() + attributed_to = APIActorSerializer() channel = serializers.SerializerMethodField() tracks_count = serializers.SerializerMethodField() id = serializers.IntegerField() @@ -125,16 +117,16 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize is_local = serializers.BooleanField() cover = cover_field + @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] - get_attributed_to = serialize_attributed_to - - def get_tracks_count(self, o): + def get_tracks_count(self, o) -> int: tracks = getattr(o, "_prefetched_tracks", None) - return len(tracks) if tracks else 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: @@ -150,68 +142,47 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize } -def serialize_artist_simple(artist): - data = { - "id": artist.id, - "fid": artist.fid, - "mbid": str(artist.mbid), - "name": artist.name, - "creation_date": DATETIME_FIELD.to_representation(artist.creation_date), - "modification_date": DATETIME_FIELD.to_representation(artist.modification_date), - "is_local": artist.is_local, - "content_category": artist.content_category, - } - if "description" in artist._state.fields_cache: - data["description"] = ( - common_serializers.ContentSerializer(artist.description).data - if artist.description - else None +class SimpleArtistSerializer(serializers.ModelSerializer): + attachment_cover = cover_field + description = common_serializers.ContentSerializer() + + class Meta: + model = models.Artist + fields = ( + "id", + "fid", + "mbid", + "name", + "creation_date", + "modification_date", + "is_local", + "content_category", + "description", + "attachment_cover", + "channel", ) - if "attachment_cover" in artist._state.fields_cache: - data["cover"] = ( - cover_field.to_representation(artist.attachment_cover) - if artist.attachment_cover - else None - ) - if "channel" in artist._state.fields_cache and artist.get_channel(): - data["channel"] = str(artist.channel.uuid) - - if getattr(artist, "_tracks_count", None) is not None: - data["tracks_count"] = artist._tracks_count - - if getattr(artist, "_prefetched_tagged_items", None) is not None: - data["tags"] = [ti.tag.name for ti in artist._prefetched_tagged_items] - - return data - class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer): - artist = serializers.SerializerMethodField() + artist = SimpleArtistSerializer() cover = cover_field is_playable = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() tracks_count = serializers.SerializerMethodField() - attributed_to = serializers.SerializerMethodField() + attributed_to = APIActorSerializer() id = serializers.IntegerField() fid = serializers.URLField() mbid = serializers.UUIDField() title = serializers.CharField() - artist = serializers.SerializerMethodField() release_date = serializers.DateField() creation_date = serializers.DateTimeField() is_local = serializers.BooleanField() duration = serializers.SerializerMethodField(read_only=True) - get_attributed_to = serialize_attributed_to - - def get_artist(self, o): - return serialize_artist_simple(o.artist) - - def get_tracks_count(self, o): + def get_tracks_count(self, o) -> int: return len(o.tracks.all()) - def get_is_playable(self, obj): + def get_is_playable(self, obj) -> bool: try: return any( [ @@ -222,11 +193,12 @@ class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer): except AttributeError: return None + @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] - def get_duration(self, obj): + def get_duration(self, obj) -> int: try: return obj.duration except AttributeError: @@ -235,11 +207,11 @@ class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer): class TrackAlbumSerializer(serializers.ModelSerializer): - artist = serializers.SerializerMethodField() + artist = SimpleArtistSerializer() cover = cover_field tracks_count = serializers.SerializerMethodField() - def get_tracks_count(self, o): + def get_tracks_count(self, o) -> int: return getattr(o, "_prefetched_tracks_count", len(o.tracks.all())) class Meta: @@ -257,11 +229,8 @@ class TrackAlbumSerializer(serializers.ModelSerializer): "tracks_count", ) - def get_artist(self, o): - return serialize_artist_simple(o.artist) - -def serialize_upload(upload): +def serialize_upload(upload) -> object: return { "uuid": str(upload.uuid), "listen_url": upload.listen_url, @@ -291,18 +260,17 @@ def sort_uploads_for_listen(uploads): class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer): - artist = serializers.SerializerMethodField() + artist = SimpleArtistSerializer() album = TrackAlbumSerializer(read_only=True) uploads = serializers.SerializerMethodField() listen_url = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() - attributed_to = serializers.SerializerMethodField() + attributed_to = APIActorSerializer() id = serializers.IntegerField() fid = serializers.URLField() mbid = serializers.UUIDField() title = serializers.CharField() - artist = serializers.SerializerMethodField() creation_date = serializers.DateTimeField() is_local = serializers.BooleanField() position = serializers.IntegerField() @@ -311,15 +279,13 @@ class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer): copyright = serializers.CharField() license = serializers.SerializerMethodField() cover = cover_field - get_attributed_to = serialize_attributed_to is_playable = serializers.SerializerMethodField() - def get_artist(self, o): - return serialize_artist_simple(o.artist) - + @extend_schema_field(OpenApiTypes.URI) def get_listen_url(self, obj): return obj.listen_url + @extend_schema_field({"type": "array", "items": {"type": "object"}}) def get_uploads(self, obj): uploads = getattr(obj, "playable_uploads", []) # we put local uploads first @@ -327,14 +293,15 @@ 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"}}) def get_tags(self, obj): tagged_items = getattr(obj, "_prefetched_tagged_items", []) return [ti.tag.name for ti in tagged_items] - def get_license(self, o): + def get_license(self, o) -> str: return o.license_id - def get_is_playable(self, obj): + def get_is_playable(self, obj) -> bool: return bool(getattr(obj, "playable_uploads", [])) @@ -359,10 +326,10 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer): ] read_only_fields = ["fid", "uuid", "creation_date", "actor"] - def get_uploads_count(self, o): - return getattr(o, "_uploads_count", o.uploads_count) + def get_uploads_count(self, o) -> int: + return getattr(o, "_uploads_count", int(o.uploads_count)) - def get_size(self, o): + def get_size(self, o) -> int: return getattr(o, "_size", 0) def on_updated_fields(self, obj, before, after): @@ -370,14 +337,14 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer): {"type": "Update", "object": {"type": "Library"}}, context={"library": obj} ) + @extend_schema_field(APIActorSerializer) def get_actor(self, o): - # Import at runtime to avoid a circular import issue - from funkwhale_api.federation import serializers as federation_serializers - - return federation_serializers.APIActorSerializer(o.actor).data + return APIActorSerializer(o.actor).data class UploadSerializer(serializers.ModelSerializer): + from funkwhale_api.audio.serializers import ChannelSerializer + track = TrackSerializer(required=False, allow_null=True) library = common_serializers.RelatedField( "uuid", @@ -387,7 +354,7 @@ class UploadSerializer(serializers.ModelSerializer): ) channel = common_serializers.RelatedField( "uuid", - audio_serializers.ChannelSerializer(), + ChannelSerializer(), required=False, filters=lambda context: {"attributed_to": context["user"].actor}, ) diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index 07a7298a7..f0554e1a4 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -5,6 +5,9 @@ from funkwhale_api.music.models import Track from funkwhale_api.music.serializers import TrackSerializer from funkwhale_api.users.serializers import UserBasicSerializer +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import models @@ -46,31 +49,34 @@ class PlaylistSerializer(serializers.ModelSerializer): ) read_only_fields = ["id", "modification_date", "creation_date"] + @extend_schema_field(federation_serializers.APIActorSerializer) def get_actor(self, obj): actor = obj.user.actor if actor: return federation_serializers.APIActorSerializer(actor).data + @extend_schema_field(OpenApiTypes.BOOL) def get_is_playable(self, obj): try: return bool(obj.playable_plts) except AttributeError: return None - def get_tracks_count(self, obj): + def get_tracks_count(self, obj) -> int: try: return obj.tracks_count except AttributeError: # no annotation? return obj.playlist_tracks.count() - def get_duration(self, obj): + def get_duration(self, obj) -> int: try: return obj.duration except AttributeError: # no annotation? return 0 + @extend_schema_field({"type": "array", "items": {"type": "uri"}}) def get_album_covers(self, obj): try: plts = obj.plts_for_cover diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index cb431e1aa..d07d2ae1c 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -307,7 +307,7 @@ class User(AbstractUser): return groups - def full_username(self): + def full_username(self) -> str: return "{}@{}".format(self.username, settings.FEDERATION_HOSTNAME) def get_avatar(self): diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 317327459..23c2da462 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -21,6 +21,9 @@ from funkwhale_api.moderation import models as moderation_models from funkwhale_api.moderation import tasks as moderation_tasks from funkwhale_api.moderation import utils as moderation_utils +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes + from . import adapters from . import models from . import authentication as users_authentication @@ -205,6 +208,7 @@ class UserReadSerializer(serializers.ModelSerializer): def get_permissions(self, o): return o.get_permissions() + @extend_schema_field(OpenApiTypes.STR) def get_full_username(self, o): if o.actor: return o.actor.full_username diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py index c91f1409f..17851fcea 100644 --- a/api/tests/audio/test_serializers.py +++ b/api/tests/audio/test_serializers.py @@ -216,7 +216,7 @@ def test_channel_serializer_representation(factories, to_api_date): channel = factories["audio.Channel"](artist__description=content) setattr(channel, "_downloads_count", 12) expected = { - "artist": music_serializers.serialize_artist_simple(channel.artist), + "artist": music_serializers.SimpleArtistSerializer(channel.artist).data, "uuid": str(channel.uuid), "creation_date": to_api_date(channel.creation_date), "actor": federation_serializers.APIActorSerializer(channel.actor).data, @@ -240,7 +240,7 @@ def test_channel_serializer_external_representation(factories, to_api_date): channel = factories["audio.Channel"](artist__description=content, external=True) expected = { - "artist": music_serializers.serialize_artist_simple(channel.artist), + "artist": music_serializers.SimpleArtistSerializer(channel.artist).data, "uuid": str(channel.uuid), "creation_date": to_api_date(channel.creation_date), "actor": None, diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index beb30279d..213167277 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -179,7 +179,7 @@ def test_album_serializer(factories, to_api_date): "fid": album.fid, "mbid": str(album.mbid), "title": album.title, - "artist": serializers.serialize_artist_simple(album.artist), + "artist": serializers.SimpleArtistSerializer(album.artist).data, "creation_date": to_api_date(album.creation_date), "is_playable": False, "duration": 0, @@ -209,7 +209,7 @@ def test_track_album_serializer(factories, to_api_date): "fid": album.fid, "mbid": str(album.mbid), "title": album.title, - "artist": serializers.serialize_artist_simple(album.artist), + "artist": serializers.SimpleArtistSerializer(album.artist).data, "creation_date": to_api_date(album.creation_date), "is_playable": False, "cover": common_serializers.AttachmentSerializer(album.attachment_cover).data, @@ -241,7 +241,7 @@ def test_track_serializer(factories, to_api_date): expected = { "id": track.id, "fid": track.fid, - "artist": serializers.serialize_artist_simple(track.artist), + "artist": serializers.SimpleArtistSerializer(track.artist).data, "album": serializers.TrackAlbumSerializer(track.album).data, "mbid": str(track.mbid), "title": track.title, diff --git a/changes/changelog.d/typehints.enhancement b/changes/changelog.d/typehints.enhancement new file mode 100644 index 000000000..61d97cfaf --- /dev/null +++ b/changes/changelog.d/typehints.enhancement @@ -0,0 +1 @@ +Added type hints to the API.