mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +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
|
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."
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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])
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user