Moved content negotiation out of response. Nicer exception handling now.

This commit is contained in:
Tom Christie 2012-09-14 22:42:29 +01:00
parent b7b8cd11b1
commit b3e29d9576
9 changed files with 199 additions and 274 deletions

View 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 ['*/*']

View File

@ -39,6 +39,15 @@ class MethodNotAllowed(APIException):
self.detail = (detail or self.default_detail) % method 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): class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = "Unsupported media type '%s' in request." default_detail = "Unsupported media type '%s' in request."

View File

@ -48,28 +48,22 @@ class BaseRenderer(object):
def __init__(self, view=None): def __init__(self, view=None):
self.view = view self.view = view
def can_handle_response(self, accept): def can_handle_format(self, format):
""" return format == self.format
Returns :const:`True` if this renderer is able to deal with the given
*accept* media type.
The default implementation for this function is to check the *accept* def can_handle_media_type(self, media_type):
argument against the :attr:`media_type` attribute set on the class to see if """
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. they match.
This may be overridden to provide for other behavior, but typically you'll This may be overridden to provide for other behavior, but typically
instead want to just set the :attr:`media_type` attribute on the class. you'll instead want to just set the `media_type` attribute on the class.
""" """
# TODO: format overriding must go out of here return media_type_matches(self.media_type, media_type)
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)
def render(self, obj=None, media_type=None): def render(self, obj=None, media_type=None):
""" """

View File

@ -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.template.response import SimpleTemplateResponse
from django.core.handlers.wsgi import STATUS_CODE_TEXT 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): class Response(SimpleTemplateResponse):
""" """
An HttpResponse that may include content that hasn't yet been serialized. An HttpResponse that allows it's data to be rendered into
arbitrary media types.
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.
""" """
_ACCEPT_QUERY_PARAM = api_settings.URL_ACCEPT_OVERRIDE def __init__(self, data=None, status=None, headers=None,
_IGNORE_IE_ACCEPT_HEADER = True 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, Setting 'renderer' and 'media_type' will typically be defered,
request=None, renderer_classes=None, format=None): For example being set automatically by the `APIView`.
# First argument taken by `SimpleTemplateResponse.__init__` is template_name, """
# which we don't need
super(Response, self).__init__(None, status=status) super(Response, self).__init__(None, status=status)
self.data = data
self.raw_content = content
self.has_content_body = content is not None
self.headers = headers and headers[:] or [] self.headers = headers and headers[:] or []
self.view = view self.renderer = renderer
self.request = request self.media_type = media_type
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]
@property @property
def rendered_content(self): def rendered_content(self):
""" self['Content-Type'] = self.media_type
The final rendered content. Accessing this attribute triggers the if self.data is None:
complete rendering cycle: selecting suitable renderer, setting return self.renderer.render()
response's actual content type, rendering data. return self.renderer.render(self.data, self.media_type)
"""
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()
@property @property
def status_text(self): def status_text(self):
@ -100,74 +37,3 @@ class Response(SimpleTemplateResponse):
Provided for convenience. Provided for convenience.
""" """
return STATUS_CODE_TEXT.get(self.status_code, '') 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])

View File

@ -38,6 +38,7 @@ DEFAULTS = {
), ),
'DEFAULT_PERMISSIONS': (), 'DEFAULT_PERMISSIONS': (),
'DEFAULT_THROTTLES': (), 'DEFAULT_THROTTLES': (),
'DEFAULT_CONTENT_NEGOTIATION': 'djangorestframework.contentnegotiation.DefaultContentNegotiation',
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,
@ -45,7 +46,8 @@ DEFAULTS = {
'FORM_METHOD_OVERRIDE': '_method', 'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content', 'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type', 'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': '_accept', 'URL_ACCEPT_OVERRIDE': 'accept',
'IGNORE_MSIE_ACCEPT_HEADER': True,
'FORMAT_SUFFIX_KWARG': 'format' 'FORMAT_SUFFIX_KWARG': 'format'
} }
@ -58,8 +60,9 @@ IMPORT_STRINGS = (
'DEFAULT_AUTHENTICATION', 'DEFAULT_AUTHENTICATION',
'DEFAULT_PERMISSIONS', 'DEFAULT_PERMISSIONS',
'DEFAULT_THROTTLES', 'DEFAULT_THROTTLES',
'DEFAULT_CONTENT_NEGOTIATION',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN' 'UNAUTHENTICATED_TOKEN',
) )
@ -68,7 +71,7 @@ def perform_import(val, setting):
If the given setting is a string import notation, If the given setting is a string import notation,
then perform the necessary import or imports. 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 return val
if isinstance(val, basestring): if isinstance(val, basestring):
@ -88,10 +91,7 @@ def import_from_string(val, setting):
module_path, class_name = '.'.join(parts[:-1]), parts[-1] module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except Exception, e: except:
import traceback
tb = traceback.format_exc()
import pdb; pdb.set_trace()
msg = "Could not import '%s' for API setting '%s'" % (val, setting) msg = "Could not import '%s' for API setting '%s'" % (val, setting)
raise ImportError(msg) raise ImportError(msg)

