diff options
| -rw-r--r-- | djangorestframework/contentnegotiation.py | 63 | ||||
| -rw-r--r-- | djangorestframework/exceptions.py | 9 | ||||
| -rw-r--r-- | djangorestframework/renderers.py | 28 | ||||
| -rw-r--r-- | djangorestframework/response.py | 168 | ||||
| -rw-r--r-- | djangorestframework/settings.py | 14 | ||||
| -rw-r--r-- | djangorestframework/tests/request.py | 54 | ||||
| -rw-r--r-- | djangorestframework/tests/response.py | 6 | ||||
| -rw-r--r-- | djangorestframework/utils/__init__.py | 38 | ||||
| -rw-r--r-- | djangorestframework/views.py | 91 | 
9 files changed, 198 insertions, 273 deletions
diff --git a/djangorestframework/contentnegotiation.py b/djangorestframework/contentnegotiation.py new file mode 100644 index 00000000..223919ef --- /dev/null +++ b/djangorestframework/contentnegotiation.py @@ -0,0 +1,63 @@ +from djangorestframework import exceptions +from djangorestframework.settings import api_settings +from djangorestframework.utils.mediatypes import order_by_precedence +import re + +MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') + + +class BaseContentNegotiation(object): +    def determine_renderer(self, request, renderers): +        raise NotImplementedError('.determine_renderer() must be implemented') + + +class DefaultContentNegotiation(object): +    settings = api_settings + +    def negotiate(self, request, renderers): +        """ +        Given a request and a list of renderers, return a two-tuple of: +        (renderer, media type). +        """ +        accepts = self.get_accept_list(request) + +        # 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 :) +        #     Worst case is we're looping over len(accept_list) * len(self.renderers) +        for media_type_set in order_by_precedence(accepts): +            for renderer in renderers: +                for media_type in media_type_set: +                    if renderer.can_handle_media_type(media_type): +                        return renderer, media_type + +        raise exceptions.NotAcceptable(available_renderers=renderers) + +    def get_accept_list(self, request): +        """ +        Given the incoming request, return a tokenised list of +        media type strings. +        """ +        if self.settings.URL_ACCEPT_OVERRIDE: +            # URL style accept override.  eg.  "?accept=application/json" +            override = request.GET.get(self.settings.URL_ACCEPT_OVERRIDE) +            if override: +                return [override] + +        if (self.settings.IGNORE_MSIE_ACCEPT_HEADER and +            'HTTP_USER_AGENT' in request.META and +            MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and +            request.META.get('HTTP_X_REQUESTED_WITH', '').lower() != 'xmlhttprequest'): +            # Ignore MSIE's broken accept behavior except for AJAX requests +            # and do something sensible instead +            return ['text/html', '*/*'] + +        if 'HTTP_ACCEPT' in request.META: +            # Standard HTTP Accept negotiation +            # Accept header specified +            tokens = request.META['HTTP_ACCEPT'].split(',') +            return [token.strip() for token in tokens] + +        # Standard HTTP Accept negotiation +        # No accept header specified +        return ['*/*'] diff --git a/djangorestframework/exceptions.py b/djangorestframework/exceptions.py index 3f5b23f6..6930515d 100644 --- a/djangorestframework/exceptions.py +++ b/djangorestframework/exceptions.py @@ -39,6 +39,15 @@ class MethodNotAllowed(APIException):          self.detail = (detail or self.default_detail) % method +class NotAcceptable(APIException): +    status_code = status.HTTP_406_NOT_ACCEPTABLE +    default_detail = "Could not satisfy the request's Accept header" + +    def __init__(self, detail=None, available_renderers=None): +        self.detail = detail or self.default_detail +        self.available_renderers = available_renderers + +  class UnsupportedMediaType(APIException):      status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE      default_detail = "Unsupported media type '%s' in request." diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 26e8cba1..729f8111 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -48,28 +48,22 @@ class BaseRenderer(object):      def __init__(self, view=None):          self.view = view -    def can_handle_response(self, accept): +    def can_handle_format(self, format): +        return format == self.format + +    def can_handle_media_type(self, media_type):          """ -        Returns :const:`True` if this renderer is able to deal with the given -        *accept* media type. +        Returns `True` if this renderer is able to deal with the given +        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 +        The default implementation for this function is to check the media type +        argument against the 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. +        This may be overridden to provide for other behavior, but typically +        you'll instead want to just set the `media_type` attribute on the class.          """ -        # TODO: format overriding must go out of here -        format = None -        if self.view is not None: -            format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None) -        if format is None and self.view is not None: -            format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None) - -        if format is not None: -            return format == self.format -        return media_type_matches(self.media_type, accept) +        return media_type_matches(self.media_type, media_type)      def render(self, obj=None, media_type=None):          """ diff --git a/djangorestframework/response.py b/djangorestframework/response.py index e1366bdb..205c0503 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -1,97 +1,34 @@ -""" -The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes. - -`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned -from any view. It is a bit smarter than Django's `HttpResponse`, for it renders automatically -its content to a serial format by using a list of :mod:`renderers`. - -To determine the content type to which it must render, default behaviour is to use standard -HTTP Accept header content negotiation. But `Response` also supports overriding the content type -by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers -from Internet Explorer user agents and use a sensible browser `Accept` header instead. -""" - - -import re  from django.template.response import SimpleTemplateResponse  from django.core.handlers.wsgi import STATUS_CODE_TEXT -from djangorestframework.settings import api_settings -from djangorestframework.utils.mediatypes import order_by_precedence -from djangorestframework import status - - -MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') - - -class NotAcceptable(Exception): -    pass  class Response(SimpleTemplateResponse):      """ -    An HttpResponse that may include content that hasn't yet been serialized. - -    Kwargs: -        - content(object). The raw content, not yet serialized. -        This must be native Python data that renderers can handle. -        (e.g.: `dict`, `str`, ...) -        - renderer_classes(list/tuple). The renderers to use for rendering the response content. +    An HttpResponse that allows it's data to be rendered into +    arbitrary media types.      """ -    _ACCEPT_QUERY_PARAM = api_settings.URL_ACCEPT_OVERRIDE -    _IGNORE_IE_ACCEPT_HEADER = True +    def __init__(self, data=None, status=None, headers=None, +                 renderer=None, media_type=None): +        """ +        Alters the init arguments slightly. +        For example, drop 'template_name', and instead use 'data'. -    def __init__(self, content=None, status=None, headers=None, view=None, -                 request=None, renderer_classes=None, format=None): -        # First argument taken by `SimpleTemplateResponse.__init__` is template_name, -        # which we don't need +        Setting 'renderer' and 'media_type' will typically be defered, +        For example being set automatically by the `APIView`. +        """          super(Response, self).__init__(None, status=status) - -        self.raw_content = content -        self.has_content_body = content is not None +        self.data = data          self.headers = headers and headers[:] or [] -        self.view = view -        self.request = request -        self.renderer_classes = renderer_classes -        self.format = format - -    def get_renderers(self): -        """ -        Instantiates and returns the list of renderers the response will use. -        """ -        if self.renderer_classes is None: -            renderer_classes = api_settings.DEFAULT_RENDERERS -        else: -            renderer_classes = self.renderer_classes - -        if self.format: -            return [cls(self.view) for cls in renderer_classes -                    if cls.format == self.format] -        return [cls(self.view) for cls in renderer_classes] +        self.renderer = renderer +        self.media_type = media_type      @property      def rendered_content(self): -        """ -        The final rendered content. Accessing this attribute triggers the -        complete rendering cycle: selecting suitable renderer, setting -        response's actual content type, rendering data. -        """ -        renderer, media_type = self._determine_renderer() - -        # Set the media type of the response -        self['Content-Type'] = renderer.media_type - -        # Render the response content -        if self.has_content_body: -            return renderer.render(self.raw_content, media_type) -        return renderer.render() - -    def render(self): -        try: -            return super(Response, self).render() -        except NotAcceptable: -            response = self._get_406_response() -            return response.render() +        self['Content-Type'] = self.media_type +        if self.data is None: +            return self.renderer.render() +        return self.renderer.render(self.data, self.media_type)      @property      def status_text(self): @@ -100,74 +37,3 @@ class Response(SimpleTemplateResponse):          Provided for convenience.          """          return STATUS_CODE_TEXT.get(self.status_code, '') - -    def _determine_accept_list(self): -        """ -        Returns a list of accepted media types. This list is determined from : - -            1. overload with `_ACCEPT_QUERY_PARAM` -            2. `Accept` header of the request - -        If those are useless, a default value is returned instead. -        """ -        request = self.request - -        if (self._ACCEPT_QUERY_PARAM and -            request.GET.get(self._ACCEPT_QUERY_PARAM, None)): -            # Use _accept parameter override -            return [request.GET.get(self._ACCEPT_QUERY_PARAM)] -        elif (self._IGNORE_IE_ACCEPT_HEADER and -              'HTTP_USER_AGENT' in request.META and -              MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and -              request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'): -            # Ignore MSIE's broken accept behavior except for AJAX requests -            # and do something sensible instead -            return ['text/html', '*/*'] -        elif 'HTTP_ACCEPT' in request.META: -            # Use standard HTTP Accept negotiation -            return [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')] -        else: -            # No accept header specified -            return ['*/*'] - -    def _determine_renderer(self): -        """ -        Determines the appropriate renderer for the output, given the list of -        accepted media types, and the :attr:`renderer_classes` set on this class. - -        Returns a 2-tuple of `(renderer, media_type)` - -        See: RFC 2616, Section 14 -        http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html -        """ - -        renderers = self.get_renderers() -        accepts = self._determine_accept_list() - -        # Not acceptable response - Ignore accept header. -        if self.status_code == 406: -            return (renderers[0], renderers[0].media_type) - -        # 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 :) -        #     Worst case is we're looping over len(accept_list) * len(self.renderers) -        for media_type_set in order_by_precedence(accepts): -            for renderer in renderers: -                for media_type in media_type_set: -                    if renderer.can_handle_response(media_type): -                        return renderer, media_type - -        # No acceptable renderers were found -        raise NotAcceptable - -    def _get_406_response(self): -        renderer = self.renderer_classes[0] -        return Response( -            { -                'detail': 'Could not satisfy the client\'s Accept header', -                'available_types': [renderer.media_type -                                    for renderer in self.renderer_classes] -            }, -            status=status.HTTP_406_NOT_ACCEPTABLE, -            view=self.view, request=self.request, renderer_classes=[renderer]) diff --git a/djangorestframework/settings.py b/djangorestframework/settings.py index e5181f4b..95215712 100644 --- a/djangorestframework/settings.py +++ b/djangorestframework/settings.py @@ -38,6 +38,7 @@ DEFAULTS = {      ),      'DEFAULT_PERMISSIONS': (),      'DEFAULT_THROTTLES': (), +    'DEFAULT_CONTENT_NEGOTIATION': 'djangorestframework.contentnegotiation.DefaultContentNegotiation',      'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',      'UNAUTHENTICATED_TOKEN': None, @@ -45,7 +46,8 @@ DEFAULTS = {      'FORM_METHOD_OVERRIDE': '_method',      'FORM_CONTENT_OVERRIDE': '_content',      'FORM_CONTENTTYPE_OVERRIDE': '_content_type', -    'URL_ACCEPT_OVERRIDE': '_accept', +    'URL_ACCEPT_OVERRIDE': 'accept', +    'IGNORE_MSIE_ACCEPT_HEADER': True,      'FORMAT_SUFFIX_KWARG': 'format'  } @@ -58,8 +60,9 @@ IMPORT_STRINGS = (      'DEFAULT_AUTHENTICATION',      'DEFAULT_PERMISSIONS',      'DEFAULT_THROTTLES', +    'DEFAULT_CONTENT_NEGOTIATION',      'UNAUTHENTICATED_USER', -    'UNAUTHENTICATED_TOKEN' +    'UNAUTHENTICATED_TOKEN',  ) @@ -68,7 +71,7 @@ def perform_import(val, setting):      If the given setting is a string import notation,      then perform the necessary import or imports.      """ -    if val is None or setting not in IMPORT_STRINGS: +    if val is None or not setting in IMPORT_STRINGS:          return val      if isinstance(val, basestring): @@ -88,10 +91,7 @@ def import_from_string(val, setting):          module_path, class_name = '.'.join(parts[:-1]), parts[-1]          module = importlib.import_module(module_path)          return getattr(module, class_name) -    except Exception, e: -        import traceback -        tb = traceback.format_exc() -        import pdb; pdb.set_trace() +    except:          msg = "Could not import '%s' for API setting '%s'" % (val, setting)          raise ImportError(msg) diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py index 51e3660c..74eae438 100644 --- a/djangorestframework/tests/request.py +++ b/djangorestframework/tests/request.py @@ -7,7 +7,7 @@ from django.test import TestCase, Client  from djangorestframework import status  from djangorestframework.authentication import SessionAuthentication -from djangorestframework.utils import RequestFactory +from djangorestframework.compat import RequestFactory  from djangorestframework.parsers import (      FormParser,      MultiPartParser, @@ -22,33 +22,21 @@ factory = RequestFactory()  class TestMethodOverloading(TestCase): -    def test_GET_method(self): +    def test_method(self):          """ -        GET requests identified. +        Request methods should be same as underlying request.          """ -        request = factory.get('/') +        request = Request(factory.get('/'))          self.assertEqual(request.method, 'GET') - -    def test_POST_method(self): -        """ -        POST requests identified. -        """ -        request = factory.post('/') +        request = Request(factory.post('/'))          self.assertEqual(request.method, 'POST') -    def test_HEAD_method(self): -        """ -        HEAD requests identified. -        """ -        request = factory.head('/') -        self.assertEqual(request.method, 'HEAD') -      def test_overloaded_method(self):          """          POST requests can be overloaded to another method by setting a          reserved form field          """ -        request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'}) +        request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'}))          self.assertEqual(request.method, 'DELETE') @@ -57,14 +45,14 @@ class TestContentParsing(TestCase):          """          Ensure request.DATA returns None for GET request with no content.          """ -        request = factory.get('/') +        request = Request(factory.get('/'))          self.assertEqual(request.DATA, None)      def test_standard_behaviour_determines_no_content_HEAD(self):          """          Ensure request.DATA returns None for HEAD request.          """ -        request = factory.head('/') +        request = Request(factory.head('/'))          self.assertEqual(request.DATA, None)      def test_standard_behaviour_determines_form_content_POST(self): @@ -72,8 +60,8 @@ class TestContentParsing(TestCase):          Ensure request.DATA returns content for POST request with form content.          """          data = {'qwerty': 'uiop'} -        parsers = (FormParser, MultiPartParser) -        request = factory.post('/', data, parser=parsers) +        request = Request(factory.post('/', data)) +        request.parser_classes = (FormParser, MultiPartParser)          self.assertEqual(request.DATA.items(), data.items())      def test_standard_behaviour_determines_non_form_content_POST(self): @@ -83,9 +71,8 @@ class TestContentParsing(TestCase):          """          content = 'qwerty'          content_type = 'text/plain' -        parsers = (PlainTextParser,) -        request = factory.post('/', content, content_type=content_type, -                               parsers=parsers) +        request = Request(factory.post('/', content, content_type=content_type)) +        request.parser_classes = (PlainTextParser,)          self.assertEqual(request.DATA, content)      def test_standard_behaviour_determines_form_content_PUT(self): @@ -93,17 +80,17 @@ class TestContentParsing(TestCase):          Ensure request.DATA returns content for PUT request with form content.          """          data = {'qwerty': 'uiop'} -        parsers = (FormParser, MultiPartParser)          from django import VERSION          if VERSION >= (1, 5):              from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart -            request = factory.put('/', encode_multipart(BOUNDARY, data), parsers=parsers, -                                  content_type=MULTIPART_CONTENT) +            request = Request(factory.put('/', encode_multipart(BOUNDARY, data), +                                  content_type=MULTIPART_CONTENT))          else: -            request = factory.put('/', data, parsers=parsers) +            request = Request(factory.put('/', data)) +        request.parser_classes = (FormParser, MultiPartParser)          self.assertEqual(request.DATA.items(), data.items())      def test_standard_behaviour_determines_non_form_content_PUT(self): @@ -113,9 +100,8 @@ class TestContentParsing(TestCase):          """          content = 'qwerty'          content_type = 'text/plain' -        parsers = (PlainTextParser, ) -        request = factory.put('/', content, content_type=content_type, -                              parsers=parsers) +        request = Request(factory.put('/', content, content_type=content_type)) +        request.parser_classes = (PlainTextParser, )          self.assertEqual(request.DATA, content)      def test_overloaded_behaviour_allows_content_tunnelling(self): @@ -128,8 +114,8 @@ class TestContentParsing(TestCase):              Request._CONTENT_PARAM: content,              Request._CONTENTTYPE_PARAM: content_type          } -        parsers = (PlainTextParser, ) -        request = factory.post('/', data, parsers=parsers) +        request = Request(factory.post('/', data)) +        request.parser_classes = (PlainTextParser, )          self.assertEqual(request.DATA, content)      # def test_accessing_post_after_data_form(self): diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index 0483d826..cdddfff4 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -4,10 +4,10 @@ import unittest  from django.conf.urls.defaults import patterns, url, include  from django.test import TestCase -from djangorestframework.response import Response, NotAcceptable +from djangorestframework.response import Response  from djangorestframework.views import APIView  from djangorestframework.compat import RequestFactory -from djangorestframework import status +from djangorestframework import status, exceptions  from djangorestframework.renderers import (      BaseRenderer,      JSONRenderer, @@ -91,7 +91,7 @@ class TestResponseDetermineRenderer(TestCase):          accept_list = ['application/json']          renderer_classes = (MockPickleRenderer, )          response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes) -        self.assertRaises(NotAcceptable, response._determine_renderer) +        self.assertRaises(exceptions.NotAcceptable, response._determine_renderer)  class TestResponseRenderContent(TestCase): diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index bb5bb6d7..f53ac0b8 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,9 +1,6 @@  from django.utils.encoding import smart_unicode  from django.utils.xmlutils import SimplerXMLGenerator -  from djangorestframework.compat import StringIO -from djangorestframework.compat import RequestFactory as DjangoRequestFactory -from djangorestframework.request import Request  import re  import xml.etree.ElementTree as ET @@ -102,38 +99,3 @@ class XMLRenderer():  def dict2xml(input):      return XMLRenderer().dict2xml(input) - - -class RequestFactory(DjangoRequestFactory): -    """ -    Replicate RequestFactory, but return Request, not HttpRequest. -    """ -    def get(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).get(*args, **kwargs) -        return Request(request, parsers) - -    def post(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).post(*args, **kwargs) -        return Request(request, parsers) - -    def put(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).put(*args, **kwargs) -        return Request(request, parsers) - -    def delete(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).delete(*args, **kwargs) -        return Request(request, parsers) - -    def head(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).head(*args, **kwargs) -        return Request(request, parsers) - -    def options(self, *args, **kwargs): -        parsers = kwargs.pop('parsers', None) -        request = super(RequestFactory, self).options(*args, **kwargs) -        return Request(request, parsers) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 9debee19..2629663a 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -54,11 +54,14 @@ def _camelcase_to_spaces(content):  class APIView(_View): +    settings = api_settings +      renderer_classes = api_settings.DEFAULT_RENDERERS      parser_classes = api_settings.DEFAULT_PARSERS      authentication_classes = api_settings.DEFAULT_AUTHENTICATION      throttle_classes = api_settings.DEFAULT_THROTTLES      permission_classes = api_settings.DEFAULT_PERMISSIONS +    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION      @classmethod      def as_view(cls, **initkwargs): @@ -169,6 +172,27 @@ class APIView(_View):          """          return self.renderer_classes[0] +    def get_format_suffix(self, **kwargs): +        """ +        Determine if the request includes a '.json' style format suffix +        """ +        if self.settings.FORMAT_SUFFIX_KWARG: +            return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) + +    def get_renderers(self, format=None): +        """ +        Instantiates and returns the list of renderers that this view can use. +        """ +        return [renderer(self) for renderer in self.renderer_classes] + +    def filter_renderers(self, renderers, format=None): +        """ +        If format suffix such as '.json' is supplied, filter the +        list of valid renderers for this request. +        """ +        return [renderer for renderer in renderers +                if renderer.can_handle_format(format)] +      def get_permissions(self):          """          Instantiates and returns the list of permissions that this view requires. @@ -177,10 +201,28 @@ class APIView(_View):      def get_throttles(self):          """ -        Instantiates and returns the list of thottles that this view requires. +        Instantiates and returns the list of thottles that this view uses.          """          return [throttle(self) for throttle in self.throttle_classes] +    def content_negotiation(self, request): +        """ +        Determine which renderer and media type to use render the response. +        """ +        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 = self.filter_renderers(renderers, self.format) +            if not renderers: +                self.format404 = True +                return (fallback, fallback.media_type) + +        conneg = self.content_negotiation_class() +        return conneg.negotiate(request, renderers) +      def check_permissions(self, request, obj=None):          """          Check if request should be permitted. @@ -204,35 +246,43 @@ class APIView(_View):          return Request(request, parser_classes=self.parser_classes,                         authentication_classes=self.authentication_classes) +    def initial(self, request, *args, **kwargs): +        """ +        Runs anything that needs to occur prior to calling the method handlers. +        """ +        self.format = self.get_format_suffix(**kwargs) +        self.renderer, self.media_type = self.content_negotiation(request) +        self.check_permissions(request) +        self.check_throttles(request) +        # If the request included a non-existant .format URL suffix, +        # raise 404, but only after first making permission checks. +        if getattr(self, 'format404', None): +            raise Http404() +      def finalize_response(self, request, response, *args, **kwargs):          """          Returns the final response object.          """          if isinstance(response, Response): -            response.view = self -            response.request = request -            response.renderer_classes = self.renderer_classes -            if api_settings.FORMAT_SUFFIX_KWARG: -                response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None) +            response.renderer = self.renderer +            response.media_type = self.media_type          for key, value in self.headers.items():              response[key] = value          return response -    def initial(self, request, *args, **kwargs): -        """ -        Runs anything that needs to occur prior to calling the method handlers. -        """ -        self.check_permissions(request) -        self.check_throttles(request) -      def handle_exception(self, exc):          """          Handle any exception that occurs, by returning an appropriate response,          or re-raising the error.          """ -        if isinstance(exc, exceptions.Throttled): +        if isinstance(exc, exceptions.NotAcceptable): +            # 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              self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait          if isinstance(exc, exceptions.APIException): @@ -250,14 +300,8 @@ class APIView(_View):      @csrf_exempt      def dispatch(self, request, *args, **kwargs):          """ -        `APIView.dispatch()` is pretty much the same as Django's regular -        `View.dispatch()`, except that it includes hooks to: - -        * Initialize the request object. -        * Finalize the response object. -        * Handle exceptions that occur in the handler method. -        * An initial hook for code such as permission checking that should -          occur prior to running the method handlers. +        `.dispatch()` is pretty much the same as Django's regular dispatch, +        but with extra hooks for startup, finalize, and exception handling.          """          request = self.initialize_request(request, *args, **kwargs)          self.request = request @@ -270,7 +314,8 @@ class APIView(_View):              # Get the appropriate handler method              if request.method.lower() in self.http_method_names: -                handler = getattr(self, request.method.lower(), self.http_method_not_allowed) +                handler = getattr(self, request.method.lower(), +                                  self.http_method_not_allowed)              else:                  handler = self.http_method_not_allowed  | 
