aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJamie Matthews2012-09-14 16:07:07 +0100
committerJamie Matthews2012-09-14 16:07:07 +0100
commit21b1116af568321227b1ce32fb15bc29ba0d8a62 (patch)
tree542cf27b43eed0e29d270b526d572cb2cd2cb946
parent886f8b47510c830483b5adae1855593cdc3df2dc (diff)
downloaddjango-rest-framework-21b1116af568321227b1ce32fb15bc29ba0d8a62.tar.bz2
First stab at new view decorators
-rw-r--r--djangorestframework/decorators.py114
-rw-r--r--djangorestframework/tests/decorators.py102
2 files changed, 186 insertions, 30 deletions
diff --git a/djangorestframework/decorators.py b/djangorestframework/decorators.py
index 814f321a..22bb8d3e 100644
--- a/djangorestframework/decorators.py
+++ b/djangorestframework/decorators.py
@@ -1,12 +1,68 @@
from functools import wraps
-from django.http import Http404
from django.utils.decorators import available_attrs
-from django.core.exceptions import PermissionDenied
-from djangorestframework import exceptions
-from djangorestframework import status
-from djangorestframework.response import Response
-from djangorestframework.request import Request
-from djangorestframework.settings import api_settings
+from djangorestframework.views import APIView
+
+
+class LazyViewCreator(object):
+
+ """
+ This class is responsible for dynamically creating an APIView subclass that
+ will wrap a function-based view. Instances of this class are created
+ by the function-based view decorators (below), and each decorator is
+ responsible for setting attributes on the instance that will eventually be
+ copied onto the final class-based view. The CBV gets created lazily the first
+ time it's needed, and then cached for future use.
+
+ This is done so that the ordering of stacked decorators is irrelevant.
+ """
+
+ def __init__(self):
+
+ # Each item in this dictionary will be copied onto the final
+ # class-based view that gets created when this object is called
+ self.final_view_attrs = {
+ 'renderer_classes': APIView.renderer_classes,
+ 'parser_classes': APIView.parser_classes,
+ 'authentication_classes': APIView.authentication_classes,
+ 'throttle_classes': APIView.throttle_classes,
+ 'permission_classes': APIView.permission_classes,
+ }
+ self._cached_view = None
+
+ @property
+ def view(self):
+ """
+ Accessor for the dynamically created class-based view. This will
+ be created if necessary and cached for next time.
+ """
+
+ if self._cached_view is None:
+
+ class WrappedAPIView(APIView):
+ pass
+
+ for attr, value in self.final_view_attrs.items():
+ setattr(WrappedAPIView, attr, value)
+
+ self._cached_view = WrappedAPIView.as_view()
+
+ return self._cached_view
+
+ def __call__(self, *args, **kwargs):
+ """
+ This is the actual code that gets run per-request
+ """
+ return self.view(*args, **kwargs)
+
+ @staticmethod
+ def maybe_create(func):
+ """
+ If the argument is already an instance of LazyViewCreator,
+ just return it. Otherwise, create a new one.
+ """
+ if isinstance(func, LazyViewCreator):
+ return func
+ return LazyViewCreator()
def api_view(allowed_methods):
@@ -19,35 +75,33 @@ def api_view(allowed_methods):
# `Response` objects will have .request set automatically
# APIException instances will be handled
"""
- allowed_methods = [method.upper() for method in allowed_methods]
def decorator(func):
- @wraps(func, assigned=available_attrs(func))
- def inner(request, *args, **kwargs):
- try:
-
- request = Request(request)
+ wrapper = LazyViewCreator.maybe_create(func)
- if request.method not in allowed_methods:
- raise exceptions.MethodNotAllowed(request.method)
-
- response = func(request, *args, **kwargs)
+ @wraps(func, assigned=available_attrs(func))
+ def handler(self, *args, **kwargs):
+ return func(*args, **kwargs)
- if isinstance(response, Response):
- response.request = request
- if api_settings.FORMAT_SUFFIX_KWARG:
- response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None)
- return response
+ for method in allowed_methods:
+ wrapper.final_view_attrs[method.lower()] = handler
- except exceptions.APIException as exc:
- return Response({'detail': exc.detail}, status=exc.status_code)
+ return wrapper
+ return decorator
- except Http404 as exc:
- return Response({'detail': 'Not found'},
- status=status.HTTP_404_NOT_FOUND)
- except PermissionDenied as exc:
- return Response({'detail': 'Permission denied'},
- status=status.HTTP_403_FORBIDDEN)
+def _create_attribute_setting_decorator(attribute):
+ def decorator(value):
+ def inner(func):
+ wrapper = LazyViewCreator.maybe_create(func)
+ wrapper.final_view_attrs[attribute] = value
+ return wrapper
return inner
return decorator
+
+
+renderer_classes = _create_attribute_setting_decorator('renderer_classes')
+parser_classes = _create_attribute_setting_decorator('parser_classes')
+authentication_classes = _create_attribute_setting_decorator('authentication_classes')
+throttle_classes = _create_attribute_setting_decorator('throttle_classes')
+permission_classes = _create_attribute_setting_decorator('permission_classes')
diff --git a/djangorestframework/tests/decorators.py b/djangorestframework/tests/decorators.py
new file mode 100644
index 00000000..0d3be8f3
--- /dev/null
+++ b/djangorestframework/tests/decorators.py
@@ -0,0 +1,102 @@
+from django.test import TestCase
+from djangorestframework.response import Response
+from djangorestframework.compat import RequestFactory
+from djangorestframework.renderers import JSONRenderer
+from djangorestframework.parsers import JSONParser
+from djangorestframework.authentication import BasicAuthentication
+from djangorestframework.throttling import SimpleRateThottle
+from djangorestframework.permissions import IsAuthenticated
+from djangorestframework.decorators import (
+ api_view,
+ renderer_classes,
+ parser_classes,
+ authentication_classes,
+ throttle_classes,
+ permission_classes,
+ LazyViewCreator
+)
+
+
+class DecoratorTestCase(TestCase):
+
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_wrap_view(self):
+
+ @api_view(['GET'])
+ def view(request):
+ return Response({})
+
+ self.assertTrue(isinstance(view, LazyViewCreator))
+
+ def test_calling_method(self):
+
+ @api_view(['GET'])
+ def view(request):
+ return Response({})
+
+ request = self.factory.get('/')
+ response = view(request)
+ self.assertEqual(response.status_code, 200)
+
+ request = self.factory.post('/')
+ response = view(request)
+ self.assertEqual(response.status_code, 405)
+
+ def test_renderer_classes(self):
+
+ @renderer_classes([JSONRenderer])
+ @api_view(['GET'])
+ def view(request):
+ return Response({})
+
+ request = self.factory.get('/')
+ response = view(request)
+ self.assertEqual(response.renderer_classes, [JSONRenderer])
+
+ def test_parser_classes(self):
+
+ @parser_classes([JSONParser])
+ @api_view(['GET'])
+ def view(request):
+ return Response({})
+
+ request = self.factory.get('/')
+ response = view(request)
+ self.assertEqual(response.request.parser_classes, [JSONParser])
+
+ def test_authentication_classes(self):
+
+ @authentication_classes([BasicAuthentication])
+ @api_view(['GET'])
+ def view(request):
+ return Response({})
+
+ request = self.factory.get('/')
+ response = view(request)
+ self.assertEqual(response.request.authentication_classes, [BasicAuthentication])
+
+# Doesn't look like these bits are working quite yet
+
+# def test_throttle_classes(self):
+#
+# @throttle_classes([SimpleRateThottle])
+# @api_view(['GET'])
+# def view(request):
+# return Response({})
+#
+# request = self.factory.get('/')
+# response = view(request)
+# self.assertEqual(response.request.throttle, [SimpleRateThottle])
+
+# def test_permission_classes(self):
+
+# @permission_classes([IsAuthenticated])
+# @api_view(['GET'])
+# def view(request):
+# return Response({})
+
+# request = self.factory.get('/')
+# response = view(request)
+# self.assertEqual(response.request.permission_classes, [IsAuthenticated])