View File

@ -7,7 +7,7 @@ from django.test import TestCase, Client
from djangorestframework import status from djangorestframework import status
from djangorestframework.authentication import SessionAuthentication from djangorestframework.authentication import SessionAuthentication
from djangorestframework.utils import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.parsers import ( from djangorestframework.parsers import (
FormParser, FormParser,
MultiPartParser, MultiPartParser,
@ -22,33 +22,21 @@ factory = RequestFactory()
class TestMethodOverloading(TestCase): 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') self.assertEqual(request.method, 'GET')
request = Request(factory.post('/'))
def test_POST_method(self):
"""
POST requests identified.
"""
request = factory.post('/')
self.assertEqual(request.method, '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): def test_overloaded_method(self):
""" """
POST requests can be overloaded to another method by setting a POST requests can be overloaded to another method by setting a
reserved form field reserved form field
""" """
request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'}) request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'}))
self.assertEqual(request.method, 'DELETE') self.assertEqual(request.method, 'DELETE')
@ -57,14 +45,14 @@ class TestContentParsing(TestCase):
""" """
Ensure request.DATA returns None for GET request with no content. Ensure request.DATA returns None for GET request with no content.
""" """
request = factory.get('/') request = Request(factory.get('/'))
self.assertEqual(request.DATA, None) self.assertEqual(request.DATA, None)
def test_standard_behaviour_determines_no_content_HEAD(self): def test_standard_behaviour_determines_no_content_HEAD(self):
""" """
Ensure request.DATA returns None for HEAD request. Ensure request.DATA returns None for HEAD request.
""" """
request = factory.head('/') request = Request(factory.head('/'))
self.assertEqual(request.DATA, None) self.assertEqual(request.DATA, None)
def test_standard_behaviour_determines_form_content_POST(self): 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. Ensure request.DATA returns content for POST request with form content.
""" """
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
parsers = (FormParser, MultiPartParser) request = Request(factory.post('/', data))
request = factory.post('/', data, parser=parsers) request.parser_classes = (FormParser, MultiPartParser)
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
def test_standard_behaviour_determines_non_form_content_POST(self): def test_standard_behaviour_determines_non_form_content_POST(self):
@ -83,9 +71,8 @@ class TestContentParsing(TestCase):
""" """
content = 'qwerty' content = 'qwerty'
content_type = 'text/plain' content_type = 'text/plain'
parsers = (PlainTextParser,) request = Request(factory.post('/', content, content_type=content_type))
request = factory.post('/', content, content_type=content_type, request.parser_classes = (PlainTextParser,)
parsers=parsers)
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
def test_standard_behaviour_determines_form_content_PUT(self): 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. Ensure request.DATA returns content for PUT request with form content.
""" """
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
parsers = (FormParser, MultiPartParser)
from django import VERSION from django import VERSION
if VERSION >= (1, 5): if VERSION >= (1, 5):
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
request = factory.put('/', encode_multipart(BOUNDARY, data), parsers=parsers, request = Request(factory.put('/', encode_multipart(BOUNDARY, data),
content_type=MULTIPART_CONTENT) content_type=MULTIPART_CONTENT))
else: else:
request = factory.put('/', data, parsers=parsers) request = Request(factory.put('/', data))
request.parser_classes = (FormParser, MultiPartParser)
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
def test_standard_behaviour_determines_non_form_content_PUT(self): def test_standard_behaviour_determines_non_form_content_PUT(self):
@ -113,9 +100,8 @@ class TestContentParsing(TestCase):
""" """
content = 'qwerty' content = 'qwerty'
content_type = 'text/plain' content_type = 'text/plain'
parsers = (PlainTextParser, ) request = Request(factory.put('/', content, content_type=content_type))
request = factory.put('/', content, content_type=content_type, request.parser_classes = (PlainTextParser, )
parsers=parsers)
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
def test_overloaded_behaviour_allows_content_tunnelling(self): def test_overloaded_behaviour_allows_content_tunnelling(self):
@ -128,8 +114,8 @@ class TestContentParsing(TestCase):
Request._CONTENT_PARAM: content, Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type Request._CONTENTTYPE_PARAM: content_type
} }
parsers = (PlainTextParser, ) request = Request(factory.post('/', data))
request = factory.post('/', data, parsers=parsers) request.parser_classes = (PlainTextParser, )
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
# def test_accessing_post_after_data_form(self): # def test_accessing_post_after_data_form(self):

