Cleaner content negotiation. Occurs after permissions. Optional 'force' flag.

This commit is contained in:
Tom Christie 2012-09-14 23:17:12 +01:00
parent 5036638d0c
commit 6543ccd244
2 changed files with 38 additions and 26 deletions

View File

@ -1,24 +1,40 @@
from djangorestframework import exceptions from djangorestframework import exceptions
from djangorestframework.settings import api_settings from djangorestframework.settings import api_settings
from djangorestframework.utils.mediatypes import order_by_precedence from djangorestframework.utils.mediatypes import order_by_precedence
from django.http import Http404
import re import re
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
class BaseContentNegotiation(object): class BaseContentNegotiation(object):
def determine_renderer(self, request, renderers): def negotiate(self, request, renderers, format=None, force=False):
raise NotImplementedError('.determine_renderer() must be implemented') raise NotImplementedError('.negotiate() must be implemented')
class DefaultContentNegotiation(object): class DefaultContentNegotiation(object):
settings = api_settings settings = api_settings
def negotiate(self, request, renderers): def negotiate(self, request, renderers, format=None, force=False):
""" """
Given a request and a list of renderers, return a two-tuple of: Given a request and a list of renderers, return a two-tuple of:
(renderer, media type). (renderer, media type).
If force is set, then suppress exceptions, and forcibly return a
fallback renderer and media_type.
""" """
try:
return self._negotiate(request, renderers, format)
except (Http404, exceptions.NotAcceptable):
if force:
return (renderers[0], renderers[0].media_type)
raise
def _negotiate(self, request, renderers, format=None):
"""
Actual implementation of negotiate, inside the 'force' wrapper.
"""
renderers = self.filter_renderers(renderers, format)
accepts = self.get_accept_list(request) accepts = self.get_accept_list(request)
# Check the acceptable media types against each renderer, # Check the acceptable media types against each renderer,
@ -33,6 +49,19 @@ class DefaultContentNegotiation(object):
raise exceptions.NotAcceptable(available_renderers=renderers) raise exceptions.NotAcceptable(available_renderers=renderers)
def filter_renderers(self, renderers, format):
"""
If there is a '.json' style format suffix, only use
renderers that accept that format.
"""
if not format:
return renderers
renderers = [renderer for renderer in renderers
if renderer.can_handle_format(format)]
if not renderers:
raise Http404()
def get_accept_list(self, request): def get_accept_list(self, request):
""" """
Given the incoming request, return a tokenised list of Given the incoming request, return a tokenised list of

View File

@ -197,24 +197,13 @@ class APIView(_View):
""" """
return [throttle(self) for throttle in self.throttle_classes] return [throttle(self) for throttle in self.throttle_classes]
def content_negotiation(self, request): def content_negotiation(self, request, force=False):
""" """
Determine which renderer and media type to use render the response. Determine which renderer and media type to use render the response.
""" """
renderers = self.get_renderers() renderers = self.get_renderers()
if self.format:
# If there is a '.json' style format suffix, only use
# renderers that accept that format.
fallback = renderers[0]
renderers = [renderer for renderer in renderers
if renderer.can_handle_format(self.format)]
if not renderers:
self.format404 = True
return (fallback, fallback.media_type)
conneg = self.content_negotiation_class() conneg = self.content_negotiation_class()
return conneg.negotiate(request, renderers) return conneg.negotiate(request, renderers, self.format, force)
def check_permissions(self, request, obj=None): def check_permissions(self, request, obj=None):
""" """
@ -244,19 +233,17 @@ class APIView(_View):
Runs anything that needs to occur prior to calling the method handlers. Runs anything that needs to occur prior to calling the method handlers.
""" """
self.format = self.get_format_suffix(**kwargs) self.format = self.get_format_suffix(**kwargs)
self.renderer, self.media_type = self.content_negotiation(request)
self.check_permissions(request) self.check_permissions(request)
self.check_throttles(request) self.check_throttles(request)
# If the request included a non-existant .format URL suffix, self.renderer, self.media_type = self.content_negotiation(request)
# raise 404, but only after first making permission checks.
if getattr(self, 'format404', None):
raise Http404()
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
""" """
Returns the final response object. Returns the final response object.
""" """
if isinstance(response, Response): if isinstance(response, Response):
if not getattr(self, 'renderer', None):
self.renderer, self.media_type = self.content_negotiation(request, force=True)
response.renderer = self.renderer response.renderer = self.renderer
response.media_type = self.media_type response.media_type = self.media_type
@ -270,11 +257,7 @@ class APIView(_View):
Handle any exception that occurs, by returning an appropriate response, Handle any exception that occurs, by returning an appropriate response,
or re-raising the error. or re-raising the error.
""" """
if isinstance(exc, exceptions.NotAcceptable): if isinstance(exc, exceptions.Throttled):
# Fall back to default renderer
self.renderer = exc.available_renderers[0]
self.media_type = exc.available_renderers[0].media_type
elif isinstance(exc, exceptions.Throttled):
# Throttle wait header # Throttle wait header
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait