mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-12-01 14:04:02 +03:00
First stab at new view decorators
This commit is contained in:
parent
886f8b4751
commit
21b1116af5
|
@ -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')
|
||||||
|
|
102
djangorestframework/tests/decorators.py
Normal file
102
djangorestframework/tests/decorators.py
Normal 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])
|
Loading…
Reference in New Issue
Block a user