View File

@ -4,10 +4,10 @@ import unittest
from django.conf.urls.defaults import patterns, url, include from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase from django.test import TestCase
from djangorestframework.response import Response, NotAcceptable from djangorestframework.response import Response
from djangorestframework.views import APIView from djangorestframework.views import APIView
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework import status from djangorestframework import status, exceptions
from djangorestframework.renderers import ( from djangorestframework.renderers import (
BaseRenderer, BaseRenderer,
JSONRenderer, JSONRenderer,
@ -91,7 +91,7 @@ class TestResponseDetermineRenderer(TestCase):
accept_list = ['application/json'] accept_list = ['application/json']
renderer_classes = (MockPickleRenderer, ) renderer_classes = (MockPickleRenderer, )
response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes) 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): class TestResponseRenderContent(TestCase):

View File

@ -1,9 +1,6 @@
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator from django.utils.xmlutils import SimplerXMLGenerator
from djangorestframework.compat import StringIO from djangorestframework.compat import StringIO
from djangorestframework.compat import RequestFactory as DjangoRequestFactory
from djangorestframework.request import Request
import re import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -102,38 +99,3 @@ class XMLRenderer():
def dict2xml(input): def dict2xml(input):
return XMLRenderer().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)

View File

@ -54,11 +54,14 @@ def _camelcase_to_spaces(content):
class APIView(_View): class APIView(_View):
settings = api_settings
renderer_classes = api_settings.DEFAULT_RENDERERS renderer_classes = api_settings.DEFAULT_RENDERERS
parser_classes = api_settings.DEFAULT_PARSERS parser_classes = api_settings.DEFAULT_PARSERS
authentication_classes = api_settings.DEFAULT_AUTHENTICATION authentication_classes = api_settings.DEFAULT_AUTHENTICATION
throttle_classes = api_settings.DEFAULT_THROTTLES throttle_classes = api_settings.DEFAULT_THROTTLES
permission_classes = api_settings.DEFAULT_PERMISSIONS permission_classes = api_settings.DEFAULT_PERMISSIONS
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION
@classmethod @classmethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
@ -169,6 +172,27 @@ class APIView(_View):
""" """
return self.renderer_classes[0] 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): def get_permissions(self):
""" """
Instantiates and returns the list of permissions that this view requires. Instantiates and returns the list of permissions that this view requires.
@ -177,10 +201,28 @@ class APIView(_View):
def get_throttles(self): 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] 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): def check_permissions(self, request, obj=None):
""" """
Check if request should be permitted. Check if request should be permitted.
@ -204,35 +246,43 @@ class APIView(_View):
return Request(request, parser_classes=self.parser_classes, return Request(request, parser_classes=self.parser_classes,
authentication_classes=self.authentication_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): def finalize_response(self, request, response, *args, **kwargs):
""" """
Returns the final response object. Returns the final response object.
""" """
if isinstance(response, Response): if isinstance(response, Response):
response.view = self response.renderer = self.renderer
response.request = request response.media_type = self.media_type
response.renderer_classes = self.renderer_classes
if api_settings.FORMAT_SUFFIX_KWARG:
response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None)
for key, value in self.headers.items(): for key, value in self.headers.items():
response[key] = value response[key] = value
return response 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): def handle_exception(self, exc):
""" """
Handle any exception that occurs, by returning an appropriate response, Handle any exception that occurs, by returning an appropriate response,
or re-raising the error. 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 self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
if isinstance(exc, exceptions.APIException): if isinstance(exc, exceptions.APIException):
@ -250,14 +300,8 @@ class APIView(_View):
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """
`APIView.dispatch()` is pretty much the same as Django's regular `.dispatch()` is pretty much the same as Django's regular dispatch,
`View.dispatch()`, except that it includes hooks to: but with extra hooks for startup, finalize, and exception handling.
* 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.
""" """
request = self.initialize_request(request, *args, **kwargs) request = self.initialize_request(request, *args, **kwargs)
self.request = request self.request = request
@ -270,7 +314,8 @@ class APIView(_View):
# Get the appropriate handler method # Get the appropriate handler method
if request.method.lower() in self.http_method_names: 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: else:
handler = self.http_method_not_allowed handler = self.http_method_not_allowed