diff options
| author | Jamie Matthews | 2012-09-14 16:07:07 +0100 |
|---|---|---|
| committer | Jamie Matthews | 2012-09-14 16:07:07 +0100 |
| commit | 21b1116af568321227b1ce32fb15bc29ba0d8a62 (patch) | |
| tree | 542cf27b43eed0e29d270b526d572cb2cd2cb946 | |
| parent | 886f8b47510c830483b5adae1855593cdc3df2dc (diff) | |
| download | django-rest-framework-21b1116af568321227b1ce32fb15bc29ba0d8a62.tar.bz2 | |
First stab at new view decorators
| -rw-r--r-- | djangorestframework/decorators.py | 114 | ||||
| -rw-r--r-- | djangorestframework/tests/decorators.py | 102 |
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]) |
