From 1274b2d84953a04f2177645629a56ec0970b3190 Mon Sep 17 00:00:00 2001 From: Kyle Robbertze Date: Sat, 16 Oct 2021 18:34:03 +0000 Subject: [PATCH] Add openapi spec for API v2 (#1388) --- api/README.md | 4 +-- api/libretimeapi/serializers.py | 11 +++--- api/libretimeapi/settings.py | 6 ++-- api/libretimeapi/tests/test_views.py | 53 ++++++++++++++++++++++++++++ api/libretimeapi/urls.py | 7 ++++ api/libretimeapi/views.py | 31 ++++++++++++++-- api/setup.py | 3 +- 7 files changed, 104 insertions(+), 11 deletions(-) diff --git a/api/README.md b/api/README.md index e2cf1f378..17b8723a6 100644 --- a/api/README.md +++ b/api/README.md @@ -15,8 +15,8 @@ restarting: Connections to the API are proxied through the Apache web server by default. Endpoint exploration and documentation is available from -`http://example.com/api/v2/`, where `example.com` is the URL for the LibreTime -instance. +`http://example.com/api/v2/schema/swagger-ui/`, where `example.com` is the URL +for the LibreTime instance. ### Development diff --git a/api/libretimeapi/serializers.py b/api/libretimeapi/serializers.py index 200e3e2ce..ab448c981 100644 --- a/api/libretimeapi/serializers.py +++ b/api/libretimeapi/serializers.py @@ -138,18 +138,21 @@ class ScheduleSerializer(serializers.HyperlinkedModelSerializer): "id", "starts", "ends", + "file", + "file_id", + "stream", + "stream_id", "clip_length", "fade_in", "fade_out", "cue_in", "cue_out", "media_item_played", - "file", - "file_id", - "stream", - "stream_id", "instance", "instance_id", + "playout_status", + "broadcasted", + "position", ] diff --git a/api/libretimeapi/settings.py b/api/libretimeapi/settings.py index 6c916a00e..ebadf9dc4 100644 --- a/api/libretimeapi/settings.py +++ b/api/libretimeapi/settings.py @@ -38,7 +38,8 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "url_filter", + "django_filters", + "drf_spectacular", ] MIDDLEWARE = [ @@ -114,8 +115,9 @@ REST_FRAMEWORK = { "libretimeapi.permissions.IsSystemTokenOrUser", ], "DEFAULT_FILTER_BACKENDS": [ - "url_filter.integrations.drf.DjangoFilterBackend", + "django_filters.rest_framework.DjangoFilterBackend", ], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "URL_FIELD_NAME": "item_url", } diff --git a/api/libretimeapi/tests/test_views.py b/api/libretimeapi/tests/test_views.py index 5ec6acf81..6ae6b322f 100644 --- a/api/libretimeapi/tests/test_views.py +++ b/api/libretimeapi/tests/test_views.py @@ -164,3 +164,56 @@ class TestScheduleViewSet(APITestCase): self.assertEqual(len(result), 1) self.assertEqual(dateparse.parse_datetime(result[0]["ends"]), scheduleItem.ends) self.assertEqual(dateparse.parse_duration(result[0]["cue_out"]), f.cueout) + + def test_schedule_item_range(self): + music_dir = baker.make( + "libretimeapi.MusicDir", + directory=os.path.join(os.path.dirname(__file__), "resources"), + ) + f = baker.make( + "libretimeapi.File", + directory=music_dir, + mime="audio/mp3", + filepath="song.mp3", + length=timedelta(seconds=40.86), + cuein=timedelta(seconds=0), + cueout=timedelta(seconds=40.8131), + ) + filter_point = datetime.now(tz=timezone.utc) + + show = baker.make( + "libretimeapi.ShowInstance", + starts=filter_point - timedelta(minutes=5), + ends=filter_point + timedelta(minutes=5), + ) + schedule_item = baker.make( + "libretimeapi.Schedule", + starts=filter_point, + ends=filter_point + f.length, + cue_out=f.cueout, + instance=show, + file=f, + ) + previous_item = baker.make( + "libretimeapi.Schedule", + starts=filter_point - timedelta(minutes=5), + ends=filter_point - timedelta(minutes=5) + f.length, + cue_out=f.cueout, + instance=show, + file=f, + ) + self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token)) + range_start = (filter_point - timedelta(minutes=1)).isoformat( + timespec="seconds" + ) + range_end = (filter_point + timedelta(minutes=1)).isoformat(timespec="seconds") + response = self.client.get( + self.path, {"starts__range": "{},{}".format(range_start, range_end)} + ) + self.assertEqual(response.status_code, 200) + result = response.json() + # The previous_item should be filtered out and not returned + self.assertEqual(len(result), 1) + self.assertEqual( + dateparse.parse_datetime(result[0]["starts"]), schedule_item.starts + ) diff --git a/api/libretimeapi/urls.py b/api/libretimeapi/urls.py index df7ca36ed..d7290dc7a 100644 --- a/api/libretimeapi/urls.py +++ b/api/libretimeapi/urls.py @@ -1,4 +1,5 @@ from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework import routers from .views import * @@ -46,6 +47,12 @@ router.register("track-types", TrackTypeViewSet) urlpatterns = [ path("api/v2/", include(router.urls)), + path("api/v2/schema/", SpectacularAPIView.as_view(), name="schema"), + path( + "api/v2/schema/swagger-ui/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), path("api/v2/version/", version), path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), ] diff --git a/api/libretimeapi/views.py b/api/libretimeapi/views.py index e42cf59f9..aeb9d5e1a 100644 --- a/api/libretimeapi/views.py +++ b/api/libretimeapi/views.py @@ -4,7 +4,8 @@ from django.conf import settings from django.db.models import F from django.http import FileResponse from django.shortcuts import get_object_or_404 -from rest_framework import status, viewsets +from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view +from rest_framework import fields, status, viewsets from rest_framework.decorators import action, api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -12,6 +13,15 @@ from rest_framework.response import Response from .permissions import IsAdminOrOwnUser from .serializers import * +FILTER_NUMERICAL_LOOKUPS = [ + "exact", + "gt", + "lt", + "gte", + "lte", + "range", +] + class UserViewSet(viewsets.ModelViewSet): queryset = get_user_model().objects.all() @@ -139,10 +149,27 @@ class PreferenceViewSet(viewsets.ModelViewSet): model_permission_name = "preference" +@extend_schema_view( + list=extend_schema( + parameters=[ + OpenApiParameter( + name="is_valid", + description="Filter on valid instances", + required=False, + type=bool, + ), + ] + ) +) class ScheduleViewSet(viewsets.ModelViewSet): queryset = Schedule.objects.all() serializer_class = ScheduleSerializer - filter_fields = ("starts", "ends", "playout_status", "broadcasted") + filter_fields = { + "starts": FILTER_NUMERICAL_LOOKUPS, + "ends": FILTER_NUMERICAL_LOOKUPS, + "playout_status": FILTER_NUMERICAL_LOOKUPS, + "broadcasted": FILTER_NUMERICAL_LOOKUPS, + } model_permission_name = "schedule" def get_queryset(self): diff --git a/api/setup.py b/api/setup.py index 16a8c7607..12397beff 100644 --- a/api/setup.py +++ b/api/setup.py @@ -25,7 +25,8 @@ setup( "coreapi", "django~=3.0", "djangorestframework", - "django-url-filter", + "django-filter", + "drf-spectacular", "markdown", "model_bakery", ],