mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-26 19:43:59 +03:00
Moved content negotiation out of response. Nicer exception handling now.
This commit is contained in:
parent
b7b8cd11b1
commit
b3e29d9576
63
djangorestframework/contentnegotiation.py
Normal file
63
djangorestframework/contentnegotiation.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from djangorestframework import exceptions
|
||||
from djangorestframework.settings import api_settings
|
||||
from djangorestframework.utils.mediatypes import order_by_precedence
|
||||
import re
|
||||
|
||||
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
def determine_renderer(self, request, renderers):
|
||||
raise NotImplementedError('.determine_renderer() must be implemented')
|
||||
|
||||
|
||||
class DefaultContentNegotiation(object):
|
||||
settings = api_settings
|
||||
|
||||
def negotiate(self, request, renderers):
|
||||
"""
|
||||
Given a request and a list of renderers, return a two-tuple of:
|
||||
(renderer, media type).
|
||||
"""
|
||||
accepts = self.get_accept_list(request)
|
||||
|
||||
# Check the acceptable media types against each renderer,
|
||||
# attempting more specific media types first
|
||||
# NB. The inner loop here isn't as bad as it first looks :)
|
||||
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
||||
for media_type_set in order_by_precedence(accepts):
|
||||
for renderer in renderers:
|
||||
for media_type in media_type_set:
|
||||
if renderer.can_handle_media_type(media_type):
|
||||
return renderer, media_type
|
||||
|
||||
raise exceptions.NotAcceptable(available_renderers=renderers)
|
||||
|
||||
def get_accept_list(self, request):
|
||||
"""
|
||||
Given the incoming request, return a tokenised list of
|
||||
media type strings.
|
||||
"""
|
||||
if self.settings.URL_ACCEPT_OVERRIDE:
|
||||
# URL style accept override. eg. "?accept=application/json"
|
||||
override = request.GET.get(self.settings.URL_ACCEPT_OVERRIDE)
|
||||
if override:
|
||||
return [override]
|
||||
|
||||
if (self.settings.IGNORE_MSIE_ACCEPT_HEADER and
|
||||
'HTTP_USER_AGENT' in request.META and
|
||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and
|
||||
request.META.get('HTTP_X_REQUESTED_WITH', '').lower() != 'xmlhttprequest'):
|
||||
# Ignore MSIE's broken accept behavior except for AJAX requests
|
||||
# and do something sensible instead
|
||||
return ['text/html', '*/*']
|
||||
|
||||
if 'HTTP_ACCEPT' in request.META:
|
||||
# Standard HTTP Accept negotiation
|
||||
# Accept header specified
|
||||
tokens = request.META['HTTP_ACCEPT'].split(',')
|
||||
return [token.strip() for token in tokens]
|
||||
|
||||
# Standard HTTP Accept negotiation
|
||||
# No accept header specified
|
||||
return ['*/*']
|
|
@ -39,6 +39,15 @@ class MethodNotAllowed(APIException):
|
|||
self.detail = (detail or self.default_detail) % method
|
||||
|
||||
|
||||
class NotAcceptable(APIException):
|
||||
status_code = status.HTTP_406_NOT_ACCEPTABLE
|
||||
default_detail = "Could not satisfy the request's Accept header"
|
||||
|
||||
def __init__(self, detail=None, available_renderers=None):
|
||||
self.detail = detail or self.default_detail
|
||||
self.available_renderers = available_renderers
|
||||
|
||||
|
||||
class UnsupportedMediaType(APIException):
|
||||
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
default_detail = "Unsupported media type '%s' in request."
|
||||
|
|
|
@ -48,28 +48,22 @@ class BaseRenderer(object):
|
|||
def __init__(self, view=None):
|
||||
self.view = view
|
||||
|
||||
def can_handle_response(self, accept):
|
||||
"""
|
||||
Returns :const:`True` if this renderer is able to deal with the given
|
||||
*accept* media type.
|
||||
def can_handle_format(self, format):
|
||||
return format == self.format
|
||||
|
||||
The default implementation for this function is to check the *accept*
|
||||
argument against the :attr:`media_type` attribute set on the class to see if
|
||||
def can_handle_media_type(self, media_type):
|
||||
"""
|
||||
Returns `True` if this renderer is able to deal with the given
|
||||
media type.
|
||||
|
||||
The default implementation for this function is to check the media type
|
||||
argument against the media_type attribute set on the class to see if
|
||||
they match.
|
||||
|
||||
This may be overridden to provide for other behavior, but typically you'll
|
||||
instead want to just set the :attr:`media_type` attribute on the class.
|
||||
This may be overridden to provide for other behavior, but typically
|
||||
you'll instead want to just set the `media_type` attribute on the class.
|
||||
"""
|
||||
# TODO: format overriding must go out of here
|
||||
format = None
|
||||
if self.view is not None:
|
||||
format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None)
|
||||
if format is None and self.view is not None:
|
||||
format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None)
|
||||
|
||||
if format is not None:
|
||||
return format == self.format
|
||||
return media_type_matches(self.media_type, accept)
|
||||
return media_type_matches(self.media_type, media_type)
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
|
|
|
@ -1,97 +1,34 @@
|
|||
"""
|
||||
The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes.
|
||||
|
||||
`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned
|
||||
from any view. It is a bit smarter than Django's `HttpResponse`, for it renders automatically
|
||||
its content to a serial format by using a list of :mod:`renderers`.
|
||||
|
||||
To determine the content type to which it must render, default behaviour is to use standard
|
||||
HTTP Accept header content negotiation. But `Response` also supports overriding the content type
|
||||
by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers
|
||||
from Internet Explorer user agents and use a sensible browser `Accept` header instead.
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
from django.template.response import SimpleTemplateResponse
|
||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||
from djangorestframework.settings import api_settings
|
||||
from djangorestframework.utils.mediatypes import order_by_precedence
|
||||
from djangorestframework import status
|
||||
|
||||
|
||||
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||
|
||||
|
||||
class NotAcceptable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Response(SimpleTemplateResponse):
|
||||
"""
|
||||
An HttpResponse that may include content that hasn't yet been serialized.
|
||||
|
||||
Kwargs:
|
||||
- content(object). The raw content, not yet serialized.
|
||||
This must be native Python data that renderers can handle.
|
||||
(e.g.: `dict`, `str`, ...)
|
||||
- renderer_classes(list/tuple). The renderers to use for rendering the response content.
|
||||
An HttpResponse that allows it's data to be rendered into
|
||||
arbitrary media types.
|
||||
"""
|
||||
|
||||
_ACCEPT_QUERY_PARAM = api_settings.URL_ACCEPT_OVERRIDE
|
||||
_IGNORE_IE_ACCEPT_HEADER = True
|
||||
def __init__(self, data=None, status=None, headers=None,
|
||||
renderer=None, media_type=None):
|
||||
"""
|
||||
Alters the init arguments slightly.
|
||||
For example, drop 'template_name', and instead use 'data'.
|
||||
|
||||
def __init__(self, content=None, status=None, headers=None, view=None,
|
||||
request=None, renderer_classes=None, format=None):
|
||||
# First argument taken by `SimpleTemplateResponse.__init__` is template_name,
|
||||
# which we don't need
|
||||
Setting 'renderer' and 'media_type' will typically be defered,
|
||||
For example being set automatically by the `APIView`.
|
||||
"""
|
||||
super(Response, self).__init__(None, status=status)
|
||||
|
||||
self.raw_content = content
|
||||
self.has_content_body = content is not None
|
||||
self.data = data
|
||||
self.headers = headers and headers[:] or []
|
||||
self.view = view
|
||||
self.request = request
|
||||
self.renderer_classes = renderer_classes
|
||||
self.format = format
|
||||
|
||||
def get_renderers(self):
|
||||
"""
|
||||
Instantiates and returns the list of renderers the response will use.
|
||||
"""
|
||||
if self.renderer_classes is None:
|
||||
renderer_classes = api_settings.DEFAULT_RENDERERS
|
||||
else:
|
||||
renderer_classes = self.renderer_classes
|
||||
|
||||
if self.format:
|
||||
return [cls(self.view) for cls in renderer_classes
|
||||
if cls.format == self.format]
|
||||
return [cls(self.view) for cls in renderer_classes]
|
||||
self.renderer = renderer
|
||||
self.media_type = media_type
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
"""
|
||||
The final rendered content. Accessing this attribute triggers the
|
||||
complete rendering cycle: selecting suitable renderer, setting
|
||||
response's actual content type, rendering data.
|
||||
"""
|
||||
renderer, media_type = self._determine_renderer()
|
||||
|
||||
# Set the media type of the response
|
||||
self['Content-Type'] = renderer.media_type
|
||||
|
||||
# Render the response content
|
||||
if self.has_content_body:
|
||||
return renderer.render(self.raw_content, media_type)
|
||||
return renderer.render()
|
||||
|
||||
def render(self):
|
||||
try:
|
||||
return super(Response, self).render()
|
||||
except NotAcceptable:
|
||||
response = self._get_406_response()
|
||||
return response.render()
|
||||
self['Content-Type'] = self.media_type
|
||||
if self.data is None:
|
||||
return self.renderer.render()
|
||||
return self.renderer.render(self.data, self.media_type)
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
|
@ -100,74 +37,3 @@ class Response(SimpleTemplateResponse):
|
|||
Provided for convenience.
|
||||
"""
|
||||
return STATUS_CODE_TEXT.get(self.status_code, '')
|
||||
|
||||
def _determine_accept_list(self):
|
||||
"""
|
||||
Returns a list of accepted media types. This list is determined from :
|
||||
|
||||
1. overload with `_ACCEPT_QUERY_PARAM`
|
||||
2. `Accept` header of the request
|
||||
|
||||
If those are useless, a default value is returned instead.
|
||||
"""
|
||||
request = self.request
|
||||
|
||||
if (self._ACCEPT_QUERY_PARAM and
|
||||
request.GET.get(self._ACCEPT_QUERY_PARAM, None)):
|
||||
# Use _accept parameter override
|
||||
return [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
||||
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
||||
'HTTP_USER_AGENT' in request.META and
|
||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and
|
||||
request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'):
|
||||
# Ignore MSIE's broken accept behavior except for AJAX requests
|
||||
# and do something sensible instead
|
||||
return ['text/html', '*/*']
|
||||
elif 'HTTP_ACCEPT' in request.META:
|
||||
# Use standard HTTP Accept negotiation
|
||||
return [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')]
|
||||
else:
|
||||
# No accept header specified
|
||||
return ['*/*']
|
||||
|
||||
def _determine_renderer(self):
|
||||
"""
|
||||
Determines the appropriate renderer for the output, given the list of
|
||||
accepted media types, and the :attr:`renderer_classes` set on this class.
|
||||
|
||||
Returns a 2-tuple of `(renderer, media_type)`
|
||||
|
||||
See: RFC 2616, Section 14
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
"""
|
||||
|
||||
renderers = self.get_renderers()
|
||||
accepts = self._determine_accept_list()
|
||||
|
||||
# Not acceptable response - Ignore accept header.
|
||||
if self.status_code == 406:
|
||||
return (renderers[0], renderers[0].media_type)
|
||||
|
||||
# Check the acceptable media types against each renderer,
|
||||
# attempting more specific media types first
|
||||
# NB. The inner loop here isn't as bad as it first looks :)
|
||||
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
||||
for media_type_set in order_by_precedence(accepts):
|
||||
for renderer in renderers:
|
||||
for media_type in media_type_set:
|
||||
if renderer.can_handle_response(media_type):
|
||||
return renderer, media_type
|
||||
|
||||
# No acceptable renderers were found
|
||||
raise NotAcceptable
|
||||
|
||||
def _get_406_response(self):
|
||||
renderer = self.renderer_classes[0]
|
||||
return Response(
|
||||
{
|
||||
'detail': 'Could not satisfy the client\'s Accept header',
|
||||
'available_types': [renderer.media_type
|
||||
for renderer in self.renderer_classes]
|
||||
},
|
||||
status=status.HTTP_406_NOT_ACCEPTABLE,
|
||||
view=self.view, request=self.request, renderer_classes=[renderer])
|
||||
|
|
|
@ -38,6 +38,7 @@ DEFAULTS = {
|
|||
),
|
||||
'DEFAULT_PERMISSIONS': (),
|
||||
'DEFAULT_THROTTLES': (),
|
||||
'DEFAULT_CONTENT_NEGOTIATION': 'djangorestframework.contentnegotiation.DefaultContentNegotiation',
|
||||
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
@ -45,7 +46,8 @@ DEFAULTS = {
|
|||
'FORM_METHOD_OVERRIDE': '_method',
|
||||
'FORM_CONTENT_OVERRIDE': '_content',
|
||||
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
|
||||
'URL_ACCEPT_OVERRIDE': '_accept',
|
||||
'URL_ACCEPT_OVERRIDE': 'accept',
|
||||
'IGNORE_MSIE_ACCEPT_HEADER': True,
|
||||
|
||||
'FORMAT_SUFFIX_KWARG': 'format'
|
||||
}
|
||||
|
@ -58,8 +60,9 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_AUTHENTICATION',
|
||||
'DEFAULT_PERMISSIONS',
|
||||
'DEFAULT_THROTTLES',
|
||||
'DEFAULT_CONTENT_NEGOTIATION',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN'
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
)
|
||||
|
||||
|
||||
|
@ -68,7 +71,7 @@ def perform_import(val, setting):
|
|||
If the given setting is a string import notation,
|
||||
then perform the necessary import or imports.
|
||||
"""
|
||||
if val is None or setting not in IMPORT_STRINGS:
|
||||
if val is None or not setting in IMPORT_STRINGS:
|
||||
return val
|
||||
|
||||
if isinstance(val, basestring):
|
||||
|
@ -88,10 +91,7 @@ def import_from_string(val, setting):
|
|||
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
import pdb; pdb.set_trace()
|
||||
except:
|
||||
msg = "Could not import '%s' for API setting '%s'" % (val, setting)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.test import TestCase, Client
|
|||
|
||||
from djangorestframework import status
|
||||
from djangorestframework.authentication import SessionAuthentication
|
||||
from djangorestframework.utils import RequestFactory
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.parsers import (
|
||||
FormParser,
|
||||
MultiPartParser,
|
||||
|
@ -22,33 +22,21 @@ factory = RequestFactory()
|
|||
|
||||
|
||||
class TestMethodOverloading(TestCase):
|
||||
def test_GET_method(self):
|
||||
def test_method(self):
|
||||
"""
|
||||
GET requests identified.
|
||||
Request methods should be same as underlying request.
|
||||
"""
|
||||
request = factory.get('/')
|
||||
request = Request(factory.get('/'))
|
||||
self.assertEqual(request.method, 'GET')
|
||||
|
||||
def test_POST_method(self):
|
||||
"""
|
||||
POST requests identified.
|
||||
"""
|
||||
request = factory.post('/')
|
||||
request = Request(factory.post('/'))
|
||||
self.assertEqual(request.method, 'POST')
|
||||
|
||||
def test_HEAD_method(self):
|
||||
"""
|
||||
HEAD requests identified.
|
||||
"""
|
||||
request = factory.head('/')
|
||||
self.assertEqual(request.method, 'HEAD')
|
||||
|
||||
def test_overloaded_method(self):
|
||||
"""
|
||||
POST requests can be overloaded to another method by setting a
|
||||
reserved form field
|
||||
"""
|
||||
request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'})
|
||||
request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'}))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
|
||||
|
@ -57,14 +45,14 @@ class TestContentParsing(TestCase):
|
|||
"""
|
||||
Ensure request.DATA returns None for GET request with no content.
|
||||
"""
|
||||
request = factory.get('/')
|
||||
request = Request(factory.get('/'))
|
||||
self.assertEqual(request.DATA, None)
|
||||
|
||||
def test_standard_behaviour_determines_no_content_HEAD(self):
|
||||
"""
|
||||
Ensure request.DATA returns None for HEAD request.
|
||||
"""
|
||||
request = factory.head('/')
|
||||
request = Request(factory.head('/'))
|
||||
self.assertEqual(request.DATA, None)
|
||||
|
||||
def test_standard_behaviour_determines_form_content_POST(self):
|
||||
|
@ -72,8 +60,8 @@ class TestContentParsing(TestCase):
|
|||
Ensure request.DATA returns content for POST request with form content.
|
||||
"""
|
||||
data = {'qwerty': 'uiop'}
|
||||
parsers = (FormParser, MultiPartParser)
|
||||
request = factory.post('/', data, parser=parsers)
|
||||
request = Request(factory.post('/', data))
|
||||
request.parser_classes = (FormParser, MultiPartParser)
|
||||
self.assertEqual(request.DATA.items(), data.items())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||
|
@ -83,9 +71,8 @@ class TestContentParsing(TestCase):
|
|||
"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
parsers = (PlainTextParser,)
|
||||
request = factory.post('/', content, content_type=content_type,
|
||||
parsers=parsers)
|
||||
request = Request(factory.post('/', content, content_type=content_type))
|
||||
request.parser_classes = (PlainTextParser,)
|
||||
self.assertEqual(request.DATA, content)
|
||||
|
||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
|
@ -93,17 +80,17 @@ class TestContentParsing(TestCase):
|
|||
Ensure request.DATA returns content for PUT request with form content.
|
||||
"""
|
||||
data = {'qwerty': 'uiop'}
|
||||
parsers = (FormParser, MultiPartParser)
|
||||
|
||||
from django import VERSION
|
||||
|
||||
if VERSION >= (1, 5):
|
||||
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
|
||||
request = factory.put('/', encode_multipart(BOUNDARY, data), parsers=parsers,
|
||||
content_type=MULTIPART_CONTENT)
|
||||
request = Request(factory.put('/', encode_multipart(BOUNDARY, data),
|
||||
content_type=MULTIPART_CONTENT))
|
||||
else:
|
||||
request = factory.put('/', data, parsers=parsers)
|
||||
request = Request(factory.put('/', data))
|
||||
|
||||
request.parser_classes = (FormParser, MultiPartParser)
|
||||
self.assertEqual(request.DATA.items(), data.items())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||
|
@ -113,9 +100,8 @@ class TestContentParsing(TestCase):
|
|||
"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
parsers = (PlainTextParser, )
|
||||
request = factory.put('/', content, content_type=content_type,
|
||||
parsers=parsers)
|
||||
request = Request(factory.put('/', content, content_type=content_type))
|
||||
request.parser_classes = (PlainTextParser, )
|
||||
self.assertEqual(request.DATA, content)
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
|
@ -128,8 +114,8 @@ class TestContentParsing(TestCase):
|
|||
Request._CONTENT_PARAM: content,
|
||||
Request._CONTENTTYPE_PARAM: content_type
|
||||
}
|
||||
parsers = (PlainTextParser, )
|
||||
request = factory.post('/', data, parsers=parsers)
|
||||
request = Request(factory.post('/', data))
|
||||
request.parser_classes = (PlainTextParser, )
|
||||
self.assertEqual(request.DATA, content)
|
||||
|
||||
# def test_accessing_post_after_data_form(self):
|
||||
|
|
|
@ -4,10 +4,10 @@ import unittest
|
|||
from django.conf.urls.defaults import patterns, url, include
|
||||
from django.test import TestCase
|
||||
|
||||
from djangorestframework.response import Response, NotAcceptable
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework.views import APIView
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework import status
|
||||
from djangorestframework import status, exceptions
|
||||
from djangorestframework.renderers import (
|
||||
BaseRenderer,
|
||||
JSONRenderer,
|
||||
|
@ -91,7 +91,7 @@ class TestResponseDetermineRenderer(TestCase):
|
|||
accept_list = ['application/json']
|
||||
renderer_classes = (MockPickleRenderer, )
|
||||
response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes)
|
||||
self.assertRaises(NotAcceptable, response._determine_renderer)
|
||||
self.assertRaises(exceptions.NotAcceptable, response._determine_renderer)
|
||||
|
||||
|
||||
class TestResponseRenderContent(TestCase):
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
|
||||
from djangorestframework.compat import StringIO
|
||||
from djangorestframework.compat import RequestFactory as DjangoRequestFactory
|
||||
from djangorestframework.request import Request
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
@ -102,38 +99,3 @@ class XMLRenderer():
|
|||
|
||||
def dict2xml(input):
|
||||
return XMLRenderer().dict2xml(input)
|
||||
|
||||
|
||||
class RequestFactory(DjangoRequestFactory):
|
||||
"""
|
||||
Replicate RequestFactory, but return Request, not HttpRequest.
|
||||
"""
|
||||
def get(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).get(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).post(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).put(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).delete(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).head(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
parsers = kwargs.pop('parsers', None)
|
||||
request = super(RequestFactory, self).options(*args, **kwargs)
|
||||
return Request(request, parsers)
|
||||
|
|
|
@ -54,11 +54,14 @@ def _camelcase_to_spaces(content):
|
|||
|
||||
|
||||
class APIView(_View):
|
||||
settings = api_settings
|
||||
|
||||
renderer_classes = api_settings.DEFAULT_RENDERERS
|
||||
parser_classes = api_settings.DEFAULT_PARSERS
|
||||
authentication_classes = api_settings.DEFAULT_AUTHENTICATION
|
||||
throttle_classes = api_settings.DEFAULT_THROTTLES
|
||||
permission_classes = api_settings.DEFAULT_PERMISSIONS
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
|
@ -169,6 +172,27 @@ class APIView(_View):
|
|||
"""
|
||||
return self.renderer_classes[0]
|
||||
|
||||
def get_format_suffix(self, **kwargs):
|
||||
"""
|
||||
Determine if the request includes a '.json' style format suffix
|
||||
"""
|
||||
if self.settings.FORMAT_SUFFIX_KWARG:
|
||||
return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG)
|
||||
|
||||
def get_renderers(self, format=None):
|
||||
"""
|
||||
Instantiates and returns the list of renderers that this view can use.
|
||||
"""
|
||||
return [renderer(self) for renderer in self.renderer_classes]
|
||||
|
||||
def filter_renderers(self, renderers, format=None):
|
||||
"""
|
||||
If format suffix such as '.json' is supplied, filter the
|
||||
list of valid renderers for this request.
|
||||
"""
|
||||
return [renderer for renderer in renderers
|
||||
if renderer.can_handle_format(format)]
|
||||
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Instantiates and returns the list of permissions that this view requires.
|
||||
|
@ -177,10 +201,28 @@ class APIView(_View):
|
|||
|
||||
def get_throttles(self):
|
||||
"""
|
||||
Instantiates and returns the list of thottles that this view requires.
|
||||
Instantiates and returns the list of thottles that this view uses.
|
||||
"""
|
||||
return [throttle(self) for throttle in self.throttle_classes]
|
||||
|
||||
def content_negotiation(self, request):
|
||||
"""
|
||||
Determine which renderer and media type to use render the response.
|
||||
"""
|
||||
renderers = self.get_renderers()
|
||||
|
||||
if self.format:
|
||||
# If there is a '.json' style format suffix, only use
|
||||
# renderers that accept that format.
|
||||
fallback = renderers[0]
|
||||
renderers = self.filter_renderers(renderers, self.format)
|
||||
if not renderers:
|
||||
self.format404 = True
|
||||
return (fallback, fallback.media_type)
|
||||
|
||||
conneg = self.content_negotiation_class()
|
||||
return conneg.negotiate(request, renderers)
|
||||
|
||||
def check_permissions(self, request, obj=None):
|
||||
"""
|
||||
Check if request should be permitted.
|
||||
|
@ -204,35 +246,43 @@ class APIView(_View):
|
|||
return Request(request, parser_classes=self.parser_classes,
|
||||
authentication_classes=self.authentication_classes)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Runs anything that needs to occur prior to calling the method handlers.
|
||||
"""
|
||||
self.format = self.get_format_suffix(**kwargs)
|
||||
self.renderer, self.media_type = self.content_negotiation(request)
|
||||
self.check_permissions(request)
|
||||
self.check_throttles(request)
|
||||
# If the request included a non-existant .format URL suffix,
|
||||
# raise 404, but only after first making permission checks.
|
||||
if getattr(self, 'format404', None):
|
||||
raise Http404()
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
"""
|
||||
Returns the final response object.
|
||||
"""
|
||||
if isinstance(response, Response):
|
||||
response.view = self
|
||||
response.request = request
|
||||
response.renderer_classes = self.renderer_classes
|
||||
if api_settings.FORMAT_SUFFIX_KWARG:
|
||||
response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None)
|
||||
response.renderer = self.renderer
|
||||
response.media_type = self.media_type
|
||||
|
||||
for key, value in self.headers.items():
|
||||
response[key] = value
|
||||
|
||||
return response
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Runs anything that needs to occur prior to calling the method handlers.
|
||||
"""
|
||||
self.check_permissions(request)
|
||||
self.check_throttles(request)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
"""
|
||||
Handle any exception that occurs, by returning an appropriate response,
|
||||
or re-raising the error.
|
||||
"""
|
||||
if isinstance(exc, exceptions.Throttled):
|
||||
if isinstance(exc, exceptions.NotAcceptable):
|
||||
# Fall back to default renderer
|
||||
self.renderer = exc.available_renderers[0]
|
||||
self.media_type = exc.available_renderers[0].media_type
|
||||
elif isinstance(exc, exceptions.Throttled):
|
||||
# Throttle wait header
|
||||
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
|
@ -250,14 +300,8 @@ class APIView(_View):
|
|||
@csrf_exempt
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
`APIView.dispatch()` is pretty much the same as Django's regular
|
||||
`View.dispatch()`, except that it includes hooks to:
|
||||
|
||||
* Initialize the request object.
|
||||
* Finalize the response object.
|
||||
* Handle exceptions that occur in the handler method.
|
||||
* An initial hook for code such as permission checking that should
|
||||
occur prior to running the method handlers.
|
||||
`.dispatch()` is pretty much the same as Django's regular dispatch,
|
||||
but with extra hooks for startup, finalize, and exception handling.
|
||||
"""
|
||||
request = self.initialize_request(request, *args, **kwargs)
|
||||
self.request = request
|
||||
|
@ -270,7 +314,8 @@ class APIView(_View):
|
|||
|
||||
# Get the appropriate handler method
|
||||
if request.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
|
||||
handler = getattr(self, request.method.lower(),
|
||||
self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user