aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdocs/api-guide/authentication.md14
-rw-r--r--docs/api-guide/content-negotiation.md6
-rw-r--r--docs/api-guide/fields.md13
-rw-r--r--docs/api-guide/filtering.md14
-rwxr-xr-xdocs/api-guide/generic-views.md12
-rw-r--r--docs/api-guide/pagination.md7
-rw-r--r--docs/api-guide/parsers.md6
-rw-r--r--docs/api-guide/permissions.md18
-rw-r--r--docs/api-guide/relations.md11
-rw-r--r--docs/api-guide/renderers.md9
-rw-r--r--docs/api-guide/reverse.md4
-rw-r--r--docs/api-guide/routers.md7
-rw-r--r--docs/api-guide/serializers.md7
-rw-r--r--docs/api-guide/settings.md34
-rw-r--r--docs/api-guide/status-codes.md1
-rw-r--r--docs/api-guide/testing.md23
-rw-r--r--docs/api-guide/throttling.md4
-rw-r--r--docs/api-guide/viewsets.md9
-rw-r--r--docs/index.md2
-rw-r--r--docs/template.html1
-rw-r--r--docs/topics/credits.md8
-rw-r--r--docs/topics/release-notes.md19
-rwxr-xr-xmkdocs.py1
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/fields.py21
-rw-r--r--rest_framework/filters.py5
-rw-r--r--rest_framework/generics.py11
-rw-r--r--rest_framework/renderers.py5
-rw-r--r--rest_framework/settings.py6
-rw-r--r--rest_framework/test.py2
-rw-r--r--rest_framework/tests/test_description.py9
-rw-r--r--rest_framework/tests/test_fields.py9
-rw-r--r--rest_framework/tests/test_files.py37
-rw-r--r--rest_framework/tests/test_testing.py30
-rw-r--r--rest_framework/tests/test_throttling.py33
-rw-r--r--rest_framework/throttling.py3
-rw-r--r--rest_framework/utils/breadcrumbs.py5
-rw-r--r--rest_framework/utils/formatting.py42
-rw-r--r--rest_framework/views.py48
39 files changed, 409 insertions, 89 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index b1ab4622..f30b16ed 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -46,6 +46,11 @@ The default authentication schemes may be set globally, using the `DEFAULT_AUTHE
You can also set the authentication scheme on a per-view or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.authentication import SessionAuthentication, BasicAuthentication
+ from rest_framework.permissions import IsAuthenticated
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
@@ -157,11 +162,16 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
+ from django.dispatch import receiver
+ from rest_framework.authtoken.models import Token
+
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
+Note that you'll want to ensure you place this code snippet in an installed `models.py` module, or some other location that will be imported by Django on startup.
+
If you've already created some users, you can generate tokens for all existing users like this:
from django.contrib.auth.models import User
@@ -336,6 +346,10 @@ If the `.authenticate_header()` method is not overridden, the authentication sch
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
+ from django.contrib.auth.models import User
+ from rest_framework import authentication
+ from rest_framework import exceptions
+
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md
index 2a774278..94dd59ca 100644
--- a/docs/api-guide/content-negotiation.md
+++ b/docs/api-guide/content-negotiation.md
@@ -54,6 +54,8 @@ The `select_renderer()` method should return a two-tuple of (renderer instance,
The following is a custom content negotiation class which ignores the client
request when selecting the appropriate parser or renderer.
+ from rest_framework.negotiation import BaseContentNegotiation
+
class IgnoreClientContentNegotiation(BaseContentNegotiation):
def select_parser(self, request, parsers):
"""
@@ -77,6 +79,10 @@ The default content negotiation class may be set globally, using the `DEFAULT_CO
You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views.
+ from myapp.negotiation import IgnoreClientContentNegotiation
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class NoNegotiationView(APIView):
"""
An example view that does not perform content negotiation.
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index d69730c9..962c49e2 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -78,6 +78,9 @@ A generic, **read-only** field. You can use this field for any attribute that d
For example, using the following model.
+ from django.db import models
+ from django.utils.timezone import now
+
class Account(models.Model):
owner = models.ForeignKey('auth.user')
name = models.CharField(max_length=100)
@@ -85,13 +88,14 @@ For example, using the following model.
payment_expiry = models.DateTimeField()
def has_expired(self):
- now = datetime.datetime.now()
- return now > self.payment_expiry
+ return now() > self.payment_expiry
A serializer definition that looked like this:
+ from rest_framework import serializers
+
class AccountSerializer(serializers.HyperlinkedModelSerializer):
- expired = Field(source='has_expired')
+ expired = serializers.Field(source='has_expired')
class Meta:
fields = ('url', 'owner', 'name', 'expired')
@@ -125,12 +129,11 @@ The `ModelField` class is generally intended for internal use, but can be used b
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
- from rest_framework import serializers
from django.contrib.auth.models import User
from django.utils.timezone import now
+ from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
-
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
class Meta:
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 05c997a3..649462da 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -20,6 +20,10 @@ You can do so by filtering based on the value of `request.user`.
For example:
+ from myapp.models import Purchase
+ from myapp.serializers import PurchaseSerializer
+ from rest_framework import generics
+
class PurchaseList(generics.ListAPIView)
serializer_class = PurchaseSerializer
@@ -90,6 +94,11 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE
You can also set the filter backends on a per-view, or per-viewset basis,
using the `GenericAPIView` class based views.
+ from django.contrib.auth.models import User
+ from myapp.serializers import UserSerializer
+ from rest_framework import filters
+ from rest_framework import generics
+
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
@@ -150,6 +159,11 @@ This will automatically create a `FilterSet` class for the given fields, and wil
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
+ import django_filters
+ from myapp.models import Product
+ from myapp.serializers import ProductSerializer
+ from rest_framework import generics
+
class ProductFilter(django_filters.FilterSet):
min_price = django_filters.NumberFilter(lookup_type='gte')
max_price = django_filters.NumberFilter(lookup_type='lte')
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 32a4feef..931cae54 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -17,6 +17,11 @@ If the generic views don't suit the needs of your API, you can drop down to usin
Typically when using the generic views, you'll override the view, and set several class attributes.
+ from django.contrib.auth.models import User
+ from myapp.serializers import UserSerializer
+ from rest_framework import generics
+ from rest_framework.permissions import IsAdminUser
+
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
@@ -108,7 +113,12 @@ For example:
filter = {}
for field in self.multiple_lookup_fields:
filter[field] = self.kwargs[field]
- return get_object_or_404(queryset, **filter)
+
+ obj = get_object_or_404(queryset, **filter)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
#### `get_serializer_class(self)`
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 912ce41b..ca0174b7 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -13,6 +13,7 @@ REST framework includes a `PaginationSerializer` class that makes it easy to ret
Let's start by taking a look at an example from the Django documentation.
from django.core.paginator import Paginator
+
objects = ['john', 'paul', 'george', 'ringo']
paginator = Paginator(objects, 2)
page = paginator.page(1)
@@ -22,6 +23,7 @@ Let's start by taking a look at an example from the Django documentation.
At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results.
from rest_framework.pagination import PaginationSerializer
+
serializer = PaginationSerializer(instance=page)
serializer.data
# {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']}
@@ -114,6 +116,9 @@ You can also override the name used for the object list field, by setting the `r
For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
+ from rest_framework import pagination
+ from rest_framework import serializers
+
class LinksSerializer(serializers.Serializer):
next = pagination.NextPageField(source='*')
prev = pagination.PreviousPageField(source='*')
@@ -135,7 +140,7 @@ To have your custom pagination serializer be used by default, use the `DEFAULT_P
Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
- class PaginatedListView(ListAPIView):
+ class PaginatedListView(generics.ListAPIView):
model = ExampleModel
pagination_serializer_class = CustomPaginationSerializer
paginate_by = 10
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 5bd79a31..1030fcb6 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -34,9 +34,13 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE
)
}
-You can also set the renderers used for an individual view, or viewset,
+You can also set the parsers used for an individual view, or viewset,
using the `APIView` class based views.
+ from rest_framework.parsers import YAMLParser
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
"""
A view that can accept POST requests with YAML content.
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 096ef369..12aa4c18 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -25,9 +25,17 @@ Object level permissions are run by REST framework's generic views when `.get_ob
As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object.
If you're writing your own views and want to enforce object level permissions,
-you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
+or if you override the `get_object` method on a generic view, then you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
+
This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions.
+For example:
+
+ def get_object(self):
+ obj = get_object_or_404(self.get_queryset())
+ self.check_object_permissions(self.request, obj)
+ return obj
+
## Setting the permission policy
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
@@ -47,6 +55,10 @@ If not specified, this setting defaults to allowing unrestricted access:
You can also set the authentication policy on a per-view, or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.permissions import IsAuthenticated
+ from rest_framework.responses import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
permission_classes = (IsAuthenticated,)
@@ -157,6 +169,8 @@ For more details see the [2.2 release announcement][2.2-announcement].
The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
+ from rest_framework import permissions
+
class BlacklistPermission(permissions.BasePermission):
"""
Global permission check for blacklisted IPs.
@@ -196,7 +210,7 @@ The following third party packages are also available.
## DRF Any Permissions
-The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to the REST framework. Only one of the given permissions has to be true in order to get access to the view.
+The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view.
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 829a3c54..aa14bc72 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -76,7 +76,7 @@ This field is read only.
For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer):
- tracks = PrimaryKeyRelatedField(many=True, read_only=True)
+ tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Album
@@ -110,8 +110,8 @@ By default this field is read-write, although you can change this behavior using
For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer):
- tracks = HyperlinkedRelatedField(many=True, read_only=True,
- view_name='track-detail')
+ tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True,
+ view_name='track-detail')
class Meta:
model = Album
@@ -148,7 +148,8 @@ By default this field is read-write, although you can change this behavior using
For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer):
- tracks = SlugRelatedField(many=True, read_only=True, slug_field='title')
+ tracks = serializers.SlugRelatedField(many=True, read_only=True,
+ slug_field='title')
class Meta:
model = Album
@@ -183,7 +184,7 @@ When using `SlugRelatedField` as a read-write field, you will normally want to e
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
- track_listing = HyperlinkedIdentityField(view_name='track-list')
+ track_listing = serializers.HyperlinkedIdentityField(view_name='track-list')
class Meta:
model = Album
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index bb3d2015..7fc1fc1f 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -30,11 +30,16 @@ The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CL
You can also set the renderers used for an individual view, or viewset,
using the `APIView` class based views.
+ from django.contrib.auth.models import User
+ from rest_framework.renderers import JSONRenderer, YAMLRenderer
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class UserCountView(APIView):
"""
- A view that returns the count of active users, in JSON or JSONp.
+ A view that returns the count of active users, in JSON or YAML.
"""
- renderer_classes = (JSONRenderer, JSONPRenderer)
+ renderer_classes = (JSONRenderer, YAMLRenderer)
def get(self, request, format=None):
user_count = User.objects.filter(active=True).count()
diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md
index 94262366..383eca4c 100644
--- a/docs/api-guide/reverse.md
+++ b/docs/api-guide/reverse.md
@@ -27,13 +27,13 @@ Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except t
You should **include the request as a keyword argument** to the function, for example:
- import datetime
from rest_framework.reverse import reverse
from rest_framework.views import APIView
+ from django.utils.timezone import now
class APIRootView(APIView):
def get(self, request):
- year = datetime.datetime.now().year
+ year = now().year
data = {
...
'year-summary-url': reverse('year-summary', args=[year], request=request)
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 072a2e79..fb48197e 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -14,6 +14,8 @@ REST framework adds support for automatic URL routing to Django, and provides yo
Here's an example of a simple URL conf, that uses `DefaultRouter`.
+ from rest_framework import routers
+
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
@@ -40,6 +42,9 @@ The example above would generate the following URL patterns:
Any methods on the viewset decorated with `@link` or `@action` will also be routed.
For example, given a method like this on the `UserViewSet` class:
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import action
+
@action(permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
@@ -120,6 +125,8 @@ The arguments to the `Route` named tuple are:
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
+ from rest_framework.routers import Route, SimpleRouter
+
class ReadOnlyRouter(SimpleRouter):
"""
A router for read-only APIs, which doesn't use trailing slashes.
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index bbc8d019..d9fd4643 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -28,6 +28,8 @@ We'll declare a serializer that we can use to serialize and deserialize `Comment
Declaring a serializer looks very similar to declaring a form:
+ from rest_framework import serializers
+
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
@@ -59,6 +61,8 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments.
At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`.
+ from rest_framework.renderers import JSONRenderer
+
json = JSONRenderer().render(serializer.data)
json
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
@@ -67,6 +71,9 @@ At this point we've translated the model instance into Python native datatypes.
Deserialization is similar. First we parse a stream into Python native datatypes...
+ from StringIO import StringIO
+ from rest_framework.parsers import JSONParser
+
stream = StringIO(json)
data = JSONParser().parse(stream)
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 0be0eb24..fe7925a5 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -274,6 +274,40 @@ Default: `['iso-8601']`
---
+## View names and descriptions
+
+**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
+
+#### VIEW_NAME_FUNCTION
+
+A string representing the function that should be used when generating view names.
+
+This should be a function with the following signature:
+
+ view_name(cls, suffix=None)
+
+* `cls`: The view class. Typically the name function would inspect the name of the class when generating a descriptive name, by accessing `cls.__name__`.
+* `suffix`: The optional suffix used when differentiating individual views in a viewset.
+
+Default: `'rest_framework.views.get_view_name'`
+
+#### VIEW_DESCRIPTION_FUNCTION
+
+A string representing the function that should be used when generating view descriptions.
+
+This setting can be changed to support markup styles other than the default markdown. For example, you can use it to support `rst` markup in your view docstrings being output in the browsable API.
+
+This should be a function with the following signature:
+
+ view_description(cls, html=False)
+
+* `cls`: The view class. Typically the description function would inspect the docstring of the class when generating a description, by accessing `cls.__doc__`
+* `html`: A boolean indicating if HTML output is required. `True` when used in the browsable API, and `False` when used in generating `OPTIONS` responses.
+
+Default: `'rest_framework.views.get_view_description'`
+
+---
+
## Miscellaneous settings
#### FORMAT_SUFFIX_KWARG
diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md
index db2e059c..409f659b 100644
--- a/docs/api-guide/status-codes.md
+++ b/docs/api-guide/status-codes.md
@@ -9,6 +9,7 @@
Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable.
from rest_framework import status
+ from rest_framework.response import Response
def empty_view(self):
content = {'please move along': 'nothing to see here'}
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index 92f8d54a..35c1f766 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -2,7 +2,7 @@
# Testing
-> Code without tests is broken as designed
+> Code without tests is broken as designed.
>
> — [Jacob Kaplan-Moss][cite]
@@ -16,6 +16,8 @@ Extends [Django's existing `RequestFactory` class][requestfactory].
The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available.
+ from rest_framework.test import APIRequestFactory
+
# Using the standard RequestFactory API to create a form POST request
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'})
@@ -49,6 +51,8 @@ For example, using `APIRequestFactory`, you can make a form PUT request like so:
Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself:
+ from django.test.client import encode_multipart, RequestFactory
+
factory = RequestFactory()
data = {'title': 'remember to email dave'}
content = encode_multipart('BoUnDaRyStRiNg', data)
@@ -72,6 +76,12 @@ To forcibly authenticate a request, use the `force_authenticate()` method.
The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set.
+For example, when forcibly authenticating using a token, you might do something like the following:
+
+ user = User.objects.get(username='olivia')
+ request = factory.get('/accounts/django-superstars/')
+ force_authenticate(request, user=user, token=user.token)
+
---
**Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called.
@@ -105,6 +115,8 @@ Extends [Django's existing `Client` class][client].
The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
+ from rest_framework.test import APIClient
+
client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')
@@ -131,8 +143,11 @@ The `login` method is appropriate for testing APIs that use session authenticati
The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client.
+ from rest_framework.authtoken.models import Token
+ from rest_framework.test import APIClient
+
# Include an appropriate `Authorization:` header on all requests.
- token = Token.objects.get(username='lauren')
+ token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
@@ -190,10 +205,10 @@ You can use any of REST framework's test case classes as you would for the regul
Ensure we can create a new account object.
"""
url = reverse('account-list')
- data = {'name': 'DabApps'}
+ expected = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, data)
+ self.assertEqual(response.data, expected)
---
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index 56f32f58..42f9c228 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -43,6 +43,10 @@ The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `mi
You can also set the throttling policy on a per-view or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.response import Response
+ from rest_framework.throttling import UserRateThrottle
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
throttle_classes = (UserRateThrottle,)
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 0c68afb0..61f9d2f8 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -19,6 +19,12 @@ Typically, rather than explicitly registering the views in a viewset in the urlc
Let's define a simple viewset that can be used to list or retrieve all the users in the system.
+ from django.contrib.auth.models import User
+ from django.shortcuts import get_object_or_404
+ from myapps.serializers import UserSerializer
+ from rest_framework import viewsets
+ from rest_framewor.responses import Response
+
class UserViewSet(viewsets.ViewSet):
"""
A simple ViewSet that for listing or retrieving users.
@@ -41,6 +47,9 @@ If we need to, we can bind this viewset into two separate views, like so:
Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated.
+ from myapp.views import UserViewSet
+ from rest_framework.routers import DefaultRouter
+
router = DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = router.urls
diff --git a/docs/index.md b/docs/index.md
index 99cd6b88..a0ae2984 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -164,6 +164,7 @@ The API guide is your complete reference manual to all the functionality provide
* [Returning URLs][reverse]
* [Exceptions][exceptions]
* [Status codes][status]
+* [Testing][testing]
* [Settings][settings]
## Topics
@@ -288,6 +289,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[reverse]: api-guide/reverse.md
[exceptions]: api-guide/exceptions.md
[status]: api-guide/status-codes.md
+[testing]: api-guide/testing.md
[settings]: api-guide/settings.md
[documenting-your-api]: topics/documenting-your-api.md
diff --git a/docs/template.html b/docs/template.html
index 27bc1062..a20c8111 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -89,6 +89,7 @@
<li><a href="{{ base_url }}/api-guide/reverse{{ suffix }}">Returning URLs</a></li>
<li><a href="{{ base_url }}/api-guide/exceptions{{ suffix }}">Exceptions</a></li>
<li><a href="{{ base_url }}/api-guide/status-codes{{ suffix }}">Status codes</a></li>
+ <li><a href="{{ base_url }}/api-guide/testing{{ suffix }}">Testing</a></li>
<li><a href="{{ base_url }}/api-guide/settings{{ suffix }}">Settings</a></li>
</ul>
</li>
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index b91f051b..16ea78c4 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -156,6 +156,10 @@ The following people have helped make REST framework great.
* Veronica Lynn - [kolvia]
* Dan Stephenson - [etos]
* Martin Clement - [martync]
+* Jeremy Satterfield - [jsatt]
+* Christopher Paolini - [chrispaolini]
+* Filipe A Ximenes - [filipeximenes]
+* Ramiro Morales - [ramiro]
Many thanks to everyone who's contributed to the project.
@@ -348,3 +352,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[kolvia]: https://github.com/kolvia
[etos]: https://github.com/etos
[martync]: https://github.com/martync
+[jsatt]: https://github.com/jsatt
+[chrispaolini]: https://github.com/chrispaolini
+[filipeximenes]: https://github.com/filipeximenes
+[ramiro]: https://github.com/ramiro
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index d379ab74..af90b1ea 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,25 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
+### Master
+
+* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
+* Bugfix: `required=True` argument fixed for boolean serializer fields.
+* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
+
+### 2.3.7
+
+**Date**: 16th August 2013
+
+* Added `APITestClient`, `APIRequestFactory` and `APITestCase` etc...
+* Refactor `SessionAuthentication` to allow esier override for CSRF exemption.
+* Remove 'Hold down "Control" message from help_text' widget messaging when not appropriate.
+* Added admin configuration for auth tokens.
+* Bugfix: `AnonRateThrottle` fixed to not throttle authenticated users.
+* Bugfix: Don't set `X-Throttle-Wait-Seconds` when throttle does not have `wait` value.
+* Bugfix: Fixed `PATCH` button title in browsable API.
+* Bugfix: Fix issue with OAuth2 provider naive datetimes.
+
### 2.3.6
**Date**: 27th June 2013
diff --git a/mkdocs.py b/mkdocs.py
index 1e3f1db3..13228a0c 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -69,6 +69,7 @@ path_list = [
'api-guide/reverse.md',
'api-guide/exceptions.md',
'api-guide/status-codes.md',
+ 'api-guide/testing.md',
'api-guide/settings.md',
'topics/documenting-your-api.md',
'topics/ajax-csrf-cors.md',
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 776618ac..087808e0 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '2.3.6'
+__version__ = '2.3.7'
VERSION = __version__ # synonym
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f9931887..3e0ca1a1 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -16,6 +16,7 @@ from django.core import validators
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db.models.fields import BLANK_CHOICE_DASH
+from django.http import QueryDict
from django.forms import widgets
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
@@ -307,7 +308,10 @@ class WritableField(Field):
try:
if self.use_files:
files = files or {}
- native = files[field_name]
+ try:
+ native = files[field_name]
+ except KeyError:
+ native = data[field_name]
else:
native = data[field_name]
except KeyError:
@@ -399,10 +403,15 @@ class BooleanField(WritableField):
}
empty = False
- # Note: we set default to `False` in order to fill in missing value not
- # supplied by html form. TODO: Fix so that only html form input gets
- # this behavior.
- default = False
+ def field_from_native(self, data, files, field_name, into):
+ # HTML checkboxes do not explicitly represent unchecked as `False`
+ # we deal with that here...
+ if isinstance(data, QueryDict):
+ self.default = False
+
+ return super(BooleanField, self).field_from_native(
+ data, files, field_name, into
+ )
def from_native(self, value):
if value in ('true', 't', 'True', '1'):
@@ -924,7 +933,7 @@ class ImageField(FileField):
if f is None:
return None
- from compat import Image
+ from rest_framework.compat import Image
assert Image is not None, 'PIL must be installed for ImageField support'
# We need to get a file object for PIL. We might have a path or we might
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c058bc71..4079e1bd 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -109,8 +109,7 @@ class OrderingFilter(BaseFilterBackend):
def get_ordering(self, request):
"""
- Search terms are set by a ?search=... query parameter,
- and may be comma and/or whitespace delimited.
+ Ordering is set by a comma delimited ?ordering=... query parameter.
"""
params = request.QUERY_PARAMS.get(self.ordering_param)
if params:
@@ -134,7 +133,7 @@ class OrderingFilter(BaseFilterBackend):
ordering = self.remove_invalid_fields(queryset, ordering)
if not ordering:
- # Use 'ordering' attribtue by default
+ # Use 'ordering' attribute by default
ordering = self.get_default_ordering(view)
if ordering:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 99e9782e..5ecf6310 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -14,6 +14,15 @@ from rest_framework.settings import api_settings
import warnings
+def strict_positive_int(integer_string):
+ """
+ Cast a string to a strictly positive integer.
+ """
+ ret = int(integer_string)
+ if ret <= 0:
+ raise ValueError()
+ return ret
+
def get_object_or_404(queryset, **filter_kwargs):
"""
Same as Django's standard shortcut, but make sure to raise 404
@@ -135,7 +144,7 @@ class GenericAPIView(views.APIView):
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
page = page_kwarg or page_query_param or 1
try:
- page_number = int(page)
+ page_number = strict_positive_int(page)
except ValueError:
if page == 'last':
page_number = paginator.num_pages
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 3a03ca33..1006e26c 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -24,7 +24,6 @@ from rest_framework.settings import api_settings
from rest_framework.request import clone_request
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.utils.formatting import get_view_name, get_view_description
from rest_framework import exceptions, parsers, status, VERSION
@@ -498,10 +497,10 @@ class BrowsableAPIRenderer(BaseRenderer):
return GenericContentForm()
def get_name(self, view):
- return get_view_name(view.__class__, getattr(view, 'suffix', None))
+ return view.get_view_name()
def get_description(self, view):
- return get_view_description(view.__class__, html=True)
+ return view.get_view_description(html=True)
def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path)
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 8fd177d5..7d25e513 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -69,6 +69,10 @@ DEFAULTS = {
'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None,
+ # View configuration
+ 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
+ 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
+
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
@@ -125,6 +129,8 @@ IMPORT_STRINGS = (
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
+ 'VIEW_NAME_FUNCTION',
+ 'VIEW_DESCRIPTION_FUNCTION'
)
diff --git a/rest_framework/test.py b/rest_framework/test.py
index a18f5a29..234d10a4 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -134,6 +134,8 @@ class APIClient(APIRequestFactory, DjangoClient):
"""
self.handler._force_user = user
self.handler._force_token = token
+ if user is None:
+ self.logout() # Also clear any possible session info if required
def request(self, **kwargs):
# Ensure that any credentials set get added to every request.
diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py
index 8019f5ec..4c03c1de 100644
--- a/rest_framework/tests/test_description.py
+++ b/rest_framework/tests/test_description.py
@@ -6,7 +6,6 @@ from rest_framework.compat import apply_markdown, smart_text
from rest_framework.views import APIView
from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
from rest_framework.tests.description import UTF8_TEST_DOCSTRING
-from rest_framework.utils.formatting import get_view_name, get_view_description
# We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring
@@ -58,7 +57,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_name(MockView), 'Mock')
+ self.assertEqual(MockView().get_view_name(), 'Mock')
def test_view_description_uses_docstring(self):
"""Ensure view descriptions are based on the docstring."""
@@ -78,7 +77,7 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #"""
- self.assertEqual(get_view_description(MockView), DESCRIPTION)
+ self.assertEqual(MockView().get_view_description(), DESCRIPTION)
def test_view_description_supports_unicode(self):
"""
@@ -86,7 +85,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
self.assertEqual(
- get_view_description(ViewWithNonASCIICharactersInDocstring),
+ ViewWithNonASCIICharactersInDocstring().get_view_description(),
smart_text(UTF8_TEST_DOCSTRING)
)
@@ -97,7 +96,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_description(MockView), '')
+ self.assertEqual(MockView().get_view_description(), '')
def test_markdown(self):
"""
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
index 6836ec86..ebccba7d 100644
--- a/rest_framework/tests/test_fields.py
+++ b/rest_framework/tests/test_fields.py
@@ -896,3 +896,12 @@ class CustomIntegerField(TestCase):
self.assertFalse(serializer.is_valid())
+class BooleanField(TestCase):
+ """
+ Tests for BooleanField
+ """
+ def test_boolean_required(self):
+ class BooleanRequiredSerializer(serializers.Serializer):
+ bool_field = serializers.BooleanField(required=True)
+
+ self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py
index 487046ac..c13c38b8 100644
--- a/rest_framework/tests/test_files.py
+++ b/rest_framework/tests/test_files.py
@@ -7,13 +7,13 @@ import datetime
class UploadedFile(object):
- def __init__(self, file, created=None):
+ def __init__(self, file=None, created=None):
self.file = file
self.created = created or datetime.datetime.now()
class UploadedFileSerializer(serializers.Serializer):
- file = serializers.FileField()
+ file = serializers.FileField(required=False)
created = serializers.DateTimeField()
def restore_object(self, attrs, instance=None):
@@ -47,5 +47,36 @@ class FileSerializerTests(TestCase):
now = datetime.datetime.now()
serializer = UploadedFileSerializer(data={'created': now})
+ self.assertTrue(serializer.is_valid())
+ self.assertEqual(serializer.object.created, now)
+ self.assertIsNone(serializer.object.file)
+
+ def test_remove_with_empty_string(self):
+ """
+ Passing empty string as data should cause file to be removed
+
+ Test for:
+ https://github.com/tomchristie/django-rest-framework/issues/937
+ """
+ now = datetime.datetime.now()
+ file = BytesIO(six.b('stuff'))
+ file.name = 'stuff.txt'
+ file.size = len(file.getvalue())
+
+ uploaded_file = UploadedFile(file=file, created=now)
+
+ serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
+ self.assertTrue(serializer.is_valid())
+ self.assertEqual(serializer.object.created, uploaded_file.created)
+ self.assertIsNone(serializer.object.file)
+
+ def test_validation_error_with_non_file(self):
+ """
+ Passing non-files should raise a validation error.
+ """
+ now = datetime.datetime.now()
+ errmsg = 'No file was submitted. Check the encoding type on the form.'
+
+ serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
self.assertFalse(serializer.is_valid())
- self.assertIn('file', serializer.errors)
+ self.assertEqual(serializer.errors, {'file': [errmsg]})
diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py
index 49d45fc2..48b8956b 100644
--- a/rest_framework/tests/test_testing.py
+++ b/rest_framework/tests/test_testing.py
@@ -17,8 +17,18 @@ def view(request):
})
+@api_view(['GET', 'POST'])
+def session_view(request):
+ active_session = request.session.get('active_session', False)
+ request.session['active_session'] = True
+ return Response({
+ 'active_session': active_session
+ })
+
+
urlpatterns = patterns('',
url(r'^view/$', view),
+ url(r'^session-view/$', session_view),
)
@@ -46,6 +56,26 @@ class TestAPITestClient(TestCase):
response = self.client.get('/view/')
self.assertEqual(response.data['user'], 'example')
+ def test_force_authenticate_with_sessions(self):
+ """
+ Setting `.force_authenticate()` forcibly authenticates each request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ self.client.force_authenticate(user)
+
+ # First request does not yet have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
+ # Subsequant requests have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], True)
+
+ # Force authenticating as `None` should also logout the user session.
+ self.client.force_authenticate(None)
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
def test_csrf_exempt_by_default(self):
"""
By default, the test client is CSRF exempt.
diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py
index 19bc691a..41bff692 100644
--- a/rest_framework/tests/test_throttling.py
+++ b/rest_framework/tests/test_throttling.py
@@ -7,7 +7,7 @@ from django.contrib.auth.models import User
from django.core.cache import cache
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
-from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle
+from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle
from rest_framework.response import Response
@@ -21,6 +21,14 @@ class User3MinRateThrottle(UserRateThrottle):
scope = 'minutes'
+class NonTimeThrottle(BaseThrottle):
+ def allow_request(self, request, view):
+ if not hasattr(self.__class__, 'called'):
+ self.__class__.called = True
+ return True
+ return False
+
+
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
@@ -35,6 +43,13 @@ class MockView_MinuteThrottling(APIView):
return Response('foo')
+class MockView_NonTimeThrottling(APIView):
+ throttle_classes = (NonTimeThrottle,)
+
+ def get(self, request):
+ return Response('foo')
+
+
class ThrottlingTests(TestCase):
def setUp(self):
"""
@@ -140,6 +155,22 @@ class ThrottlingTests(TestCase):
(80, None)
))
+ def test_non_time_throttle(self):
+ """
+ Ensure for second based throttles.
+ """
+ request = self.factory.get('/')
+
+ self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('X-Throttle-Wait-Seconds' in response)
+
+ self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('X-Throttle-Wait-Seconds' in response)
+
class ScopedRateThrottleTests(TestCase):
"""
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index f6bb1cc8..65b45593 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -96,6 +96,9 @@ class SimpleRateThrottle(BaseThrottle):
return True
self.key = self.get_cache_key(request, view)
+ if self.key is None:
+ return True
+
self.history = cache.get(self.key, [])
self.now = self.timer()
diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py
index d51374b0..0384faba 100644
--- a/rest_framework/utils/breadcrumbs.py
+++ b/rest_framework/utils/breadcrumbs.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
from django.core.urlresolvers import resolve, get_script_prefix
-from rest_framework.utils.formatting import get_view_name
def get_breadcrumbs(url):
@@ -29,8 +28,8 @@ def get_breadcrumbs(url):
# Don't list the same view twice in a row.
# Probably an optional trailing slash.
if not seen or seen[-1] != view:
- suffix = getattr(view, 'suffix', None)
- name = get_view_name(view.cls, suffix)
+ instance = view.cls()
+ name = instance.get_view_name()
breadcrumbs_list.insert(0, (name, prefix + url))
seen.append(view)
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 4bec8387..4b59ba84 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -5,11 +5,13 @@ from __future__ import unicode_literals
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown, smart_text
+from rest_framework.compat import apply_markdown
+from rest_framework.settings import api_settings
+from textwrap import dedent
import re
-def _remove_trailing_string(content, trailing):
+def remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view classes.
@@ -19,10 +21,14 @@ def _remove_trailing_string(content, trailing):
return content
-def _remove_leading_indent(content):
+def dedent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
+
+ Note that python's `textwrap.dedent` doesn't quite cut it,
+ as it fails to dedent multiline docstrings that include
+ unindented text on the initial line.
"""
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
@@ -31,11 +37,10 @@ def _remove_leading_indent(content):
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
- content = content.strip('\n')
- return content
+ return content.strip()
-def _camelcase_to_spaces(content):
+def camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes.
@@ -44,31 +49,6 @@ def _camelcase_to_spaces(content):
content = re.sub(camelcase_boundry, ' \\1', content).strip()
return ' '.join(content.split('_')).title()
-
-def get_view_name(cls, suffix=None):
- """
- Return a formatted name for an `APIView` class or `@api_view` function.
- """
- name = cls.__name__
- name = _remove_trailing_string(name, 'View')
- name = _remove_trailing_string(name, 'ViewSet')
- name = _camelcase_to_spaces(name)
- if suffix:
- name += ' ' + suffix
- return name
-
-
-def get_view_description(cls, html=False):
- """
- Return a description for an `APIView` class or `@api_view` function.
- """
- description = cls.__doc__ or ''
- description = _remove_leading_indent(smart_text(description))
- if html:
- return markup_description(description)
- return description
-
-
def markup_description(description):
"""
Apply HTML markup to the given description.
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 37bba7f0..727a9f95 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -8,11 +8,29 @@ from django.http import Http404
from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import View, HttpResponseBase
+from rest_framework.compat import smart_text, HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.utils.formatting import get_view_name, get_view_description
+from rest_framework.utils import formatting
+
+
+def get_view_name(cls, suffix=None):
+ name = cls.__name__
+ name = formatting.remove_trailing_string(name, 'View')
+ name = formatting.remove_trailing_string(name, 'ViewSet')
+ name = formatting.camelcase_to_spaces(name)
+ if suffix:
+ name += ' ' + suffix
+
+ return name
+
+def get_view_description(cls, html=False):
+ description = cls.__doc__ or ''
+ description = formatting.dedent(smart_text(description))
+ if html:
+ return formatting.markup_description(description)
+ return description
class APIView(View):
@@ -110,6 +128,22 @@ class APIView(View):
'request': getattr(self, 'request', None)
}
+ def get_view_name(self):
+ """
+ Return the view name, as used in OPTIONS responses and in the
+ browsable API.
+ """
+ func = api_settings.VIEW_NAME_FUNCTION
+ return func(self.__class__, getattr(self, 'suffix', None))
+
+ def get_view_description(self, html=False):
+ """
+ Return some descriptive text for the view, as used in OPTIONS responses
+ and in the browsable API.
+ """
+ func = api_settings.VIEW_DESCRIPTION_FUNCTION
+ return func(self.__class__, html)
+
# API policy instantiation methods
def get_format_suffix(self, **kwargs):
@@ -269,7 +303,7 @@ class APIView(View):
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
- if isinstance(exc, exceptions.Throttled):
+ if isinstance(exc, exceptions.Throttled) and exc.wait is not None:
# Throttle wait header
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
@@ -342,16 +376,12 @@ class APIView(View):
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
"""
-
- # This is used by ViewSets to disambiguate instance vs list views
- view_name_suffix = getattr(self, 'suffix', None)
-
# By default we can't provide any form-like information, however the
# generic views override this implementation and add additional
# information for POST and PUT methods, based on the serializer.
ret = SortedDict()
- ret['name'] = get_view_name(self.__class__, view_name_suffix)
- ret['description'] = get_view_description(self.__class__)
+ ret['name'] = self.get_view_name()
+ ret['description'] = self.get_view_description()
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
ret['parses'] = [parser.media_type for parser in self.parser_classes]
return ret