Fix @api_view decorator tests

This commit is contained in:
Tom Christie 2012-09-26 21:47:19 +01:00
parent 622e001e0b
commit 0cc7030aab
9 changed files with 178 additions and 60 deletions

View File

@ -2,4 +2,23 @@
# Parsers # Parsers
## .parse(request) > Machine interacting web services tend to use more
structured formats for sending data than form-encoded, since they're
sending more complex data than simple forms
>
> — Malcom Tredinnick, [Django developers group][cite]
## JSONParser
## YAMLParser
## XMLParser
## FormParser
## MultiPartParser
## Custom parsers
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion

View File

@ -2,5 +2,22 @@
# Renderers # Renderers
## .render(response) > Before a TemplateResponse instance can be returned to the client, it must be rendered. The rendering process takes the intermediate representation of template and context, and turns it into the final byte stream that can be served to the client.
>
> — [Django documentation][cite]
## JSONRenderer
## JSONPRenderer
## YAMLRenderer
## XMLRenderer
## DocumentingHTMLRenderer
## TemplatedHTMLRenderer
## Custom renderers
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process

View File

@ -4,7 +4,7 @@
> If you're doing REST-based web service stuff ... you should ignore request.POST. > If you're doing REST-based web service stuff ... you should ignore request.POST.
> >
> Malcom Tredinnick, [Django developers group][cite] > — Malcom Tredinnick, [Django developers group][cite]
REST framework's `Request` class extends the standard `HttpRequest`, adding support for parsing multiple content types, allowing browser-based `PUT`, `DELETE` and other methods, and adding flexible per-request authentication. REST framework's `Request` class extends the standard `HttpRequest`, adding support for parsing multiple content types, allowing browser-based `PUT`, `DELETE` and other methods, and adding flexible per-request authentication.

View File

@ -1,4 +1,4 @@
<a class="github" href="views.py"></a> <a class="github" href="decorators.py"></a> <a class="github" href="views.py"></a>
# Views # Views
@ -6,36 +6,108 @@
> >
> &mdash; [Reinout van Rees][cite] > &mdash; [Reinout van Rees][cite]
REST framework provides a simple `APIView` class, built on Django's `django.generics.views.View`. The `APIView` class ensures five main things: REST framework provides an `APIView` class, which subclasses Django's `View` class.
1. Any requests inside the view will become `Request` instances. `APIView` classes are different from regular `View` classes in the following ways:
2. `Request` instances will have their `renderers` and `authentication` attributes automatically set.
3. `Response` instances will have their `parsers` and `serializer` attributes automatically set.
4. `APIException` exceptions will be caught and return appropriate responses.
5. Any permissions provided will be checked prior to passing the request to a handler method.
Additionally there are a some minor extras, such as providing a default `options` handler, setting some common headers on the response prior to return, and providing the useful `initial()` and `final()` hooks. * Requests passed to the handler methods will be REST framework's `Request` instances, not Django's `HttpRequest` instances.
* Handler methods may return REST framework's `Response`, instead of Django's `HttpResponse`. The view will manage content negotiation and setting the correct renderer on the response.
* Any `APIException` exceptions will be caught and mediated into appropriate responses.
* Incoming requests will be authenticated and appropriate permission and/or throttle checks will be run before dispatching the request to the handler method.
## APIView Using the `APIView` class is pretty much the same as using a regular `View` class, as usual, the incoming request is dispatched to an appropriate handler method such as `.get()` or `.post()`. Additionally, a number of attributes may be set on the class that control various aspects of the API policy.
## Method handlers For example:
Describe that APIView handles regular .get(), .post(), .put(), .delete() etc... class ListUsers(APIView):
"""
View to list all users in the system.
## .initial(request, *args, **kwargs) * Requires token authentication.
* Only admin users are able to access this view.
"""
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAdmin,)
## .final(request, response, *args, **kwargs) def get(self, request, format=None):
"""
Return a list of all users.
"""
users = [user.username for user in User.objects.all()]
return Response(users)
## .parsers ## API policy attributes
## .renderers The following attributes control the pluggable aspects of API views.
## .serializer ### .renderer_classes
## .authentication ### .parser_classes
## .permissions ### .authentication_classes
## .headers ### .throttle_classes
### .permission_classes
### .content_negotiation_class
## API policy instantiation methods
The following methods are used by REST framework to instantiate the various pluggable API policies. You won't typically need to override these methods.
### .get_renderers(self)
### .get_parsers(self)
### .get_authenticators(self)
### .get_throttles(self)
### .get_permissions(self)
### .get_content_negotiator(self)
## API policy implementation methods
The following methods are called before dispatching to the handler method.
### .check_permissions(...)
### .check_throttles(...)
### .perform_content_negotiation(...)
## Dispatch methods
The following methods are called directly by the view's `.dispatch()` method.
These perform any actions that need to occur before or after calling the handler methods such as `.get()`, `.post()`, `put()` and `.delete()`.
### .initial(self, request, *args, **kwargs)
Performs any actions that need to occur before the handler method gets called.
This method is used to enforce permissions and throttling, and perform content negotiation.
You won't typically need to override this method.
### .handle_exception(self, exc)
Any exception thrown by the handler method will be passed to this method, which either returns a `Response` instance, or re-raises the exception.
The default implementation handles any subclass of `rest_framework.exceptions.APIException`, as well as Django's `Http404` and `PermissionDenied` exceptions, and returns an appropriate error response.
If you need to customize the error responses your API returns you should subclass this method.
### .initialize_request(self, request, *args, **kwargs)
Ensures that the request object that is passed to the handler method is an instance of `Request`, rather than the usual Django `HttpRequest`.
You won't typically need to override this method.
### .finalize_response(self, request, response, *args, **kwargs)
Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotation.
You won't typically need to override this method.
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html

