From df2211913f593ebf32240947024212137c0ce559 Mon Sep 17 00:00:00 2001 From: "T. Franzel" <13507857+tfranzel@users.noreply.github.com> Date: Fri, 8 May 2020 17:56:07 +0200 Subject: [PATCH] explicit override for non-list serializers on ViewSet list #49 --- drf_spectacular/openapi.py | 2 +- drf_spectacular/utils.py | 18 +++++++++++++++++ tests/test_regressions.py | 40 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py index 73e42458..034aae96 100644 --- a/drf_spectacular/openapi.py +++ b/drf_spectacular/openapi.py @@ -806,7 +806,7 @@ def _get_response_for_code(self, serializer): schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = 'Unspecified response body' - if self._is_list_view(serializer): + if self._is_list_view(serializer) and not get_override(serializer, 'many') is False: schema = build_array_type(schema) paginator = self._get_paginator() if paginator: diff --git a/drf_spectacular/utils.py b/drf_spectacular/utils.py index 29ecdeef..11458cab 100644 --- a/drf_spectacular/utils.py +++ b/drf_spectacular/utils.py @@ -200,3 +200,21 @@ def decorator(f): return f return decorator + + +def extend_schema_serializer(many=None): + """ + Decorator for the "serializer" kind. Intended for overriding default serializer behaviour that + cannot be influenced through `.extend_schema`. + + :param many: override how serializer is initialized. Mainly used to coerce the list view detection + heuristic to acknowledge a non-list serializer. + """ + def decorator(klass): + if not hasattr(klass, '_spectacular_annotation'): + klass._spectacular_annotation = {} + if many is not None: + klass._spectacular_annotation['many'] = many + return klass + + return decorator diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 11f4e5fa..56e6a357 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -493,3 +493,43 @@ def get(self, request): assert schema['components']['schemas']['X']['properties']['x']['readOnly'] is True assert 'enum' in schema['components']['schemas']['XEnum'] assert schema['components']['schemas']['XEnum']['type'] == 'integer' + + +def test_viewset_list_with_envelope(no_warnings): + class XSerializer(serializers.Serializer): + x = serializers.IntegerField() + + def enveloper(serializer_class, list): + @extend_schema_serializer(many=False) + class EnvelopeSerializer(serializers.Serializer): + status = serializers.BooleanField() + data = XSerializer(many=list) + + class Meta: + ref_name = 'Enveloped{}{}'.format( + serializer_class.__name__.replace("Serializer", ""), + "List" if list else "", + ) + return EnvelopeSerializer + + class XViewset(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): + @extend_schema(responses=enveloper(XSerializer, True)) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) # pragma: no cover + + @extend_schema( + responses=enveloper(XSerializer, False), + parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)], + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) # pragma: no cover + + schema = generate_schema('x', viewset=XViewset) + + operation_list = schema['paths']['/x/']['get'] + assert operation_list['operationId'] == 'x_list' + assert get_response_schema(operation_list)['$ref'] == '#/components/schemas/EnvelopedXList' + + operation_retrieve = schema['paths']['/x/{id}/']['get'] + assert operation_retrieve['operationId'] == 'x_retrieve' + assert get_response_schema(operation_retrieve)['$ref'] == '#/components/schemas/EnvelopedX'