From ae5ba11e39fec55c7e3d2a42750ba169ae48b26c Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Wed, 6 Apr 2022 08:05:03 +0100 Subject: [PATCH] Ensure that viewset actions may be identified as list views is_list_view only checks to see whether the view action is 'list'. This means you cannot define an action with `detail=False` that will generate a schema with a list response. This commit allows specifying 'many=True' on an action to achieve this --- rest_framework/schemas/utils.py | 2 +- rest_framework/viewsets.py | 3 +++ tests/schemas/test_openapi.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas/utils.py b/rest_framework/schemas/utils.py index 60ed69829..848157083 100644 --- a/rest_framework/schemas/utils.py +++ b/rest_framework/schemas/utils.py @@ -15,7 +15,7 @@ def is_list_view(path, method, view): """ if hasattr(view, 'action'): # Viewsets have an explicitly defined action, which we can inspect. - return view.action == 'list' + return view.action == 'list' or view.many if method.lower() != 'get': return False diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 5a1f8acf5..18c8b1e01 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -75,6 +75,9 @@ class ViewSetMixin: # The detail initkwarg is reserved for introspecting the viewset type. cls.detail = None + # The many initkwarg is reserved for introspecting the viewset return type. + cls.many = None + # Setting a basename allows a view to reverse its action urls. This # value is provided by the router through the initkwargs. cls.basename = None diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index daa035a3f..6bc8e76b3 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import filters, generics, pagination, routers, serializers from rest_framework.authtoken.views import obtain_auth_token from rest_framework.compat import uritemplate +from rest_framework.decorators import action from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import ( BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer, @@ -866,6 +867,22 @@ class TestOperationIntrospection(TestCase): assert schema['paths']['/account/{id}/']['patch']['operationId'] == 'partialUpdateExampleViewSet' assert schema['paths']['/account/{id}/']['delete']['operationId'] == 'destroyExampleViewSet' + def test_viewset_with_list_action(self): + router = routers.SimpleRouter() + + class ViewSetWithAction(views.ExampleViewSet): + @action(detail=False, many=True) + def list_action(self, request, *args, **kwargs): + pass + + router.register('account', ViewSetWithAction, basename="account") + urlpatterns = router.urls + + generator = SchemaGenerator(patterns=urlpatterns) + request = create_request('/') + schema = generator.get_schema(request=request) + assert schema['paths']['/account/list_action/']['get']['responses']['200']['content']['application/json']['schema']['type'] == 'array' + def test_serializer_datefield(self): path = '/' method = 'GET'