Renderers can now cope with parameterised args. ResponseMixin gets cleaned up & added Renderer.can_handle_response(), mirroring Parsers.can_handle_request()

This commit is contained in:
Tom Christie 2011-05-24 13:29:30 +01:00
parent eafda85508
commit ce6e5fdc01
8 changed files with 173 additions and 140 deletions

View File

@ -14,7 +14,7 @@ from djangorestframework.parsers import FormParser, MultiPartParser
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 @@ class RequestMixin(object):
@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 @@ class ResponseMixin(object):
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 @@ class ResponseMixin(object):
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 @@ class ResponseMixin(object):
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
return self._default_renderer(self)
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)]
# 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 @@ class ResponseMixin(object):
@property
def _default_renderer(self):
"""
Return the view's default renderer.
Return the view's default renderer class.
"""
return self.renderers[0]

View File

@ -54,7 +54,7 @@ class BaseParser(object):
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):
"""

View File

@ -16,7 +16,7 @@ from djangorestframework.compat import apply_markdown
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,17 +125,22 @@ 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):
@ -121,6 +148,7 @@ 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 @@ class DocumentingTemplateRenderer(BaseRenderer):
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'

View File

@ -4,7 +4,7 @@ from urllib import quote
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:

View File

@ -13,23 +13,24 @@ DUMMYCONTENT = 'dummycontent'
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])),

View File

@ -13,6 +13,9 @@ import xml.etree.ElementTree as ET
# """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):

View File

@ -51,6 +51,22 @@ def get_media_type_params(media_type):
return _MediaType(media_type).params
def order_by_precedence(media_type_lst):
"""
Returns a list of lists of media type strings, ordered by precedence.
Precedence is determined by how specific a media type is:
3. 'type/subtype; param=val'
2. 'type/subtype'
1. 'type/*'
0. '*/*'
"""
ret = [[],[],[],[]]
for media_type in media_type_lst:
precedence = _MediaType(media_type).precedence
ret[3-precedence].append(media_type)
return ret
class _MediaType(object):
def __init__(self, media_type_str):
@ -61,53 +77,54 @@ class _MediaType(object):
self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other):
"""Return true if this MediaType satisfies the constraint of the given MediaType."""
for key in other.params.keys():
if key != 'q' and other.params[key] != self.params.get(key, None):
"""Return true if this MediaType satisfies the given MediaType."""
for key in self.params.keys():
if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
return False
if other.sub_type != '*' and other.sub_type != self.sub_type:
if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
return False
if other.main_type != '*' and other.main_type != self.main_type:
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
return False
return True
@property
def precedence(self):
"""
Return a precedence level for the media type given how specific it is.
Return a precedence level from 0-3 for the media type given how specific it is.
"""
if self.main_type == '*':
return 1
return 0
elif self.sub_type == '*':
return 2
return 1
elif not self.params or self.params.keys() == ['q']:
return 3
return 4
return 2
return 3
def quality(self):
"""
Return a quality level for the media type.
"""
try:
return Decimal(self.params.get('q', '1.0'))
except:
return Decimal(0)
#def quality(self):
# """
# Return a quality level for the media type.
# """
# try:
# return Decimal(self.params.get('q', '1.0'))
# except:
# return Decimal(0)
#def score(self):
# """
# Return an overall score for a given media type given it's quality and precedence.
# """
# # NB. quality values should only have up to 3 decimal points
# # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
# return self.quality * 10000 + self.precedence
def score(self):
"""
Return an overall score for a given media type given it's quality and precedence.
"""
# NB. quality values should only have up to 3 decimal points
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
return self.quality * 10000 + self.precedence
def as_tuple(self):
return (self.main_type, self.sub_type, self.params)
#def as_tuple(self):
# return (self.main_type, self.sub_type, self.params)
def __repr__(self):
return "<MediaType %s>" % (self.as_tuple(),)
#def __repr__(self):
# return "<MediaType %s>" % (self.as_tuple(),)
def __str__(self):
return unicode(self).encode('utf-8')

View File

@ -99,52 +99,58 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
try:
self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions
self._check_permissions()
# Get the appropriate handler method
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response_obj = handler(request, *args, **kwargs)
# Allow return value to be either HttpResponse, Response, or an object, or None
if isinstance(response_obj, HttpResponse):
return response_obj
elif isinstance(response_obj, Response):
response = response_obj
elif response_obj is not None:
response = Response(status.HTTP_200_OK, response_obj)
else:
response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.filter_response(response.raw_content)
self.request = request
self.args = args
self.kwargs = kwargs
except ErrorResponse, exc:
response = exc.response
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
return self.render(response)
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
try:
self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions
self._check_permissions()
# Get the appropriate handler method
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response_obj = handler(request, *args, **kwargs)
# Allow return value to be either HttpResponse, Response, or an object, or None
if isinstance(response_obj, HttpResponse):
return response_obj
elif isinstance(response_obj, Response):
response = response_obj
elif response_obj is not None:
response = Response(status.HTTP_200_OK, response_obj)
else:
response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.filter_response(response.raw_content)
except ErrorResponse, exc:
response = exc.response
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
return self.render(response)
except:
import traceback
traceback.print_exc()
raise