First stab at new view decorators

This commit is contained in:
Jamie Matthews 2012-09-14 16:07:07 +01:00
parent 886f8b4751
commit 21b1116af5
2 changed files with 186 additions and 30 deletions

View File

@ -1,12 +1,68 @@
from functools import wraps from functools import wraps
from django.http import Http404
from django.utils.decorators import available_attrs from django.utils.decorators import available_attrs
from django.core.exceptions import PermissionDenied from djangorestframework.views import APIView
from djangorestframework import exceptions
from djangorestframework import status
from djangorestframework.response import Response class LazyViewCreator(object):
from djangorestframework.request import Request
from djangorestframework.settings import api_settings """
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): def api_view(allowed_methods):
@ -19,35 +75,33 @@ def api_view(allowed_methods):
# `Response` objects will have .request set automatically # `Response` objects will have .request set automatically
# APIException instances will be handled # APIException instances will be handled
""" """
allowed_methods = [method.upper() for method in allowed_methods]
def decorator(func): def decorator(func):
wrapper = LazyViewCreator.maybe_create(func)
@wraps(func, assigned=available_attrs(func)) @wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs): def handler(self, *args, **kwargs):
try: return func(*args, **kwargs)
request = Request(request) for method in allowed_methods:
wrapper.final_view_attrs[method.lower()] = handler
if request.method not in allowed_methods: return wrapper
raise exceptions.MethodNotAllowed(request.method) return decorator
response = func(request, *args, **kwargs)
if isinstance(response, Response): def _create_attribute_setting_decorator(attribute):
response.request = request def decorator(value):
if api_settings.FORMAT_SUFFIX_KWARG: def inner(func):
response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None) wrapper = LazyViewCreator.maybe_create(func)
return response wrapper.final_view_attrs[attribute] = value
return wrapper
except exceptions.APIException as exc:
return Response({'detail': exc.detail}, status=exc.status_code)
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)
return inner return inner
return decorator 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')

View File

@ -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])