aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/response.py
diff options
context:
space:
mode:
Diffstat (limited to 'djangorestframework/response.py')
-rw-r--r--djangorestframework/response.py141
1 files changed, 129 insertions, 12 deletions
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 96345cee..4f9b3a62 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -5,25 +5,62 @@ into a HTTP response depending on what renderers are set on your view and
als depending on the accept header of the request.
"""
+from django.template.response import SimpleTemplateResponse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
-__all__ = ('Response', 'ErrorResponse')
+from djangorestframework.utils.mediatypes import order_by_precedence
+from djangorestframework.utils import MSIE_USER_AGENT_REGEX
+from djangorestframework import status
+
-# TODO: remove raw_content/cleaned_content and just use content?
+__all__ = ('Response', 'ErrorResponse')
-class Response(object):
+class Response(SimpleTemplateResponse):
"""
An HttpResponse that may include content that hasn't yet been serialized.
"""
- def __init__(self, status=200, content=None, headers=None):
- self.status = status
- self.media_type = None
+ _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
+ _IGNORE_IE_ACCEPT_HEADER = True
+
+ def __init__(self, content=None, status=None, request=None, renderers=None):
+ """
+ content is the raw content.
+
+ The set of renderers that the response can handle.
+
+ Should be a tuple/list of classes as described in the :mod:`renderers` module.
+ """
+ # First argument taken by `SimpleTemplateResponse.__init__` is template_name,
+ # which we don't need
+ super(Response, self).__init__(None, status=status)
+ # We need to store our content in raw content to avoid overriding HttpResponse's
+ # `content` property
+ self.raw_content = content
self.has_content_body = content is not None
- self.raw_content = content # content prior to filtering
- self.cleaned_content = content # content after filtering
- self.headers = headers or {}
+ self.request = request
+ if renderers is not None:
+ self.renderers = renderers
+ # TODO: must go
+ self.view = None
+
+ # TODO: wrap this behavior around dispatch(), ensuring it works
+ # out of the box with existing Django classes that use render_to_response.
+ @property
+ def rendered_content(self):
+ """
+ """
+ renderer, media_type = self._determine_renderer()
+ # TODO: renderer *could* override media_type in .render() if required.
+
+ # 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()
@property
def status_text(self):
@@ -33,12 +70,92 @@ class Response(object):
"""
return STATUS_CODE_TEXT.get(self.status, '')
+ def _determine_accept_list(self):
+ request = self.request
+ if request is None:
+ return ['*/*']
+
+ 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'])):
+ # Ignore MSIE's broken accept behavior 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 client's 'Accept' header,
+ and the :attr:`renderers` 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
+ """
+ # 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)
+ renderers = [renderer_cls(self.view) for renderer_cls in self.renderers]
+
+ for media_type_list in order_by_precedence(self._determine_accept_list()):
+ for renderer in renderers:
+ for media_type in media_type_list:
+ if renderer.can_handle_response(media_type):
+ return renderer, media_type
+
+ # No acceptable renderers were found
+ raise ErrorResponse(content={'detail': 'Could not satisfy the client\'s Accept header',
+ 'available_types': self._rendered_media_types},
+ status=status.HTTP_406_NOT_ACCEPTABLE,
+ renderers=self.renderers)
+
+ def _get_renderers(self):
+ """
+ This just provides a default when renderers havent' been set.
+ """
+ if hasattr(self, '_renderers'):
+ return self._renderers
+ return ()
+
+ def _set_renderers(self, value):
+ self._renderers = value
+
+ renderers = property(_get_renderers, _set_renderers)
+
+ @property
+ def _rendered_media_types(self):
+ """
+ Return an list of all the media types that this response can render.
+ """
+ return [renderer.media_type for renderer in self.renderers]
+
+ @property
+ def _rendered_formats(self):
+ """
+ Return a list of all the formats that this response can render.
+ """
+ return [renderer.format for renderer in self.renderers]
+
+ @property
+ def _default_renderer(self):
+ """
+ Return the response's default renderer class.
+ """
+ return self.renderers[0]
+
-class ErrorResponse(BaseException):
+class ErrorResponse(Response, BaseException):
"""
An exception representing an Response that should be returned immediately.
Any content should be serialized as-is, without being filtered.
"""
+ pass
- def __init__(self, status, content=None, headers={}):
- self.response = Response(status, content=content, headers=headers)