mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +03:00
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:
parent
eafda85508
commit
ce6e5fdc01
|
@ -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]
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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])),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user