diff --git a/.gitpod.yml b/.gitpod.yml index e8aaf4e4d9dea29c792b212889799650c4089f18..e27625a4c380a7d6839fb467b78031e2001d9de5 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -45,6 +45,7 @@ tasks: COMPOSE_FILE: /workspace/funkwhale/.gitpod/docker-compose.yml ENV_FILE: /workspace/funkwhale/.gitpod/.env VUE_EDITOR: code + DJANGO_SETTINGS_MODULE: config.settings.local command: | clear echo "" diff --git a/api/.coveragerc b/api/.coveragerc index 4e3cf2bad4b049a84f378461b2f353267d2cbcba..a27c034bb811aed4ae217ef69532cad8bd9a5af1 100644 --- a/api/.coveragerc +++ b/api/.coveragerc @@ -1,5 +1,5 @@ [run] include = funkwhale_api/* -omit = *migrations*, *tests* +omit = *migrations*, *tests*, funkwhale_api/schema.py plugins = django_coverage_plugin diff --git a/api/config/schema.py b/api/config/schema.py index 556536909c7b73b2c3dabe1d18c9cd693963aaf6..b1e5ab63219c2c1ef78995623262b26ec52553f5 100644 --- a/api/config/schema.py +++ b/api/config/schema.py @@ -42,11 +42,18 @@ class CustomApplicationTokenExt(OpenApiAuthenticationExtension): def custom_preprocessing_hook(endpoints): filtered = [] + # your modifications to the list of operations that are exposed in the schema 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("/api/v1/users/users"): + 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 3dacaf9ee72d5caf0dfe19d2cee96544d07d78a9..67e89691f2fec718d3316dfd8dff2c31b95ec752 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -99,7 +99,7 @@ CELERY_TASK_ALWAYS_EAGER = False CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS] -REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema" +REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "funkwhale_api.schema.CustomAutoSchema" SPECTACULAR_SETTINGS = { "TITLE": "Funkwhale API", "DESCRIPTION": open("Readme.md", "r").read(), diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py index 701dd04b8cfbacc1f0b5fc75ab65597c87a99b55..15524c8f1483fdbdda2d7e8785817a3c617ef07e 100644 --- a/api/funkwhale_api/activity/views.py +++ b/api/funkwhale_api/activity/views.py @@ -1,6 +1,8 @@ from rest_framework import viewsets from rest_framework.response import Response +from drf_spectacular.utils import extend_schema + from funkwhale_api.common.permissions import ConditionalAuthentication from funkwhale_api.favorites.models import TrackFavorite @@ -13,6 +15,7 @@ class ActivityViewSet(viewsets.GenericViewSet): permission_classes = [ConditionalAuthentication] queryset = TrackFavorite.objects.none() + @extend_schema(operation_id="get_activity") def list(self, request, *args, **kwargs): activity = utils.get_activity(user=request.user) serializer = self.serializer_class(activity, many=True) diff --git a/api/funkwhale_api/audio/views.py b/api/funkwhale_api/audio/views.py index 66a71fd390b23689bcd7098f4a852dcfdca0e3e2..8989ed9b3407881090d0f5ff97ac317f3e9e515f 100644 --- a/api/funkwhale_api/audio/views.py +++ b/api/funkwhale_api/audio/views.py @@ -5,6 +5,8 @@ from rest_framework import permissions as rest_permissions from rest_framework import response from rest_framework import viewsets +from drf_spectacular.utils import extend_schema, extend_schema_view + from django import http from django.db import transaction from django.db.models import Count, Prefetch, Q, Sum @@ -43,6 +45,12 @@ class ChannelsMixin(object): return super().dispatch(request, *args, **kwargs) +@extend_schema_view( + metedata_choices=extend_schema(operation_id="get_channel_metadata_choices"), + subscribe=extend_schema(operation_id="subscribe_channel"), + unsubscribe=extend_schema(operation_id="unsubscribe_channel"), + rss_subscribe=extend_schema(operation_id="subscribe_channel_rss"), +) class ChannelViewSet( ChannelsMixin, MultipleLookupDetailMixin, @@ -322,6 +330,7 @@ class SubscriptionsViewSet( qs = super().get_queryset() return qs.filter(actor=self.request.user.actor) + @extend_schema(operation_id="get_all_subscriptions") @decorators.action(methods=["get"], detail=False) def all(self, request, *args, **kwargs): """ diff --git a/api/funkwhale_api/common/decorators.py b/api/funkwhale_api/common/decorators.py index 49a2fb1939cd80d72111bcbec5139734e0ecd314..df6a5b47037f86eddf704b9bc464a227f47300e2 100644 --- a/api/funkwhale_api/common/decorators.py +++ b/api/funkwhale_api/common/decorators.py @@ -5,6 +5,8 @@ from rest_framework import exceptions from rest_framework import response from rest_framework import status +from drf_spectacular.utils import extend_schema, OpenApiParameter + from . import filters from . import models from . import mutations as common_mutations @@ -87,6 +89,16 @@ def mutations_route(types): ) return response.Response(serializer.data, status=status.HTTP_201_CREATED) - return decorators.action( - methods=["get", "post"], detail=True, required_scope="edits" - )(mutations) + return extend_schema( + methods=["post"], responses=serializers.APIMutationSerializer() + )( + extend_schema( + methods=["get"], + responses=serializers.APIMutationSerializer(many=True), + parameters=[OpenApiParameter("id", location="query", exclude=True)], + )( + decorators.action( + methods=["get", "post"], detail=True, required_scope="edits" + )(mutations) + ) + ) diff --git a/api/funkwhale_api/common/views.py b/api/funkwhale_api/common/views.py index d6cbf1953c82b8a1e9f9ca8a01e19583c8a38ced..a1992759984a9d2abd1dd1910efb14c429066572 100644 --- a/api/funkwhale_api/common/views.py +++ b/api/funkwhale_api/common/views.py @@ -12,6 +12,8 @@ from rest_framework import response from rest_framework import views from rest_framework import viewsets +from drf_spectacular.utils import extend_schema + from config import plugins from funkwhale_api.users.oauth import permissions as oauth_permissions @@ -78,6 +80,7 @@ class MutationViewSet( return super().perform_destroy(instance) + @extend_schema(operation_id="approve_mutation") @action(detail=True, methods=["post"]) @transaction.atomic def approve(self, request, *args, **kwargs): @@ -107,6 +110,7 @@ class MutationViewSet( ) return response.Response({}, status=200) + @extend_schema(operation_id="reject_mutation") @action(detail=True, methods=["post"]) @transaction.atomic def reject(self, request, *args, **kwargs): @@ -201,6 +205,7 @@ class AttachmentViewSet( class TextPreviewView(views.APIView): permission_classes = [] + @extend_schema(operation_id="preview_text") def post(self, request, *args, **kwargs): payload = request.data if "text" not in payload: @@ -273,6 +278,7 @@ class PluginViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): user.plugins.filter(code=kwargs["pk"]).delete() return response.Response(status=204) + @extend_schema(operation_id="enable_plugin") @action(detail=True, methods=["post"]) def enable(self, request, *args, **kwargs): user = request.user @@ -281,6 +287,7 @@ class PluginViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): plugins.enable_conf(kwargs["pk"], True, user) return response.Response({}, status=200) + @extend_schema(operation_id="disable_plugin") @action(detail=True, methods=["post"]) def disable(self, request, *args, **kwargs): user = request.user diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index db0c909001edef6f854216a9a22895a2a42c0fb0..d01a546c98fbf03d89261190497294eee4fd09d8 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -2,6 +2,8 @@ from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from drf_spectacular.utils import extend_schema + from django.db.models import Prefetch from funkwhale_api.activity import record @@ -38,6 +40,7 @@ class TrackFavoriteViewSet( return serializers.UserTrackFavoriteSerializer return serializers.UserTrackFavoriteWriteSerializer + @extend_schema(operation_id="favorite_track") def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -67,6 +70,7 @@ class TrackFavoriteViewSet( favorite = models.TrackFavorite.add(track=track, user=self.request.user) return favorite + @extend_schema(operation_id="unfavorite_track") @action(methods=["delete", "post"], detail=False) def remove(self, request, *args, **kwargs): try: @@ -77,6 +81,7 @@ class TrackFavoriteViewSet( favorite.delete() return Response([], status=status.HTTP_204_NO_CONTENT) + @extend_schema(operation_id="get_all_favorite_tracks") @action(methods=["get"], detail=False) def all(self, request, *args, **kwargs): """ diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py index f49c51c3575869e2417b672ea6adfb1d88c823d1..13cb202073a40942c55fedf46cad80ed3fc61311 100644 --- a/api/funkwhale_api/federation/api_views.py +++ b/api/funkwhale_api/federation/api_views.py @@ -10,6 +10,8 @@ from rest_framework import permissions from rest_framework import response from rest_framework import viewsets +from drf_spectacular.utils import extend_schema, extend_schema_view + from funkwhale_api.common import preferences from funkwhale_api.common import utils as common_utils from funkwhale_api.common.permissions import ConditionalAuthentication @@ -38,6 +40,13 @@ def update_follow(follow, approved): routes.outbox.dispatch({"type": "Reject"}, context={"follow": follow}) +@extend_schema_view( + list=extend_schema(operation_id="get_federation_library_follows"), + create=extend_schema(operation_id="create_federation_library_follow"), +) +# NOTE: For some weird reason, @extend_schema_view doesn't work with `retrieve` and `destroy` methods. +@extend_schema(operation_id="get_federation_library_follow", methods=["get"]) +@extend_schema(operation_id="delete_federation_library_follow", methods=["delete"]) class LibraryFollowViewSet( mixins.CreateModelMixin, mixins.ListModelMixin, @@ -77,6 +86,7 @@ class LibraryFollowViewSet( context["actor"] = self.request.user.actor return context + @extend_schema(operation_id="accept_federation_library_follow") @decorators.action(methods=["post"], detail=True) def accept(self, request, *args, **kwargs): try: @@ -88,6 +98,7 @@ class LibraryFollowViewSet( update_follow(follow, approved=True) return response.Response(status=204) + @extend_schema(operation_id="reject_federation_library_follow") @decorators.action(methods=["post"], detail=True) def reject(self, request, *args, **kwargs): try: @@ -100,6 +111,7 @@ class LibraryFollowViewSet( update_follow(follow, approved=False) return response.Response(status=204) + @extend_schema(operation_id="get_all_federation_library_follows") @decorators.action(methods=["get"], detail=False) def all(self, request, *args, **kwargs): """ diff --git a/api/funkwhale_api/federation/decorators.py b/api/funkwhale_api/federation/decorators.py index 3d2d62567613f7899eea0c2a9356812150bcd160..0a3416beb982cc77720482c2589b2a76e25d745f 100644 --- a/api/funkwhale_api/federation/decorators.py +++ b/api/funkwhale_api/federation/decorators.py @@ -5,6 +5,8 @@ from rest_framework import permissions from rest_framework import response from rest_framework import status +from drf_spectacular.utils import extend_schema, OpenApiParameter + from funkwhale_api.common import utils as common_utils from . import api_serializers @@ -42,8 +44,16 @@ def fetches_route(): serializer = api_serializers.FetchSerializer(fetch) return response.Response(serializer.data, status=status.HTTP_201_CREATED) - return decorators.action( - methods=["get", "post"], - detail=True, - permission_classes=[permissions.IsAuthenticated], - )(fetches) + return extend_schema(methods=["post"], responses=api_serializers.FetchSerializer())( + extend_schema( + methods=["get"], + responses=api_serializers.FetchSerializer(many=True), + parameters=[OpenApiParameter("id", location="query", exclude=True)], + )( + decorators.action( + methods=["get", "post"], + detail=True, + permission_classes=[permissions.IsAuthenticated], + )(fetches) + ) + ) diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index dd8ea7b5376577b4e0a331f36e3fe4832562f517..7eb34135d2cbd77b83463d67f426ec8e01474e23 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -53,6 +53,7 @@ class InstanceSettings(generics.GenericAPIView): ] return api_preferences + @extend_schema(operation_id="get_instance_settings") def get(self, request): queryset = self.get_queryset() data = GlobalPreferenceSerializer(queryset, many=True).data @@ -121,6 +122,7 @@ class SpaManifest(views.APIView): permission_classes = [] authentication_classes = [] + @extend_schema(operation_id="get_spa_manifest") def get(self, request, *args, **kwargs): existing_manifest = middleware.get_spa_file( settings.FUNKWHALE_SPA_HTML_ROOT, "manifest.json" diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index dc90ffe7970442ee43eaed714012c1e393aa631a..292ac1fa1521548ab05b1d093bface7ce867f33e 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -1,6 +1,8 @@ from rest_framework import mixins, response, viewsets from rest_framework import decorators as rest_decorators +from drf_spectacular.utils import extend_schema + from django.db import transaction from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery from django.db.models.functions import Coalesce, Length @@ -93,6 +95,7 @@ class ManageArtistViewSet( required_scope = "instance:libraries" ordering_fields = ["creation_date", "name"] + @extend_schema(operation_id="admin_get_library_artist_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): artist = self.get_object() @@ -135,6 +138,7 @@ class ManageAlbumViewSet( required_scope = "instance:libraries" ordering_fields = ["creation_date", "title", "release_date"] + @extend_schema(operation_id="admin_get_library_album_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): album = self.get_object() @@ -196,6 +200,7 @@ class ManageTrackViewSet( "disc_number", ] + @extend_schema(operation_id="admin_get_track_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): track = self.get_object() @@ -257,6 +262,7 @@ class ManageLibraryViewSet( filterset_class = filters.ManageLibraryFilterSet required_scope = "instance:libraries" + @extend_schema(operation_id="admin_get_library_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): library = self.get_object() @@ -424,6 +430,7 @@ class ManageDomainViewSet( domain.refresh_from_db() return response.Response(domain.nodeinfo, status=200) + @extend_schema(operation_id="admin_get_federation_domain_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): domain = self.get_object() @@ -468,6 +475,7 @@ class ManageActorViewSet( return obj + @extend_schema(operation_id="admin_get_account_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): obj = self.get_object() @@ -709,6 +717,7 @@ class ManageChannelViewSet( required_scope = "instance:libraries" ordering_fields = ["creation_date", "name"] + @extend_schema(operation_id="admin_get_channel_stats") @rest_decorators.action(methods=["get"], detail=True) def stats(self, request, *args, **kwargs): channel = self.get_object() diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index d7eba93cfc8d98bd497d2e7c59b1b9b432c49b8a..9b6eb524fa76422e8683f0a58b655733e6652ad8 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -16,6 +16,8 @@ from rest_framework import views, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from drf_spectacular.utils import extend_schema, OpenApiParameter + import requests.exceptions from funkwhale_api.common import decorators as common_decorators @@ -66,7 +68,10 @@ def get_libraries(filter_uploads): serializer = federation_api_serializers.LibrarySerializer(qs, many=True) return Response(serializer.data) - return libraries + return extend_schema( + responses=federation_api_serializers.LibrarySerializer(many=True), + parameters=[OpenApiParameter("id", location="query", exclude=True)], + )(action(methods=["get"], detail=True)(libraries)) def refetch_obj(obj, queryset): @@ -167,11 +172,9 @@ class ArtistViewSet( Prefetch("albums", queryset=albums), TAG_PREFETCH ) - libraries = action(methods=["get"], detail=True)( - get_libraries( - filter_uploads=lambda o, uploads: uploads.filter( - Q(track__artist=o) | Q(track__album__artist=o) - ) + libraries = get_libraries( + lambda o, uploads: uploads.filter( + Q(track__artist=o) | Q(track__album__artist=o) ) ) @@ -231,9 +234,7 @@ class AlbumViewSet( Prefetch("tracks", queryset=tracks), TAG_PREFETCH ) - libraries = action(methods=["get"], detail=True)( - get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track__album=o)) - ) + libraries = get_libraries(lambda o, uploads: uploads.filter(track__album=o)) def get_serializer_class(self): if self.action in ["create"]: @@ -430,9 +431,7 @@ class TrackViewSet( ) return queryset.prefetch_related(TAG_PREFETCH) - libraries = action(methods=["get"], detail=True)( - get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o)) - ) + libraries = get_libraries(lambda o, uploads: uploads.filter(track=o)) def get_serializer_context(self): context = super().get_serializer_context() @@ -744,6 +743,7 @@ class UploadViewSet( qs = qs.playable_by(actor) return qs + @extend_schema(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() @@ -802,6 +802,7 @@ class Search(views.APIView): required_scope = "libraries" anonymous_policy = "setting" + @extend_schema(operation_id="get_search_results") def get(self, request, *args, **kwargs): query = request.GET.get("query", request.GET.get("q", "")) or "" query = query.strip() diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index e4b1d3037811f5e19ac24747adc4a9827c84f79c..e2ebd59ab3978adcfc877df72cf80b743d860086 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -1,9 +1,12 @@ from django.db import transaction from django.db.models import Count + from rest_framework import exceptions, mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from drf_spectacular.utils import extend_schema + from funkwhale_api.common import fields, permissions from funkwhale_api.music import utils as music_utils from funkwhale_api.users.oauth import permissions as oauth_permissions @@ -38,6 +41,7 @@ class PlaylistViewSet( filterset_class = filters.PlaylistFilter ordering_fields = ("id", "name", "creation_date", "modification_date") + @extend_schema(responses=serializers.PlaylistTrackSerializer(many=True)) @action(methods=["get"], detail=True) def tracks(self, request, *args, **kwargs): playlist = self.get_object() @@ -48,6 +52,9 @@ class PlaylistViewSet( data = {"count": len(plts), "results": serializer.data} return Response(data, status=200) + @extend_schema( + operation_id="add_to_playlist", request=serializers.PlaylistAddManySerializer + ) @action(methods=["post"], detail=True) @transaction.atomic def add(self, request, *args, **kwargs): @@ -72,6 +79,7 @@ class PlaylistViewSet( data = {"count": len(plts), "results": serializer.data} return Response(data, status=201) + @extend_schema(operation_id="clear_playlist") @action(methods=["delete"], detail=True) @transaction.atomic def clear(self, request, *args, **kwargs): @@ -93,6 +101,7 @@ class PlaylistViewSet( ), ) + @extend_schema(operation_id="remove_from_playlist") @action(methods=["post", "delete"], detail=True) @transaction.atomic def remove(self, request, *args, **kwargs): @@ -111,6 +120,7 @@ class PlaylistViewSet( return Response(status=204) + @extend_schema(operation_id="reorder_track_in_playlist") @action(methods=["post"], detail=True) @transaction.atomic def move(self, request, *args, **kwargs): diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py index b8c10f1f338bacc21350f3edfc7a935a971eb9d0..1d3098d59268a0f8df7dd8b02af11e89d5a1a321 100644 --- a/api/funkwhale_api/radios/views.py +++ b/api/funkwhale_api/radios/views.py @@ -1,8 +1,11 @@ from django.db.models import Q + from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from drf_spectacular.utils import extend_schema + from funkwhale_api.common import permissions as common_permissions from funkwhale_api.music.serializers import TrackSerializer from funkwhale_api.music import utils as music_utils @@ -63,6 +66,7 @@ class RadioViewSet( ) return Response(serializer.data) + @extend_schema(operation_id="validate_radio") @action(methods=["post"], detail=False) def validate(self, request, *args, **kwargs): try: @@ -124,6 +128,7 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet) queryset = models.RadioSessionTrack.objects.all() permission_classes = [] + @extend_schema(operation_id="get_next_radio_track") def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/api/funkwhale_api/schema.py b/api/funkwhale_api/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..2f6892f4fcf2493b151441f4c9de5e6976c2baf1 --- /dev/null +++ b/api/funkwhale_api/schema.py @@ -0,0 +1,73 @@ +from drf_spectacular.openapi import AutoSchema + +from pluralizer import Pluralizer + +import re + + +class CustomAutoSchema(AutoSchema): + method_mapping = { + "get": "get", + "post": "create", + "put": "update", + "patch": "partial_update", + "delete": "delete", + } + + pluralizer = Pluralizer() + + def get_operation_id(self): + # Modified operation id getter from + # https://github.com/tfranzel/drf-spectacular/blob/6973aa48f4ff08f7f33799d50c288fcc79ea8076/drf_spectacular/openapi.py#L424-L441 + + tokenized_path = self._tokenize_path() + + # replace dashes as they can be problematic later in code generation + tokenized_path = [t.replace("-", "_") for t in tokenized_path] + + # replace plural forms with singular forms + tokenized_path = [self.pluralizer.singular(t) for t in tokenized_path] + + if not tokenized_path: + tokenized_path.append("root") + + model = tokenized_path.pop() + model_singular = model + + if self.method == "GET" and self._is_list_view(): + action = "get" + model = self.pluralizer.plural(model) + else: + action = self.method_mapping[self.method.lower()] + + if re.search(r"", self.path_regex): + tokenized_path.append("formatted") + + # rename `create_radio_radio` to `create_radio`. Works with all models + if len(tokenized_path) > 0 and model_singular == tokenized_path[0]: + tokenized_path.pop(0) + + # rename `get_radio_radio_track` to `get_radio_track`. Works with all models + if len(tokenized_path) > 1 and tokenized_path[0] == tokenized_path[1]: + tokenized_path.pop(0) + + # rename `get_manage_channel` to `admin_get_channel` + if len(tokenized_path) > 0 and tokenized_path[0] == "manage": + tokenized_path.pop(0) + + # rename `get_manage_library_album` to `admin_get_album` + if len(tokenized_path) > 0 and tokenized_path[0] == "library": + tokenized_path.pop(0) + + # rename `get_manage_user_users` to `admin_get_users` + elif len(tokenized_path) > 0 and tokenized_path[0] == "user": + tokenized_path.pop(0) + + # rename `get_manage_moderation_note` to `moderation_get_note` + elif len(tokenized_path) > 0 and tokenized_path[0] == "moderation": + tokenized_path.pop(0) + return "_".join(["moderation", action] + tokenized_path + [model]) + + return "_".join(["admin", action] + tokenized_path + [model]) + + return "_".join([action] + tokenized_path + [model]) diff --git a/api/funkwhale_api/users/oauth/views.py b/api/funkwhale_api/users/oauth/views.py index c1202650127db629c0f9adb838405b6e1e8eba71..d4d640650bb82a9f4214853545ca55a86986137c 100644 --- a/api/funkwhale_api/users/oauth/views.py +++ b/api/funkwhale_api/users/oauth/views.py @@ -4,9 +4,12 @@ import urllib.parse from django import http from django.utils import timezone from django.db.models import Q + from rest_framework import mixins, permissions, response, views, viewsets from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema + from oauth2_provider import exceptions as oauth2_exceptions from oauth2_provider import views as oauth_views from oauth2_provider.settings import oauth2_settings @@ -83,6 +86,7 @@ class ApplicationViewSet( qs = qs.filter(user=self.request.user) return qs + @extend_schema(operation_id="refresh_oauth_token") @action( detail=True, methods=["post"], diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index b9891e594e9c38a326414e33c30535f047c64647..a780e8d7d4b118ef9519339a84fab82c69988237 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -12,6 +12,8 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response +from drf_spectacular.utils import extend_schema + from funkwhale_api.common import authentication from funkwhale_api.common import preferences from funkwhale_api.common import throttling @@ -19,6 +21,7 @@ from funkwhale_api.common import throttling from . import models, serializers, tasks +@extend_schema(operation_id="register", methods=["post"]) class RegisterView(registration_views.RegisterView): serializer_class = serializers.RegisterSerializer permission_classes = [] @@ -43,18 +46,22 @@ class RegisterView(registration_views.RegisterView): return user +@extend_schema(operation_id="verify_email") class VerifyEmailView(registration_views.VerifyEmailView): action = "verify-email" +@extend_schema(operation_id="change_password") class PasswordChangeView(rest_auth_views.PasswordChangeView): action = "password-change" +@extend_schema(operation_id="reset_password") class PasswordResetView(rest_auth_views.PasswordResetView): action = "password-reset" +@extend_schema(operation_id="confirm_password_reset") class PasswordResetConfirmView(rest_auth_views.PasswordResetConfirmView): action = "password-reset-confirm" @@ -66,6 +73,8 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): lookup_value_regex = r"[a-zA-Z0-9-_.]+" required_scope = "profile" + @extend_schema(operation_id="get_authenticated_user", methods=["get"]) + @extend_schema(operation_id="delete_authenticated_user", methods=["delete"]) @action(methods=["get", "delete"], detail=False) def me(self, request, *args, **kwargs): """Return information about the current user or delete it""" @@ -80,6 +89,7 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer = serializers.MeSerializer(request.user) return Response(serializer.data) + @extend_schema(operation_id="update_settings") @action(methods=["post"], detail=False, url_name="settings", url_path="settings") def set_settings(self, request, *args, **kwargs): """Return information about the current user or delete it""" @@ -111,6 +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") @action( methods=["post"], required_scope="security", @@ -138,6 +149,8 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): return super().partial_update(request, *args, **kwargs) +@extend_schema(operation_id="login") +@action(methods=["post"], detail=False) def login(request): throttling.check_request(request, "login") if request.method != "POST": @@ -157,6 +170,8 @@ def login(request): return response +@extend_schema(operation_id="logout") +@action(methods=["post"], detail=False) def logout(request): if request.method != "POST": return http.HttpResponse(status=405) diff --git a/api/poetry.lock b/api/poetry.lock index b1c7d98c2b56152a65196e3e59a2d4e8eb817941..a1dd842cc7ead4bd5cbc5685766a6b39fe411e82 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1358,6 +1358,14 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pluralizer" +version = "1.2.0" +description = "Singularize or pluralize a given word useing a pre-defined list of rules" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "prompt-toolkit" version = "3.0.31" @@ -2182,7 +2190,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "a95a52abbe6891ec4a40bf876b12b69b39eef53e7781247609efa618178ad116" +content-hash = "235c0f30fce9509c81e057fbbd0d32705cffc360027abca2f629e862ed83214c" [metadata.files] aiohttp = [ @@ -3209,6 +3217,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] +pluralizer = [ + {file = "pluralizer-1.2.0-py3-none-any.whl", hash = "sha256:d8f92ffa787661d9e704d1e0d8abc6c6c4bbaae9e790d7c709707eafbe17ed12"}, + {file = "pluralizer-1.2.0.tar.gz", hash = "sha256:fe3fb8e1e53fabf372e77d8cbebe04b0f8fc7db853aeff50095dbd5628ac39c5"}, +] prompt-toolkit = [ {file = "prompt_toolkit-3.0.31-py3-none-any.whl", hash = "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d"}, {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, diff --git a/api/pyproject.toml b/api/pyproject.toml index bc6c4ee29b794bb1c9db9b4bcf2db599f942a658..bfa4f91cee7fc8569c1c9d068894f6e2b2eac1ca 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -58,6 +58,7 @@ django-cache-memoize = "0.1.10" requests-http-message-signatures = "==0.3.1" drf-spectacular = "==0.23.1" sentry-sdk = "==1.9.8" +pluralizer = "==1.2.0" [tool.poetry.dev-dependencies] flake8 = "==3.9.2" diff --git a/changes/changelog.d/rename_operation_id_for_api_client.enhancement b/changes/changelog.d/rename_operation_id_for_api_client.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..9ddb886c9e4c1cf3906a857c74e57a6aca15491d --- /dev/null +++ b/changes/changelog.d/rename_operation_id_for_api_client.enhancement @@ -0,0 +1 @@ +Rename OpenAPI schema's operation ids for nicer API client method names.