aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2012-09-14 22:42:29 +0100
committerTom Christie2012-09-14 22:42:29 +0100
commitb3e29d9576f1b1b6d12f9abfeb4a06f669b45202 (patch)
treed89770f15bdfa6b00e4a91e2a8d0a270106065b8
parentb7b8cd11b1aad3fcf4bad221d164bb55e0bf5859 (diff)
downloaddjango-rest-framework-b3e29d9576f1b1b6d12f9abfeb4a06f669b45202.tar.bz2
Moved content negotiation out of response. Nicer exception handling now.
-rw-r--r--djangorestframework/contentnegotiation.py63
-rw-r--r--djangorestframework/exceptions.py9
-rw-r--r--djangorestframework/renderers.py28
-rw-r--r--djangorestframework/response.py168
-rw-r--r--djangorestframework/settings.py14
-rw-r--r--djangorestframework/tests/request.py54
-rw-r--r--djangorestframework/tests/response.py6
-rw-r--r--djangorestframework/utils/__init__.py38
-rw-r--r--djangorestframework/views.py91
9 files changed, 198 insertions, 273 deletions
diff --git a/djangorestframework/contentnegotiation.py b/djangorestframework/contentnegotiation.py
new file mode 100644
index 00000000..223919ef
--- /dev/null
+++ b/djangorestframework/contentnegotiation.py
@@ -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 ['*/*']
diff --git a/djangorestframework/exceptions.py b/djangorestframework/exceptions.py
index 3f5b23f6..6930515d 100644
--- a/djangorestframework/exceptions.py
+++ b/djangorestframework/exceptions.py
@@ -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."
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 26e8cba1..729f8111 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -48,28 +48,22 @@ class BaseRenderer(object):
def __init__(self, view=None):
self.view = view
- def can_handle_response(self, accept):
+ def can_handle_format(self, format):
+ return format == self.format
+
+ def can_handle_media_type(self, media_type):
"""
- Returns :const:`True` if this renderer is able to deal with the given
- *accept* 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 *accept*
- argument against the :attr:`media_type` attribute set on the class to see if
+ 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):
"""
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index e1366bdb..205c0503 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -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])
diff --git a/djangorestframework/settings.py b/djangorestframework/settings.py
index e5181f4b..95215712 100644
--- a/djangorestframework/settings.py
+++ b/djangorestframework/settings.py
@@ -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)
diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py
index 51e3660c..74eae438 100644
--- a/djangorestframework/tests/request.py
+++ b/djangorestframework/tests/request.py
@@ -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):
diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py
index 0483d826..cdddfff4 100644
--- a/djangorestframework/tests/response.py
+++ b/djangorestframework/tests/response.py
@@ -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):
diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py
index bb5bb6d7..f53ac0b8 100644
--- a/djangorestframework/utils/__init__.py
+++ b/djangorestframework/utils/__init__.py
@@ -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)
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 9debee19..2629663a 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -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