Permalink
Browse files

Renderers can now cope with parameterised args. ResponseMixin gets cl…

…eaned up & added Renderer.can_handle_response(), mirroring Parsers.can_handle_request()
  • Loading branch information...
1 parent eafda85 commit ce6e5fdc01b6d820f317bc1d8edc4ede4a946516 Tom Christie committed May 24, 2011
@@ -14,7 +14,7 @@
from djangorestframework.resources import Resource
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
-from djangorestframework.utils.mediatypes import is_form_media_type
+from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
from decimal import Decimal
import re
@@ -206,7 +206,7 @@ def _parsed_media_types(self):
@property
def _default_parser(self):
"""
- Return the view's default parser.
+ Return the view's default parser class.
"""
return self.parsers[0]
@@ -245,15 +245,15 @@ def render(self, response):
try:
renderer = self._determine_renderer(self.request)
except ErrorResponse, exc:
- renderer = self._default_renderer
+ renderer = self._default_renderer(self)
response = exc.response
# Serialize the response content
# TODO: renderer.media_type isn't the right thing to do here...
if response.has_content_body:
- content = renderer(self).render(response.cleaned_content, renderer.media_type)
+ content = renderer.render(response.cleaned_content, renderer.media_type)
else:
- content = renderer(self).render()
+ content = renderer.render()
# Build the HTTP Response
# TODO: renderer.media_type isn't the right thing to do here...
@@ -264,10 +264,6 @@ def render(self, response):
return resp
- # TODO: This should be simpler now.
- # Add a handles_response() to the renderer, then iterate through the
- # acceptable media types, ordered by how specific they are,
- # calling handles_response on each renderer.
def _determine_renderer(self, request):
"""
Return the appropriate renderer for the output, given the client's 'Accept' header,
@@ -282,60 +278,33 @@ def _determine_renderer(self, request):
elif (self._IGNORE_IE_ACCEPT_HEADER and
request.META.has_key('HTTP_USER_AGENT') and
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
+ # Ignore MSIE's broken accept behavior and do something sensible instead
accept_list = ['text/html', '*/*']
elif request.META.has_key('HTTP_ACCEPT'):
# Use standard HTTP Accept negotiation
- accept_list = request.META["HTTP_ACCEPT"].split(',')
+ accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
else:
# No accept header specified
- return self._default_renderer
-
- # Parse the accept header into a dict of {qvalue: set of media types}
- # We ignore mietype parameters
- accept_dict = {}
- for token in accept_list:
- components = token.split(';')
- mimetype = components[0].strip()
- qvalue = Decimal('1.0')
-
- if len(components) > 1:
- # Parse items that have a qvalue eg 'text/html; q=0.9'
- try:
- (q, num) = components[-1].split('=')
- if q == 'q':
- qvalue = Decimal(num)
- except:
- # Skip malformed entries
- continue
-
- if accept_dict.has_key(qvalue):
- accept_dict[qvalue].add(mimetype)
- else:
- accept_dict[qvalue] = set((mimetype,))
-
- # Convert to a list of sets ordered by qvalue (highest first)
- accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
+ return self._default_renderer(self)
+
+ # 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 :)
+ # We're effectivly looping over max len(accept_list) * len(self.renderers)
+ renderers = [renderer_cls(self) for renderer_cls in self.renderers]
+
+ for media_type_lst in order_by_precedence(accept_list):
+ for renderer in renderers:
+ for media_type in media_type_lst:
+ if renderer.can_handle_response(media_type):
+ return renderer
- for accept_set in accept_sets:
- # Return any exact match
- for renderer in self.renderers:
- if renderer.media_type in accept_set:
- return renderer
-
- # Return any subtype match
- for renderer in self.renderers:
- if renderer.media_type.split('/')[0] + '/*' in accept_set:
- return renderer
-
- # Return default
- if '*/*' in accept_set:
- return self._default_renderer
-
-
+ # No acceptable renderers were found
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
{'detail': 'Could not satisfy the client\'s Accept header',
'available_types': self._rendered_media_types})
+
@property
def _rendered_media_types(self):
"""
@@ -346,7 +315,7 @@ def _rendered_media_types(self):
@property
def _default_renderer(self):
"""
- Return the view's default renderer.
+ Return the view's default renderer class.
"""
return self.renderers[0]
@@ -54,7 +54,7 @@ def can_handle_request(self, content_type):
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.
"""
- return media_type_matches(content_type, self.media_type)
+ return media_type_matches(self.media_type, content_type)
def parse(self, stream):
"""
@@ -16,7 +16,7 @@
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.utils.description import get_name, get_description
-from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param
+from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from decimal import Decimal
import re
@@ -39,11 +39,26 @@ class BaseRenderer(object):
All renderers must extend this class, set the :attr:`media_type` attribute,
and override the :meth:`render` method.
"""
+
media_type = None
def __init__(self, view):
self.view = view
+ def can_handle_response(self, accept):
+ """
+ Returns :const:`True` if this renderer is able to deal with the given
+ *accept* media type.
+
+ 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
+ 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.
+ """
+ return media_type_matches(self.media_type, accept)
+
def render(self, obj=None, media_type=None):
"""
Given an object render it into a string.
@@ -66,9 +81,13 @@ class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to JSON
"""
+
media_type = 'application/json'
def render(self, obj=None, media_type=None):
+ """
+ Renders *obj* into serialized JSON.
+ """
if obj is None:
return ''
@@ -92,6 +111,9 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
def render(self, obj=None, media_type=None):
+ """
+ Renders *obj* into serialized XML.
+ """
if obj is None:
return ''
return dict2xml(obj)
@@ -103,24 +125,30 @@ class TemplateRenderer(BaseRenderer):
Render the object simply by using the given template.
To create a template renderer, subclass this class, and set
- the :attr:`media_type` and `:attr:template` attributes.
+ the :attr:`media_type` and :attr:`template` attributes.
"""
+
media_type = None
template = None
def render(self, obj=None, media_type=None):
+ """
+ Renders *obj* using the :attr:`template` specified on the class.
+ """
if obj is None:
return ''
- context = RequestContext(self.request, obj)
- return self.template.render(context)
+ template = loader.get_template(self.template)
+ context = RequestContext(self.view.request, {'object': obj})
+ return template.render(context)
class DocumentingTemplateRenderer(BaseRenderer):
"""
Base class for renderers used to self-document the API.
Implementing classes should extend this class and set the template attribute.
"""
+
template = None
def _get_content(self, view, request, obj, media_type):
@@ -215,6 +243,12 @@ def __init__(self, view):
def render(self, obj=None, media_type=None):
+ """
+ Renders *obj* using the :attr:`template` set on the class.
+
+ The context used in the template contains all the information
+ needed to self-document the response to this request.
+ """
content = self._get_content(self.view, self.view.request, obj, media_type)
form_instance = self._get_form_instance(self.view)
@@ -272,6 +306,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
Renderer which provides a browsable HTML interface for an API.
See the examples at http://api.django-rest-framework.org to see this in action.
"""
+
media_type = 'text/html'
template = 'renderer.html'
@@ -282,6 +317,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
given their Accept headers.
"""
+
media_type = 'application/xhtml+xml'
template = 'renderer.html'
@@ -292,6 +328,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
documentation of the returned status and headers, and of the resource's name and description.
Useful for browsing an API with command line tools.
"""
+
media_type = 'text/plain'
template = 'renderer.txt'
@@ -4,7 +4,7 @@
register = Library()
def add_query_param(url, param):
- (key, val) = param.split('=')
+ (key, sep, val) = param.partition('=')
param = '%s=%s' % (key, quote(val))
(scheme, netloc, path, params, query, fragment) = urlparse(url)
if query:
@@ -13,23 +13,24 @@
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
-class MockView(ResponseMixin, DjangoView):
- def get(self, request):
- response = Response(DUMMYSTATUS, DUMMYCONTENT)
- return self.render(response)
-
class RendererA(BaseRenderer):
media_type = 'mock/renderera'
- def render(self, obj=None, content_type=None):
+ def render(self, obj=None, media_type=None):
return RENDERER_A_SERIALIZER(obj)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
- def render(self, obj=None, content_type=None):
+ def render(self, obj=None, media_type=None):
return RENDERER_B_SERIALIZER(obj)
+class MockView(ResponseMixin, DjangoView):
+ renderers = (RendererA, RendererB)
+
+ def get(self, request):
+ response = Response(DUMMYSTATUS, DUMMYCONTENT)
+ return self.render(response)
urlpatterns = patterns('',
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
@@ -13,6 +13,9 @@
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
+from mediatypes import media_type_matches, is_form_media_type
+from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence
+
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
def as_tuple(obj):
Oops, something went wrong.

0 comments on commit ce6e5fd

Please sign in to comment.