diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 93e0751b7a..a69a613ba4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -87,8 +87,26 @@ def handler(self, *args, **kwargs): return decorator +def _check_decorator_order(func, decorator_name): + """ + Check if an API policy decorator is being applied after @api_view. + """ + # Check if func is actually a view function (result of APIView.as_view()) + if hasattr(func, 'cls') and issubclass(func.cls, APIView): + raise TypeError( + f"@{decorator_name} must come after (below) the @api_view decorator. " + "The correct order is:\n\n" + " @api_view(['GET'])\n" + f" @{decorator_name}(...)\n" + " def my_view(request):\n" + " ...\n\n" + "See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators" + ) + + def renderer_classes(renderer_classes): def decorator(func): + _check_decorator_order(func, 'renderer_classes') func.renderer_classes = renderer_classes return func return decorator @@ -96,6 +114,7 @@ def decorator(func): def parser_classes(parser_classes): def decorator(func): + _check_decorator_order(func, 'parser_classes') func.parser_classes = parser_classes return func return decorator @@ -103,6 +122,7 @@ def decorator(func): def authentication_classes(authentication_classes): def decorator(func): + _check_decorator_order(func, 'authentication_classes') func.authentication_classes = authentication_classes return func return decorator @@ -110,6 +130,7 @@ def decorator(func): def throttle_classes(throttle_classes): def decorator(func): + _check_decorator_order(func, 'throttle_classes') func.throttle_classes = throttle_classes return func return decorator @@ -117,6 +138,7 @@ def decorator(func): def permission_classes(permission_classes): def decorator(func): + _check_decorator_order(func, 'permission_classes') func.permission_classes = permission_classes return func return decorator @@ -124,6 +146,7 @@ def decorator(func): def content_negotiation_class(content_negotiation_class): def decorator(func): + _check_decorator_order(func, 'content_negotiation_class') func.content_negotiation_class = content_negotiation_class return func return decorator @@ -131,6 +154,7 @@ def decorator(func): def metadata_class(metadata_class): def decorator(func): + _check_decorator_order(func, 'metadata_class') func.metadata_class = metadata_class return func return decorator @@ -138,6 +162,7 @@ def decorator(func): def versioning_class(versioning_class): def decorator(func): + _check_decorator_order(func, 'versioning_class') func.versioning_class = versioning_class return func return decorator @@ -145,6 +170,7 @@ def decorator(func): def schema(view_inspector): def decorator(func): + _check_decorator_order(func, 'schema') func.schema = view_inspector return func return decorator diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 0c070bc10b..cc7cab4d7d 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -204,6 +204,124 @@ def view(request): assert isinstance(view.cls.schema, CustomSchema) + def test_incorrect_decorator_order_permission_classes(self): + """ + If @permission_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @permission_classes([IsAuthenticated]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@permission_classes must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_renderer_classes(self): + """ + If @renderer_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @renderer_classes([JSONRenderer]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@renderer_classes must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_parser_classes(self): + """ + If @parser_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @parser_classes([JSONParser]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@parser_classes must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_authentication_classes(self): + """ + If @authentication_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @authentication_classes([BasicAuthentication]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@authentication_classes must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_throttle_classes(self): + """ + If @throttle_classes is applied after @api_view, we should raise a TypeError. + """ + class OncePerDayUserThrottle(UserRateThrottle): + rate = '1/day' + + with self.assertRaises(TypeError) as cm: + @throttle_classes([OncePerDayUserThrottle]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@throttle_classes must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_versioning_class(self): + """ + If @versioning_class is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @versioning_class(QueryParameterVersioning) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@versioning_class must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_metadata_class(self): + """ + If @metadata_class is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @metadata_class(None) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@metadata_class must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_content_negotiation_class(self): + """ + If @content_negotiation_class is applied after @api_view, we should raise a TypeError. + """ + class CustomContentNegotiation(BaseContentNegotiation): + def select_renderer(self, request, renderers, format_suffix): + return (renderers[0], renderers[0].media_type) + + with self.assertRaises(TypeError) as cm: + @content_negotiation_class(CustomContentNegotiation) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@content_negotiation_class must come after (below) the @api_view decorator' in str(cm.exception) + + def test_incorrect_decorator_order_schema(self): + """ + If @schema is applied after @api_view, we should raise a TypeError. + """ + class CustomSchema(AutoSchema): + pass + + with self.assertRaises(TypeError) as cm: + @schema(CustomSchema()) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@schema must come after (below) the @api_view decorator' in str(cm.exception) + class ActionDecoratorTestCase(TestCase):