diff options
| author | Tom Christie | 2012-08-26 23:06:52 +0100 | 
|---|---|---|
| committer | Tom Christie | 2012-08-26 23:06:52 +0100 | 
| commit | 73cc77553ed5411f1959a51574b156a47ad5340d (patch) | |
| tree | 0b3a0f707fefd116a274adcfa80364596e8df76d | |
| parent | edd8f5963cb32063931a1557d3c6ac29d19b3425 (diff) | |
| download | django-rest-framework-73cc77553ed5411f1959a51574b156a47ad5340d.tar.bz2 | |
Drop ImmediateResponse
| -rw-r--r-- | djangorestframework/exceptions.py | 33 | ||||
| -rw-r--r-- | djangorestframework/permissions.py | 18 | ||||
| -rw-r--r-- | djangorestframework/response.py | 22 | ||||
| -rw-r--r-- | djangorestframework/tests/response.py | 2 | ||||
| -rw-r--r-- | djangorestframework/tests/throttling.py | 6 | ||||
| -rw-r--r-- | djangorestframework/views.py | 28 | 
6 files changed, 56 insertions, 53 deletions
diff --git a/djangorestframework/exceptions.py b/djangorestframework/exceptions.py index 3f7e9029..315c1b1d 100644 --- a/djangorestframework/exceptions.py +++ b/djangorestframework/exceptions.py @@ -1,9 +1,15 @@ +""" +Handled exceptions raised by REST framework. + +In addition Django's built in 403 and 404 exceptions are handled. +(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) +"""  from djangorestframework import status  class ParseError(Exception):      status_code = status.HTTP_400_BAD_REQUEST -    default_detail = 'Malformed request' +    default_detail = 'Malformed request.'      def __init__(self, detail=None):          self.detail = detail or self.default_detail @@ -11,7 +17,7 @@ class ParseError(Exception):  class PermissionDenied(Exception):      status_code = status.HTTP_403_FORBIDDEN -    default_detail = 'You do not have permission to access this resource' +    default_detail = 'You do not have permission to access this resource.'      def __init__(self, detail=None):          self.detail = detail or self.default_detail @@ -19,19 +25,30 @@ class PermissionDenied(Exception):  class MethodNotAllowed(Exception):      status_code = status.HTTP_405_METHOD_NOT_ALLOWED -    default_detail = "Method '%s' not allowed" +    default_detail = "Method '%s' not allowed." -    def __init__(self, method, detail): +    def __init__(self, method, detail=None):          self.detail = (detail or self.default_detail) % method  class UnsupportedMediaType(Exception):      status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE -    default_detail = "Unsupported media type '%s' in request" +    default_detail = "Unsupported media type '%s' in request."      def __init__(self, media_type, detail=None):          self.detail = (detail or self.default_detail) % media_type -# class Throttled(Exception): -#     def __init__(self, detail): -#         self.detail = detail + +class Throttled(Exception): +    status_code = status.HTTP_429_TOO_MANY_REQUESTS +    default_detail = "Request was throttled. Expected available in %d seconds." + +    def __init__(self, wait, detail=None): +        import math +        self.detail = (detail or self.default_detail) % int(math.ceil(wait)) + + +REST_FRAMEWORK_EXCEPTIONS = ( +    ParseError, PermissionDenied, MethodNotAllowed, +    UnsupportedMediaType, Throttled +) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index b56d8a32..bdda4def 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -6,9 +6,7 @@ Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` c  """  from django.core.cache import cache -from djangorestframework import status -from djangorestframework.exceptions import PermissionDenied -from djangorestframework.response import ImmediateResponse +from djangorestframework.exceptions import PermissionDenied, Throttled  import time  __all__ = ( @@ -24,11 +22,6 @@ __all__ = (  SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -_503_SERVICE_UNAVAILABLE = ImmediateResponse( -    {'detail': 'request was throttled'}, -    status=status.HTTP_503_SERVICE_UNAVAILABLE) - -  class BasePermission(object):      """      A base class from which all permission classes should inherit. @@ -192,7 +185,7 @@ class BaseThrottle(BasePermission):          """          self.history.insert(0, self.now)          cache.set(self.key, self.history, self.duration) -        header = 'status=SUCCESS; next=%s sec' % self.next() +        header = 'status=SUCCESS; next=%.2f sec' % self.next()          self.view.headers['X-Throttle'] = header      def throttle_failure(self): @@ -200,9 +193,10 @@ class BaseThrottle(BasePermission):          Called when a request to the API has failed due to throttling.          Raises a '503 service unavailable' response.          """ -        header = 'status=FAILURE; next=%s sec' % self.next() +        wait = self.next() +        header = 'status=FAILURE; next=%.2f sec' % wait          self.view.headers['X-Throttle'] = header -        raise _503_SERVICE_UNAVAILABLE +        raise Throttled(wait)      def next(self):          """ @@ -215,7 +209,7 @@ class BaseThrottle(BasePermission):          available_requests = self.num_requests - len(self.history) + 1 -        return '%.2f' % (remaining_duration / float(available_requests)) +        return remaining_duration / float(available_requests)  class PerUserThrottling(BaseThrottle): diff --git a/djangorestframework/response.py b/djangorestframework/response.py index ea9a938c..ac16e79a 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -161,25 +161,3 @@ class Response(SimpleTemplateResponse):              },              status=status.HTTP_406_NOT_ACCEPTABLE,              view=self.view, request=self.request, renderers=[renderer]) - - -class ImmediateResponse(Response, Exception): -    """ -    An exception representing an Response that should be returned immediately. -    Any content should be serialized as-is, without being filtered. -    """ -    #TODO: this is just a temporary fix, the whole rendering/support for ImmediateResponse, should be remade : see issue #163 - -    def render(self): -        try: -            return super(Response, self).render() -        except ImmediateResponse: -            renderer, media_type = self._determine_renderer() -            self.renderers.remove(renderer) -            if len(self.renderers) == 0: -                raise RuntimeError('Caught an ImmediateResponse while '\ -                    'trying to render an ImmediateResponse') -            return self.render() - -    def __init__(self, *args, **kwargs): -        self.response = Response(*args, **kwargs) diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index 07d0f4fb..ded0a3da 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -4,7 +4,7 @@ import unittest  from django.conf.urls.defaults import patterns, url, include  from django.test import TestCase -from djangorestframework.response import Response, NotAcceptable, ImmediateResponse +from djangorestframework.response import Response, NotAcceptable  from djangorestframework.views import View  from djangorestframework.compat import RequestFactory  from djangorestframework import status diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py index d307cd32..ad22d2d2 100644 --- a/djangorestframework/tests/throttling.py +++ b/djangorestframework/tests/throttling.py @@ -45,7 +45,7 @@ class ThrottlingTests(TestCase):          request = self.factory.get('/')          for dummy in range(4):              response = MockView.as_view()(request) -        self.assertEqual(503, response.status_code) +        self.assertEqual(429, response.status_code)      def set_throttle_timer(self, view, value):          """ @@ -62,7 +62,7 @@ class ThrottlingTests(TestCase):          request = self.factory.get('/')          for dummy in range(4):              response = MockView.as_view()(request) -        self.assertEqual(503, response.status_code) +        self.assertEqual(429, response.status_code)          # Advance the timer by one second          self.set_throttle_timer(MockView, 1) @@ -90,7 +90,7 @@ class ThrottlingTests(TestCase):          """          Ensure request rate is limited globally per View for PerViewThrottles          """ -        self.ensure_is_throttled(MockView_PerViewThrottling, 503) +        self.ensure_is_throttled(MockView_PerViewThrottling, 429)      def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):          """ diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 36d05721..a7540e0c 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -6,12 +6,14 @@ By setting or modifying class attributes on your view, you change it's predefine  """  import re +from django.core.exceptions import PermissionDenied +from django.http import Http404  from django.utils.html import escape  from django.utils.safestring import mark_safe  from django.views.decorators.csrf import csrf_exempt  from djangorestframework.compat import View as DjangoView, apply_markdown -from djangorestframework.response import Response, ImmediateResponse +from djangorestframework.response import Response  from djangorestframework.request import Request  from djangorestframework import renderers, parsers, authentication, permissions, status, exceptions @@ -219,13 +221,27 @@ class View(DjangoView):              response[key] = value          return response +    def handle_exception(self, exc): +        """ +        Handle any exception that occurs, by returning an appropriate response, +        or re-raising the error. +        """ +        if isinstance(exc, exceptions.REST_FRAMEWORK_EXCEPTIONS): +            return Response({'detail': exc.detail}, status=exc.status_code) +        elif isinstance(exc, Http404): +            return Response({'detail': 'Not found'}, +                            status=status.HTTP_404_NOT_FOUND) +        elif isinstance(exc, PermissionDenied): +            return Response({'detail': 'Permission denied'}, +                            status=status.HTTP_403_FORBIDDEN) +        raise +      # Note: session based authentication is explicitly CSRF validated,      # all other authentication is CSRF exempt.      @csrf_exempt      def dispatch(self, request, *args, **kwargs):          request = Request(request, parsers=self.parsers, authentication=self.authentication)          self.request = request -          self.args = args          self.kwargs = kwargs          self.headers = self.default_response_headers @@ -244,10 +260,8 @@ class View(DjangoView):              response = handler(request, *args, **kwargs) -        except ImmediateResponse, exc: -            response = exc.response -        except (exceptions.ParseError, exceptions.PermissionDenied) as exc: -            response = Response({'detail': exc.detail}, status=exc.status_code) +        except Exception as exc: +            response = self.handle_exception(exc)          self.response = self.final(request, response, *args, **kwargs)          return self.response @@ -265,4 +279,4 @@ class View(DjangoView):              for name, field in form.fields.iteritems():                  field_name_types[name] = field.__class__.__name__              content['fields'] = field_name_types -        raise ImmediateResponse(content, status=status.HTTP_200_OK) +        raise Response(content, status=status.HTTP_200_OK)  | 
