aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--djangorestframework/mixins.py77
-rw-r--r--djangorestframework/parsers.py2
-rw-r--r--djangorestframework/renderers.py45
-rw-r--r--djangorestframework/templatetags/add_query_param.py2
-rw-r--r--djangorestframework/tests/renderers.py15
-rw-r--r--djangorestframework/utils/__init__.py3
-rw-r--r--djangorestframework/utils/mediatypes.py79
-rw-r--r--djangorestframework/views.py94
8 files changed, 175 insertions, 142 deletions
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 90c75970..d99b6f15 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -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
-
- 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 @@ 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]
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 7c76bcc6..726e09e9 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -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):
"""
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 3e59511c..245bfdfe 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -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'
diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py
index 91c1a312..94833bce 100644
--- a/djangorestframework/templatetags/add_query_param.py
+++ b/djangorestframework/templatetags/add_query_param.py
@@ -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:
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
index 5364cd2e..54276993 100644
--- a/djangorestframework/tests/renderers.py
+++ b/djangorestframework/tests/renderers.py
@@ -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])),
diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py
index 67870001..99f9724c 100644
--- a/djangorestframework/utils/__init__.py
+++ b/djangorestframework/utils/__init__.py
@@ -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):
diff --git a/djangorestframework/utils/mediatypes.py b/djangorestframework/utils/mediatypes.py
index 190cdc2d..ae734e62 100644
--- a/djangorestframework/utils/mediatypes.py
+++ b/djangorestframework/utils/mediatypes.py
@@ -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
-
- 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
+ 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 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')
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 3d6a6c40..ade90cac 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -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