View File

@ -1,11 +1,3 @@
from functools import wraps
from django.utils.decorators import available_attrs
from django.core.exceptions import PermissionDenied
from rest_framework import exceptions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView

View File

@ -77,3 +77,10 @@ class Throttled(APIException):
self.detail = format % (self.wait, self.wait != 1 and 's' or '') self.detail = format % (self.wait, self.wait != 1 and 's' or '')
else: else:
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
class ConfigurationError(Exception):
"""
Indicates an internal server error.
"""
pass

View File

@ -39,6 +39,10 @@ DEFAULTS = {
'DEFAULT_THROTTLES': (), 'DEFAULT_THROTTLES': (),
'DEFAULT_CONTENT_NEGOTIATION': 'DEFAULT_CONTENT_NEGOTIATION':
'rest_framework.negotiation.DefaultContentNegotiation', 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_THROTTLE_RATES': {
'user': None,
'anon': None,
},
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,

View File

@ -1,10 +1,11 @@
from django.test import TestCase from django.test import TestCase
from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.compat import RequestFactory from rest_framework.compat import RequestFactory
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.authentication import BasicAuthentication from rest_framework.authentication import BasicAuthentication
from rest_framework.throttling import SimpleRateThottle from rest_framework.throttling import UserRateThrottle
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.decorators import ( from rest_framework.decorators import (
@ -23,7 +24,6 @@ class DecoratorTestCase(TestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
def _finalize_response(self, request, response, *args, **kwargs): def _finalize_response(self, request, response, *args, **kwargs):
print "HAI"
response.request = request response.request = request
return APIView.finalize_response(self, request, response, *args, **kwargs) return APIView.finalize_response(self, request, response, *args, **kwargs)
@ -87,21 +87,24 @@ class DecoratorTestCase(TestCase):
@api_view(['GET']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def view(request): def view(request):
self.assertEqual(request.permission_classes, [IsAuthenticated])
return Response({}) return Response({})
request = self.factory.get('/') request = self.factory.get('/')
view(request) response = view(request)
self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN)
# Doesn't look like this bits are working quite yet def test_throttle_classes(self):
class OncePerDayUserThrottle(UserRateThrottle):
rate = '1/day'
# def test_throttle_classes(self): @api_view(['GET'])
@throttle_classes([OncePerDayUserThrottle])
def view(request):
return Response({})
# @api_view(['GET']) request = self.factory.get('/')
# @throttle_classes([SimpleRateThottle]) response = view(request)
# def view(request): self.assertEquals(response.status_code, status.HTTP_200_OK)
# self.assertEqual(request.throttle_classes, [SimpleRateThottle])
# return Response({})
# request = self.factory.get('/') response = view(request)
# view(request) self.assertEquals(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)

View File

@ -1,5 +1,6 @@
import time import time
from django.core.cache import cache from django.core.cache import cache
from rest_framework import exceptions
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -49,8 +50,9 @@ class SimpleRateThottle(BaseThrottle):
def __init__(self, view): def __init__(self, view):
super(SimpleRateThottle, self).__init__(view) super(SimpleRateThottle, self).__init__(view)
rate = self.get_rate_description() if not getattr(self, 'rate', None):
self.num_requests, self.duration = self.parse_rate_description(rate) self.rate = self.get_rate()
self.num_requests, self.duration = self.parse_rate(self.rate)
def get_cache_key(self, request): def get_cache_key(self, request):
""" """
@ -61,21 +63,28 @@ class SimpleRateThottle(BaseThrottle):
""" """
raise NotImplementedError('.get_cache_key() must be overridden') raise NotImplementedError('.get_cache_key() must be overridden')
def get_rate_description(self): def get_rate(self):
""" """
Determine the string representation of the allowed request rate. Determine the string representation of the allowed request rate.
""" """
try: if not getattr(self, 'scope', None):
return self.rate msg = ("You must set either `.scope` or `.rate` for '%s' thottle" %
except AttributeError: self.__class__.__name__)
return self.settings.DEFAULT_THROTTLE_RATES.get(self.scope) raise exceptions.ConfigurationError(msg)
def parse_rate_description(self, rate): try:
return self.settings.DEFAULT_THROTTLE_RATES[self.scope]
except KeyError:
msg = "No default throttle rate set for '%s' scope" % self.scope
raise exceptions.ConfigurationError(msg)
def parse_rate(self, rate):
""" """
Given the request rate string, return a two tuple of: Given the request rate string, return a two tuple of:
<allowed number of requests>, <period of time in seconds> <allowed number of requests>, <period of time in seconds>
""" """
assert rate, "No throttle rate set for '%s'" % self.__class__.__name__ if rate is None:
return (None, None)
num, period = rate.split('/') num, period = rate.split('/')
num_requests = int(num) num_requests = int(num)
duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
@ -88,6 +97,9 @@ class SimpleRateThottle(BaseThrottle):
On success calls `throttle_success`. On success calls `throttle_success`.
On failure calls `throttle_failure`. On failure calls `throttle_failure`.
""" """
if self.rate is None:
return True
self.key = self.get_cache_key(request) self.key = self.get_cache_key(request)
self.history = cache.get(self.key, []) self.history = cache.get(self.key, [])
self.now = self.timer() self.now = self.timer()
@ -188,14 +200,6 @@ class ScopedRateThrottle(SimpleRateThottle):
self.scope = getattr(self.view, self.scope_attr, None) self.scope = getattr(self.view, self.scope_attr, None)
super(ScopedRateThrottle, self).__init__(view) super(ScopedRateThrottle, self).__init__(view)
def parse_rate_description(self, rate):
"""
Subclassed so that we don't fail if `view.throttle_scope` is not set.
"""
if not rate:
return (None, None)
return super(ScopedRateThrottle, self).parse_rate_description(rate)
def get_cache_key(self, request): def get_cache_key(self, request):
""" """
If `view.throttle_scope` is not set, don't apply this throttle. If `view.throttle_scope` is not set, don't apply this throttle.