From f1a82e8843f96d25b213d20fa977b882511dafd2 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 11 Apr 2026 17:02:07 -0500 Subject: [PATCH] Bug Fix: Fixed several incorrect or incomplete OpenAPI (`@extend_schema`) schemas across the API --- CHANGELOG.md | 5 +++++ apps/channels/api_views.py | 13 +++++++++++-- apps/connect/api_views.py | 30 +++++++++++++++++++++++++++++- apps/epg/api_views.py | 10 ++++++++-- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7141826c..658fbfee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed `PATCH /api/channels/channels/edit/bulk/` returning a 500 error when the request body included a `streams` list. The bulk edit handler was iterating `validated_data` directly and calling `setattr(channel, "streams", value)`, which Django prohibits on ManyToMany fields. Also added an `@extend_schema` decorator so the Swagger UI correctly documents the endpoint as accepting a JSON array and shows the `streams` field. (Fixes #883) +- Fixed several incorrect or incomplete OpenAPI (`@extend_schema`) schemas across the API: + - `POST /api/epg/import/` — request body was undocumented; now correctly shows the `id` field. Description updated from "import" to "refresh" to match frontend and backend terminology. + - `DELETE /api/channels/logos/bulk-delete/` — `delete_files` boolean was missing from the documented request body. + - `POST /api/channels/channels/batch-set-epg/` — `epg_data_id` inside each association object was not marked `allow_null`/`required=False`, even though passing `null` is the correct way to remove an EPG link. + - `PUT /api/connect/integrations/{id}/subscriptions/set/` — endpoint had no `@extend_schema` at all; now documents that the request body is a JSON array of subscription objects. ### Changed diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index b2ada2ba..dc82d482 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -1565,7 +1565,11 @@ class ChannelViewSet(viewsets.ModelViewSet): name="EpgAssociation", fields={ "channel_id": serializers.IntegerField(), - "epg_data_id": serializers.IntegerField(), + "epg_data_id": serializers.IntegerField( + required=False, + allow_null=True, + help_text="EPG data ID to link. Pass null to remove EPG linkage.", + ), }, ), ) @@ -1741,7 +1745,12 @@ class BulkDeleteLogosAPIView(APIView): "logo_ids": serializers.ListField( child=serializers.IntegerField(), help_text="Logo IDs to delete", - ) + ), + "delete_files": serializers.BooleanField( + required=False, + default=False, + help_text="Whether to also delete local logo files from disk.", + ), }, ), ) diff --git a/apps/connect/api_views.py b/apps/connect/api_views.py index 739e8e32..fdb5d43a 100644 --- a/apps/connect/api_views.py +++ b/apps/connect/api_views.py @@ -1,9 +1,10 @@ -from rest_framework import viewsets, status +from rest_framework import viewsets, status, serializers from rest_framework.pagination import PageNumberPagination from django_filters.rest_framework import DjangoFilterBackend from rest_framework.response import Response from rest_framework.decorators import action from django.utils import timezone +from drf_spectacular.utils import extend_schema, inline_serializer from .models import Integration, EventSubscription, DeliveryLog from .serializers import ( IntegrationSerializer, @@ -37,6 +38,33 @@ class IntegrationViewSet(viewsets.ModelViewSet): serializer = EventSubscriptionSerializer(qs, many=True) return Response(serializer.data) + @extend_schema( + methods=["PUT"], + description=( + "Replace the integration's event subscriptions with the provided list. " + "Accepts a JSON array of subscription objects. " + "Existing subscriptions not in the list will be deleted. " + "The 'payload_template' field is only relevant for webhook integrations." + ), + request=inline_serializer( + name="SetSubscriptionsRequest", + fields={ + "event": serializers.CharField(help_text="Event name (e.g. 'channel_start')."), + "enabled": serializers.BooleanField(required=False, default=True), + "payload_template": serializers.CharField(required=False, allow_blank=True, allow_null=True, help_text="Custom payload template (webhook integrations only)."), + }, + many=True, + ), + responses={200: inline_serializer( + name="SetSubscriptionsResponse", + fields={ + "event": serializers.CharField(), + "enabled": serializers.BooleanField(), + "payload_template": serializers.CharField(allow_null=True), + }, + many=True, + )}, + ) @action(detail=True, methods=["put"], url_path=r"subscriptions/set") def set_subscriptions(self, request, pk=None): """ diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index 9832b5c1..1a53f5ae 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -427,7 +427,13 @@ class EPGImportAPIView(APIView): return [Authenticated()] @extend_schema( - description="Triggers an EPG data import", + description="Triggers an EPG data refresh for the given source.", + request=inline_serializer( + name="EPGImportRequest", + fields={ + "id": serializers.IntegerField(help_text="ID of the EPG source to refresh."), + }, + ), ) def post(self, request, format=None): logger.info("EPGImportAPIView: Received request to import EPG data.") @@ -449,7 +455,7 @@ class EPGImportAPIView(APIView): refresh_epg_data.delay(epg_id) # Trigger Celery task logger.info("EPGImportAPIView: Task dispatched to refresh EPG data.") return Response( - {"success": True, "message": "EPG data import initiated."}, + {"success": True, "message": "EPG data refresh initiated."}, status=status.HTTP_202_ACCEPTED, )