Merge branch 'master' into 2.4.0

This commit is contained in:
Tom Christie 2013-08-19 20:58:28 +01:00
commit 28e44efe25
69 changed files with 1155 additions and 321 deletions

View File

@ -121,7 +121,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y
'rest_framework.authtoken'
)
Make sure to run `manage.py syncdb` after changing your settings.
Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below).
You'll also need to create tokens for your users.
@ -184,9 +184,11 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a
Note that the default `obtain_auth_token` view explicitly uses JSON requests and responses, rather than using default renderer and parser classes in your settings. If you need a customized version of the `obtain_auth_token` view, you can do so by overriding the `ObtainAuthToken` view class, and using that in your url conf instead.
#### Custom user models
#### Schema migrations
The `rest_framework.authtoken` app includes a south migration that will create the authtoken table. If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.
The `rest_framework.authtoken` app includes a south migration that will create the authtoken table.
If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.
You can do so by inserting a `needed_by` attribute in your user migration:
@ -201,6 +203,12 @@ You can do so by inserting a `needed_by` attribute in your user migration:
For more details, see the [south documentation on dependencies][south-dependencies].
Also note that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example:
python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`.
python manage.py migrate
python manage.py createsuperuser
## SessionAuthentication
This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website.

View File

@ -43,7 +43,11 @@ This is a valid approach as the HTTP spec deliberately underspecifies how a serv
It's unlikely that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`.
REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods.
REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods.
The `select_parser()` method should return one of the parser instances from the list of available parsers, or `None` if none of the parsers can handle the incoming request.
The `select_renderer()` method should return a two-tuple of (renderer instance, media type), or raise a `NotAcceptable` exception.
## Example
@ -61,6 +65,27 @@ request when selecting the appropriate parser or renderer.
"""
Select the first renderer in the `.renderer_classes` list.
"""
return renderers[0]
return (renderers[0], renderers[0].media_type)
## Setting the content negotiation
The default content negotiation class may be set globally, using the `DEFAULT_CONTENT_NEGOTIATION_CLASS` setting. For example, the following settings would use our example `IgnoreClientContentNegotiation` class.
REST_FRAMEWORK = {
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'myapp.negotiation.IgnoreClientContentNegotiation',
}
You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views.
class NoNegotiationView(APIView):
"""
An example view that does not perform content negotiation.
"""
content_negotiation_class = IgnoreClientContentNegotiation
def get(self, request, format=None):
return Response({
'accepted media type': request.accepted_renderer.media_type
})
[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

View File

@ -40,7 +40,7 @@ For more complex cases you might also want to override various methods on the vi
For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
url(r'^/users/', ListCreateAPIView.as_view(model=User) name='user-list')
url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')
---

View File

@ -147,7 +147,7 @@ If you need to test if a request is a read operation or a write operation, you s
**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required.
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explicit and obvious. The old style signature continues to work, but its use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
For more details see the [2.2 release announcement][2.2-announcement].
@ -188,6 +188,16 @@ Note that the generic views will check the appropriate object level permissions,
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details.
---
# Third party packages
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 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
[throttling]: throttling.md
@ -197,3 +207,4 @@ Also note that the generic views will only check the object-level permissions fo
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
[2.2-announcement]: ../topics/2.2-announcement.md
[filtering]: filtering.md
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions

View File

@ -39,7 +39,7 @@ In order to explain the various types of relational fields, we'll use a couple o
## RelatedField
`RelatedField` may be used to represent the target of the relationship using it's `__unicode__` method.
`RelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
For example, the following serializer.
@ -71,7 +71,7 @@ This field is read only.
## PrimaryKeyRelatedField
`PrimaryKeyRelatedField` may be used to represent the target of the relationship using it's primary key.
`PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key.
For example, the following serializer:
@ -252,7 +252,7 @@ If you want to implement a read-write relational field, you must also implement
## Example
For, example, we could define a relational field, to serialize a track to a custom string representation, using it's ordering, title, and duration.
For, example, we could define a relational field, to serialize a track to a custom string representation, using its ordering, title, and duration.
import time
@ -386,7 +386,7 @@ For more information see [the Django documentation on generic relations][generic
By default, relational fields that target a ``ManyToManyField`` with a
``through`` model specified are set to read-only.
If you exlicitly specify a relational field pointing to a
If you explicitly specify a relational field pointing to a
``ManyToManyField`` with a through model, be sure to set ``read_only``
to ``True``.

View File

@ -217,13 +217,31 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
**.charset**: `utf-8`
#### Customizing BrowsableAPIRenderer
By default the response content will be rendered with the highest priority renderer apart from `BrowseableAPIRenderer`. If you need to customize this behavior, for example to use HTML as the default return format, but use JSON in the browsable API, you can do so by overriding the `get_default_renderer()` method. For example:
class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_default_renderer(self, view):
return JSONRenderer()
## MultiPartRenderer
This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing].
**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg`
**.format**: `'.multipart'`
**.charset**: `utf-8`
---
# Custom renderers
To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
The method should return a bytestring, which wil be used as the body of the HTTP response.
The method should return a bytestring, which will be used as the body of the HTTP response.
The arguments passed to the `.render()` method are:
@ -373,6 +391,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[cors]: http://www.w3.org/TR/cors/
[cors-docs]: ../topics/ajax-csrf-cors.md
[testing]: testing.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[application/vnd.github+json]: http://developer.github.com/v3/media/

View File

@ -24,7 +24,7 @@ Unless you want to heavily customize REST framework for some reason, you should
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any Python primitives.
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object.
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primitive datatypes before creating the `Response` object.
You can use REST framework's `Serializer` classes to perform this data serialization, or use your own custom serialization.
@ -54,7 +54,7 @@ The rendered content of the response. The `.render()` method must have been cal
## .template_name
The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the reponse.
The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the response.
## .accepted_renderer

View File

@ -17,7 +17,7 @@ The advantages of doing so are:
REST framework provides two utility functions to make it more simple to return absolute URIs from your Web API.
There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink it's output for you, which makes browsing the API much easier.
There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier.
## reverse

View File

@ -38,7 +38,7 @@ The example above would generate the following URL patterns:
### Extra link and actions
Any methods on the viewset decorated with `@link` or `@action` will also be routed.
For example, a given method like this on the `UserViewSet` class:
For example, given a method like this on the `UserViewSet` class:
@action(permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
@ -66,7 +66,7 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
By default the URLs created by `SimpleRouter` are appending with a trailing slash.
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
router = SimpleRouter(trailing_slash=False)
@ -90,13 +90,13 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
router = DefaultRouter(trailing_slash=False)
# Custom Routers
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.
@ -139,7 +139,7 @@ The `SimpleRouter` class provides another example of setting the `.routes` attri
## Advanced custom routers
If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should insect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.

View File

@ -308,6 +308,12 @@ By default, all the model fields on the class will be mapped to corresponding se
Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Other models fields will be mapped to a corresponding serializer field.
---
**Note**: When validation is applied to a `ModelSerializer`, both the serializer fields, and their corresponding model fields must correctly validate. If you have optional fields on your model, make sure to correctly set `blank=True` on the model field, as well as setting `required=False` on the serializer field.
---
## Specifying which fields should be included
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`.
@ -397,7 +403,7 @@ You can change the field that is used for object lookups by setting the `lookup_
Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
class AccountSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
@ -423,7 +429,7 @@ You can create customized subclasses of `ModelSerializer` or `HyperlinkedModelSe
Doing so should be considered advanced usage, and will only be needed if you have some particular serializer requirements that you often need to repeat.
## Dynamically modifiying fields
## Dynamically modifying fields
Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer.
@ -443,7 +449,7 @@ For example, if you wanted to be able to set which fields should be used by a se
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instatiate the superclass normally
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields:

View File

@ -28,7 +28,7 @@ you should use the `api_settings` object. For example.
print api_settings.DEFAULT_AUTHENTICATION_CLASSES
The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
---
@ -149,6 +149,33 @@ Default: `None`
---
## Test settings
*The following settings control the behavior of APIRequestFactory and APIClient*
#### TEST_REQUEST_DEFAULT_FORMAT
The default format that should be used when making test requests.
This should match up with the format of one of the renderer classes in the `TEST_REQUEST_RENDERER_CLASSES` setting.
Default: `'multipart'`
#### TEST_REQUEST_RENDERER_CLASSES
The renderer classes that are supported when building test requests.
The format of any of these renderer classes may be used when constructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')`
Default:
(
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer'
)
---
## Browser overrides
*The following settings provide URL or form-based overrides of the default browser behavior.*
@ -247,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

257
docs/api-guide/testing.md Normal file
View File

@ -0,0 +1,257 @@
<a class="github" href="test.py"></a>
# Testing
> Code without tests is broken as designed
>
> &mdash; [Jacob Kaplan-Moss][cite]
REST framework includes a few helper classes that extend Django's existing test framework, and improve support for making API requests.
# APIRequestFactory
Extends [Django's existing `RequestFactory` class][requestfactory].
## Creating test requests
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.
# Using the standard RequestFactory API to create a form POST request
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'})
#### Using the `format` argument
Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example:
# Create a JSON POST request
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'}, format='json')
By default the available formats are `'multipart'` and `'json'`. For compatibility with Django's existing `RequestFactory` the default format is `'multipart'`.
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
#### Explicitly encoding the request body
If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example:
request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')
#### PUT and PATCH with form data
One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`.
For example, using `APIRequestFactory`, you can make a form PUT request like so:
factory = APIRequestFactory()
request = factory.put('/notes/547/', {'title': 'remember to email dave'})
Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself:
factory = RequestFactory()
data = {'title': 'remember to email dave'}
content = encode_multipart('BoUnDaRyStRiNg', data)
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
request = factory.put('/notes/547/', content, content_type=content_type)
## Forcing authentication
When testing views directly using a request factory, it's often convenient to be able to directly authenticate the request, rather than having to construct the correct authentication credentials.
To forcibly authenticate a request, use the `force_authenticate()` method.
factory = APIRequestFactory()
user = User.objects.get(username='olivia')
view = AccountDetail.as_view()
# Make an authenticated request to the view...
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user)
response = view(request)
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.
---
**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.
This means that setting attributes directly on the request object may not always have the effect you expect. For example, setting `.token` directly will have no effect, and setting `.user` directly will only work if session authentication is being used.
# Request will only authenticate if `SessionAuthentication` is in use.
request = factory.get('/accounts/django-superstars/')
request.user = user
response = view(request)
---
## Forcing CSRF validation
By default, requests created with `APIRequestFactory` will not have CSRF validation applied when passed to a REST framework view. If you need to explicitly turn CSRF validation on, you can do so by setting the `enforce_csrf_checks` flag when instantiating the factory.
factory = APIRequestFactory(enforce_csrf_checks=True)
---
**Note**: It's worth noting that Django's standard `RequestFactory` doesn't need to include this option, because when using regular Django the CSRF validation takes place in middleware, which is not run when testing views directly. When using REST framework, CSRF validation takes place inside the view, so the request factory needs to disable view-level CSRF checks.
---
# APIClient
Extends [Django's existing `Client` class][client].
## Making requests
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:
client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
## Authenticating
#### .login(**kwargs)
The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`.
# Make all requests in the context of a logged in session.
client = APIClient()
client.login(username='lauren', password='secret')
To logout, call the `logout` method as usual.
# Log out
client.logout()
The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API.
#### .credentials(**kwargs)
The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client.
# Include an appropriate `Authorization:` header on all requests.
token = Token.objects.get(username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments.
# Stop including any credentials
client.credentials()
The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes.
#### .force_authenticate(user=None, token=None)
Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated.
This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests.
user = User.objects.get(username='lauren')
client = APIClient()
client.force_authenticate(user=user)
To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`.
client.force_authenticate(user=None)
## CSRF validation
By default CSRF validation is not applied when using `APIClient`. If you need to explicitly enable CSRF validation, you can do so by setting the `enforce_csrf_checks` flag when instantiating the client.
client = APIClient(enforce_csrf_checks=True)
As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling `login()`.
---
# Test cases
REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`.
* `APISimpleTestCase`
* `APITransactionTestCase`
* `APITestCase`
* `APILiveServerTestCase`
## Example
You can use any of REST framework's test case classes as you would for the regular Django test case classes. The `self.client` attribute will be an `APIClient` instance.
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
class AccountTests(APITestCase):
def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data, data)
---
# Testing responses
## Checking the response data
When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response.
For example, it's easier to inspect `request.data`:
response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
Instead of inspecting the result of parsing `request.content`:
response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
## Rendering responses
If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response.
view = UserDetail.as_view()
request = factory.get('/users/4')
response = view(request, pk='4')
response.render() # Cannot access `response.content` without this.
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')
---
# Configuration
## Setting the default format
The default format used to make test requests may be set using the `TEST_REQUEST_DEFAULT_FORMAT` setting key. For example, to always use JSON for test requests by default instead of standard multipart form requests, set the following in your `settings.py` file:
REST_FRAMEWORK = {
...
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
## Setting the available formats
If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting.
For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file.
REST_FRAMEWORK = {
...
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.YAMLRenderer'
)
}
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
[configuration]: #configuration

View File

@ -12,7 +12,7 @@ As with permissions, multiple throttles may be used. Your API might have a rest
Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due to some services being particularly resource-intensive.
Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth, and a paid data service might want to throttle against a certain number of a records being accessed.
@ -44,7 +44,7 @@ You can also set the throttling policy on a per-view or per-viewset basis,
using the `APIView` class based views.
class ExampleView(APIView):
throttle_classes = (UserThrottle,)
throttle_classes = (UserRateThrottle,)
def get(self, request, format=None):
content = {
@ -55,7 +55,7 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
@api_view('GET')
@throttle_classes(UserThrottle)
@throttle_classes(UserRateThrottle)
def example_view(request, format=None):
content = {
'status': 'request was permitted'
@ -72,22 +72,22 @@ The throttle classes provided by REST framework use Django's cache backend. You
## AnonRateThrottle
The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
The `AnonRateThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
The allowed request rate is determined from one of the following (in order of preference).
* The `rate` property on the class, which may be provided by overriding `AnonThrottle` and setting the property.
* The `rate` property on the class, which may be provided by overriding `AnonRateThrottle` and setting the property.
* The `DEFAULT_THROTTLE_RATES['anon']` setting.
`AnonThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
`AnonRateThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
## UserRateThrottle
The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
The `UserRateThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
The allowed request rate is determined from one of the following (in order of preference).
* The `rate` property on the class, which may be provided by overriding `UserThrottle` and setting the property.
* The `rate` property on the class, which may be provided by overriding `UserRateThrottle` and setting the property.
* The `DEFAULT_THROTTLE_RATES['user']` setting.
An API may have multiple `UserRateThrottles` in place at the same time. To do so, override `UserRateThrottle` and set a unique "scope" for each class.
@ -113,11 +113,11 @@ For example, multiple user throttle rates could be implemented by using the foll
}
}
`UserThrottle` is suitable if you want simple global rate restrictions per-user.
`UserRateThrottle` is suitable if you want simple global rate restrictions per-user.
## ScopedRateThrottle
The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address.
The `ScopedRateThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address.
The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope".

View File

@ -110,7 +110,7 @@ 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.
Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotiation.
You won't typically need to override this method.
@ -137,11 +137,11 @@ The core of this functionality is the `api_view` decorator, which takes a list o
return Response({"message": "Hello, world!"})
This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings).
This view will use the default renderers, parsers, authentication classes etc specified in the [settings].
## API policy decorators
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.throttling import UserRateThrottle

View File

@ -98,8 +98,10 @@ For example:
from django.contrib.auth.models import User
from rest_framework import viewsets
from rest_framework import status
from rest_framework.decorators import action
from myapp.serializers import UserSerializer
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
@ -176,7 +178,7 @@ Note that you can use any of the standard attributes or method overrides provide
permission_classes = [IsAccountAdminOrReadOnly]
def get_queryset(self):
return request.user.accounts.all()
return self.request.user.accounts.all()
Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes.
@ -205,9 +207,9 @@ You may need to provide custom `ViewSet` classes that do not have the full set o
To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions:
class CreateListRetrieveViewSet(mixins.CreateMixin,
mixins.ListMixin,
mixins.RetrieveMixin,
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
"""
A viewset that provides `retrieve`, `update`, and `list` actions.

BIN
docs/img/autocomplete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -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

View File

@ -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>

View File

@ -136,15 +136,15 @@ Now becomes:
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but it's use will raise a `PendingDeprecationWarning`.
If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but its use will raise a `PendingDeprecationWarning`.
Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions.
### More explicit hyperlink relations behavior
When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fall back to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but it's use will raise a `PendingDeprecationWarning`.
From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`.
[xordoquy]: https://github.com/xordoquy
[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3

View File

@ -131,7 +131,7 @@ The `get_object` and `get_paginate_by` methods no longer take an optional querys
Using an optional queryset with these methods continues to be supported, but will raise a `PendingDeprecationWarning`.
The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropraite page size, or returns `None`, if pagination is not configured for the view.
The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropriate page size, or returns `None`, if pagination is not configured for the view.
Using the `page_size` argument is still supported and will trigger the old-style return type, but will raise a `PendingDeprecationWarning`.
@ -195,13 +195,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P
2.3 introduces a `DecimalField` serializer field, which returns `Decimal` instances.
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implmentation.
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation.
## ModelSerializers and reverse relationships
The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed.
In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For revese relationships `model_field` will be `None`.
In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`.
The old-style signature will continue to function but will raise a `PendingDeprecationWarning`.
@ -219,7 +219,7 @@ Note that the relevant methods have always been private APIs, and the docstrings
## More explicit style
The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explict `queryset` and `serializer_class` attributes.
The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explicit `queryset` and `serializer_class` attributes.
For example, the following is now the recommended style for using generic views:
@ -227,7 +227,7 @@ For example, the following is now the recommended style for using generic views:
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
Using an explict `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
Using an explicit `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
It also makes the usage of the `get_queryset()` or `get_serializer_class()` methods more obvious.
@ -246,7 +246,7 @@ It also makes the usage of the `get_queryset()` or `get_serializer_class()` meth
## Django 1.3 support
The 2.3.x release series will be the last series to provide compatiblity with Django 1.3.
The 2.3.x release series will be the last series to provide compatibility with Django 1.3.
## Version 2.2 API changes

View File

@ -23,7 +23,7 @@ To guard against these type of attacks, you need to do two things:
If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
The Django documentation describes how to [include CSRF tokens in AJAX requests][csrf-ajax].
In order to make AJAX requests, you need to include CSRF token in the HTTP header, as [described in the Django documentation][csrf-ajax].
## CORS

View File

@ -24,8 +24,8 @@ To customize the default style, create a template called `rest_framework/api.htm
**templates/rest_framework/api.html**
{% extends "rest_framework/base.html" %}
... # Override blocks with required customizations
... # Override blocks with required customizations
### Overriding the default theme
@ -75,6 +75,7 @@ All of the blocks available in the browsable API base template that can be used
* `branding` - Branding section of the navbar, see [Bootstrap components][bcomponentsnav].
* `breadcrumbs` - Links showing resource nesting, allowing the user to go back up the resources. It's recommended to preserve these, but they can be overridden using the breadcrumbs block.
* `footer` - Any copyright notices or similar footer materials can go here (by default right-aligned).
* `script` - JavaScript files for the page.
* `style` - CSS stylesheets for the page.
* `title` - Title of the page.
* `userlinks` - This is a list of links on the right of the header, by default containing login/logout links. To add links instead of replace, use `{{ block.super }}` to preserve the authentication links.
@ -89,14 +90,14 @@ The browsable API makes use of the Bootstrap tooltips component. Any element wi
### Login Template
To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/base_login.html`.
To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/login_base.html`.
You can add your site name or branding by including the branding block:
{% block branding %}
<h3 style="margin: 0 0 20px;">My Site Name</h3>
{% endblock %}
You can also customize the style by adding the `bootstrap_theme` or `style` block similar to `api.html`.
### Advanced Customization
@ -125,6 +126,37 @@ The context that's available to the template:
For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you.
#### Autocompletion
When a `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly. One solution is to replace the selector by an autocomplete widget, that only loads and renders a subset of the available options as needed.
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light]. To setup `django-autocomplete-light`, follow the [installation documentation][django-autocomplete-light-install], add the the following to the `api.html` template:
{% block script %}
{{ block.super }}
{% include 'autocomplete_light/static.html' %}
{% endblock %}
You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer field.
import autocomplete_light
class BookSerializer(serializers.ModelSerializer):
author = serializers.ChoiceField(
widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete')
)
class Meta:
model = Book
---
![Autocomplete][autocomplete-image]
*Screenshot of the autocomplete-light widget*
---
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead
[drfreverse]: ../api-guide/reverse.md
[ffjsonview]: https://addons.mozilla.org/en-US/firefox/addon/jsonview/
@ -136,4 +168,7 @@ For more advanced customization, such as not having a Bootstrap basis or tighter
[bswatch]: http://bootswatch.com/
[bcomponents]: http://twitter.github.com/bootstrap/components.html
[bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install
[autocomplete-image]: ../img/autocomplete.png

View File

@ -145,6 +145,19 @@ The following people have helped make REST framework great.
* Philip Douglas - [freakydug]
* Igor Kalat - [trwired]
* Rudolf Olah - [omouse]
* Gertjan Oude Lohuis - [gertjanol]
* Matthias Jacob - [cyroxx]
* Pavel Zinovkin - [pzinovkin]
* Will Kahn-Greene - [willkg]
* Kevin Brown - [kevin-brown]
* Rodrigo Martell - [coderigo]
* James Rutherford - [jimr]
* Ricky Rosario - [rlr]
* Veronica Lynn - [kolvia]
* Dan Stephenson - [etos]
* Martin Clement - [martync]
* Jeremy Satterfield - [jsatt]
* Christopher Paolini - [chrispaolini]
Many thanks to everyone who's contributed to the project.
@ -326,3 +339,16 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[freakydug]: https://github.com/freakydug
[trwired]: https://github.com/trwired
[omouse]: https://github.com/omouse
[gertjanol]: https://github.com/gertjanol
[cyroxx]: https://github.com/cyroxx
[pzinovkin]: https://github.com/pzinovkin
[coderigo]: https://github.com/coderigo
[willkg]: https://github.com/willkg
[kevin-brown]: https://github.com/kevin-brown
[jimr]: https://github.com/jimr
[rlr]: https://github.com/rlr
[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

View File

@ -16,7 +16,7 @@ The most common way to document Web APIs today is to produce documentation that
Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints.
The pacakge is fully documented, well supported, and comes highly recommended.
The package is fully documented, well supported, and comes highly recommended.
Django REST Swagger supports REST framework versions 2.3 and above.
@ -42,7 +42,7 @@ There are various other online tools and services for providing API documentatio
## Self describing APIs
The browsable API that REST framwork provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser.
The browsable API that REST framework provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser.
![Screenshot - Self describing API][image-self-describing-api]
@ -93,11 +93,11 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me
## The hypermedia approach
To be fully RESTful an API should present it's available actions as hypermedia controls in the responses that it sends.
To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends.
In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documention includes pointers to background reading, as well as links to various hypermedia formats.
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger

View File

@ -40,6 +40,23 @@ 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.
### 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

View File

@ -236,7 +236,7 @@ Edit the `snippet/views.py` file, and add the following.
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders it's content into JSON.
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)

View File

@ -81,7 +81,7 @@ Okay, we're done. If you run the development server everything should be workin
One of the big wins of using class based views is that it allows us to easily compose reusable bits of behaviour.
The create/retrieve/update/delete operations that we've been using so far are going to be pretty simliar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
Let's take a look at how we can compose our views by using the mixin classes.

View File

@ -80,7 +80,7 @@ We can easily re-write our existing serializers to use hyperlinking.
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = models.Snippet
model = Snippet
fields = ('url', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')

View File

@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
Let's take our current set of views, and refactor them into view sets.
First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace them with a single class:
from rest_framework import viewsets

View File

@ -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',

View File

@ -1,4 +1,4 @@
__version__ = '2.3.6'
__version__ = '2.3.7'
VERSION = __version__ # synonym

View File

@ -26,6 +26,12 @@ def get_authorization_header(request):
return auth
class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class BaseAuthentication(object):
"""
All authentication classes should extend BaseAuthentication.
@ -103,27 +109,27 @@ class SessionAuthentication(BaseAuthentication):
"""
# Get the underlying HttpRequest object
http_request = request._request
user = getattr(http_request, 'user', None)
request = request._request
user = getattr(request, 'user', None)
# Unauthenticated, CSRF validation not required
if not user or not user.is_active:
return None
# Enforce CSRF validation for session based authentication.
class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
reason = CSRFCheck().process_view(http_request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
self.enforce_csrf(request)
# CSRF passed with authenticated user
return (user, None)
def enforce_csrf(self, request):
"""
Enforce CSRF validation for session based authentication.
"""
reason = CSRFCheck().process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
"""

View File

@ -0,0 +1,11 @@
from django.contrib import admin
from rest_framework.authtoken.models import Token
class TokenAdmin(admin.ModelAdmin):
list_display = ('key', 'user', 'created')
fields = ('user',)
ordering = ('-created',)
admin.site.register(Token, TokenAdmin)

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
import django
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
# Try to import six from Django, fallback to included `six`.
try:
@ -83,7 +84,6 @@ def get_concrete_model(model_cls):
# Django 1.5 add support for custom auth user model
if django.VERSION >= (1, 5):
from django.conf import settings
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
else:
AUTH_USER_MODEL = 'auth.User'
@ -436,6 +436,42 @@ except ImportError:
return force_text(url)
# RequestFactory only provide `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
try:
# In 1.5 the test client uses force_bytes
from django.utils.encoding import force_bytes_or_smart_bytes
except ImportError:
# In 1.3 and 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
parsed = urlparse.urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': force_text(parsed[4]),
'REQUEST_METHOD': str(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
'CONTENT_TYPE': str(content_type),
'wsgi.input': FakePayload(data),
})
elif django.VERSION <= (1, 4):
# For 1.3 we need an empty WSGI payload
r.update({
'wsgi.input': FakePayload('')
})
r.update(extra)
return self.request(**r)
# Markdown is optional
try:
import markdown

View File

@ -100,6 +100,19 @@ def humanize_strptime(format_string):
return format_string
def strip_multiple_choice_msg(help_text):
"""
Remove the 'Hold down "control" ...' message that is Django enforces in
select multiple fields on ModelForms. (Required for 1.5 and earlier)
See https://code.djangoproject.com/ticket/9321
"""
multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
multiple_choice_msg = force_text(multiple_choice_msg)
return help_text.replace(multiple_choice_msg, '')
class Field(object):
read_only = True
creation_counter = 0
@ -122,7 +135,7 @@ class Field(object):
self.label = smart_text(label)
if help_text is not None:
self.help_text = smart_text(help_text)
self.help_text = strip_multiple_choice_msg(smart_text(help_text))
def initialize(self, parent, field_name):
"""
@ -492,7 +505,7 @@ class EmailField(CharField):
form_field_class = forms.EmailField
default_error_messages = {
'invalid': _('Enter a valid e-mail address.'),
'invalid': _('Enter a valid email address.'),
}
default_validators = [validators.validate_email]
@ -904,7 +917,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

View File

@ -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:

View File

@ -14,6 +14,7 @@ from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header
from django.template import RequestContext, loader, Template
from django.test.client import encode_multipart
from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework.compat import StringIO
from rest_framework.compat import six
@ -23,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
@ -497,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)
@ -571,3 +571,13 @@ class BrowsableAPIRenderer(BaseRenderer):
response.status_code = status.HTTP_200_OK
return ret
class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
charset = 'utf-8'
BOUNDARY = 'BoUnDaRyStRiNg'
def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data)

View File

@ -64,6 +64,20 @@ def clone_request(request, method):
return ret
class ForcedAuthentication(object):
"""
This authentication class is used if the test client or request factory
forcibly authenticated the request.
"""
def __init__(self, force_user, force_token):
self.force_user = force_user
self.force_token = force_token
def authenticate(self, request):
return (self.force_user, self.force_token)
class Request(object):
"""
Wrapper allowing to enhance a standard `HttpRequest` instance.
@ -98,6 +112,12 @@ class Request(object):
self.parser_context['request'] = self
self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET
force_user = getattr(request, '_force_auth_user', None)
force_token = getattr(request, '_force_auth_token', None)
if (force_user is not None or force_token is not None):
forced_auth = ForcedAuthentication(force_user, force_token)
self.authenticators = (forced_auth,)
def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()

View File

@ -659,14 +659,14 @@ class ModelSerializer(Serializer):
# in the `read_only_fields` option
for field_name in self.opts.read_only_fields:
assert field_name not in self.base_fields.keys(), \
"field '%s' on serializer '%s' specfied in " \
"field '%s' on serializer '%s' specified in " \
"`read_only_fields`, but also added " \
"as an explict field. Remove it from `read_only_fields`." % \
"as an explicit field. Remove it from `read_only_fields`." % \
(field_name, self.__class__.__name__)
assert field_name in ret, \
"Noexistant field '%s' specified in `read_only_fields` " \
"Non-existant field '%s' specified in `read_only_fields` " \
"on serializer '%s'." % \
(self.__class__.__name__, field_name)
(field_name, self.__class__.__name__)
ret[field_name].read_only = True
return ret

View File

@ -69,10 +69,21 @@ 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,
# Testing
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer'
),
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
# Browser enhancements
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
@ -115,8 +126,11 @@ IMPORT_STRINGS = (
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS',
'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
'VIEW_NAME_FUNCTION',
'VIEW_DESCRIPTION_FUNCTION'
)

View File

@ -196,7 +196,7 @@
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
{% endif %}
{% if raw_data_patch_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PUT request on the {{ name }} resource">PATCH</button>
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
{% endif %}
</div>
</fieldset>

157
rest_framework/test.py Normal file
View File

@ -0,0 +1,157 @@
# -- coding: utf-8 --
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
# to make it harder for the user to import the wrong thing without realizing.
from __future__ import unicode_literals
import django
from django.conf import settings
from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler
from django.test import testcases
from rest_framework.settings import api_settings
from rest_framework.compat import RequestFactory as DjangoRequestFactory
from rest_framework.compat import force_bytes_or_smart_bytes, six
def force_authenticate(request, user=None, token=None):
request._force_auth_user = user
request._force_auth_token = token
class APIRequestFactory(DjangoRequestFactory):
renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT
def __init__(self, enforce_csrf_checks=False, **defaults):
self.enforce_csrf_checks = enforce_csrf_checks
self.renderer_classes = {}
for cls in self.renderer_classes_list:
self.renderer_classes[cls.format] = cls
super(APIRequestFactory, self).__init__(**defaults)
def _encode_data(self, data, format=None, content_type=None):
"""
Encode the data returning a two tuple of (bytes, content_type)
"""
if not data:
return ('', None)
assert format is None or content_type is None, (
'You may not set both `format` and `content_type`.'
)
if content_type:
# Content type specified explicitly, treat data as a raw bytestring
ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
else:
format = format or self.default_format
assert format in self.renderer_classes, ("Invalid format '{0}'. "
"Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
"to enable extra request formats.".format(
format,
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
)
)
# Use format and render the data into a bytestring
renderer = self.renderer_classes[format]()
ret = renderer.render(data)
# Determine the content-type header from the renderer
content_type = "{0}; charset={1}".format(
renderer.media_type, renderer.charset
)
# Coerce text to bytes if required.
if isinstance(ret, six.text_type):
ret = bytes(ret.encode(renderer.charset))
return ret, content_type
def post(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('POST', path, data, content_type, **extra)
def put(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('PUT', path, data, content_type, **extra)
def patch(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('PATCH', path, data, content_type, **extra)
def delete(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('DELETE', path, data, content_type, **extra)
def options(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('OPTIONS', path, data, content_type, **extra)
def request(self, **kwargs):
request = super(APIRequestFactory, self).request(**kwargs)
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
return request
class ForceAuthClientHandler(ClientHandler):
"""
A patched version of ClientHandler that can enforce authentication
on the outgoing requests.
"""
def __init__(self, *args, **kwargs):
self._force_user = None
self._force_token = None
super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
def get_response(self, request):
# This is the simplest place we can hook into to patch the
# request object.
force_authenticate(request, self._force_user, self._force_token)
return super(ForceAuthClientHandler, self).get_response(request)
class APIClient(APIRequestFactory, DjangoClient):
def __init__(self, enforce_csrf_checks=False, **defaults):
super(APIClient, self).__init__(**defaults)
self.handler = ForceAuthClientHandler(enforce_csrf_checks)
self._credentials = {}
def credentials(self, **kwargs):
"""
Sets headers that will be used on every outgoing request.
"""
self._credentials = kwargs
def force_authenticate(self, user=None, token=None):
"""
Forcibly authenticates outgoing requests with the given
user and/or token.
"""
self.handler._force_user = user
self.handler._force_token = token
def request(self, **kwargs):
# Ensure that any credentials set get added to every request.
kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs)
class APITransactionTestCase(testcases.TransactionTestCase):
client_class = APIClient
class APITestCase(testcases.TestCase):
client_class = APIClient
if django.VERSION >= (1, 4):
class APISimpleTestCase(testcases.SimpleTestCase):
client_class = APIClient
class APILiveServerTestCase(testcases.LiveServerTestCase):
client_class = APIClient

View File

@ -52,7 +52,7 @@ class CallableDefaultValueModel(RESTFrameworkModel):
class ManyToManyModel(RESTFrameworkModel):
rel = models.ManyToManyField(Anchor)
rel = models.ManyToManyField(Anchor, help_text='Some help text.')
class ReadOnlyManyToManyModel(RESTFrameworkModel):

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.test import Client, TestCase
from django.test import TestCase
from django.utils import unittest
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
@ -21,14 +21,13 @@ from rest_framework.authtoken.models import Token
from rest_framework.compat import patterns, url, include
from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
from rest_framework.compat import oauth, oauth_provider
from rest_framework.tests.utils import RequestFactory
from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
import json
import base64
import time
import datetime
factory = RequestFactory()
factory = APIRequestFactory()
class MockView(APIView):
@ -68,7 +67,7 @@ class BasicAuthTests(TestCase):
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
@ -87,7 +86,7 @@ class BasicAuthTests(TestCase):
credentials = ('%s:%s' % (self.username, self.password))
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
auth = 'Basic %s' % base64_credentials
response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post_form_failing_basic_auth(self):
@ -97,7 +96,7 @@ class BasicAuthTests(TestCase):
def test_post_json_failing_basic_auth(self):
"""Ensure POSTing json over basic auth without correct credentials fails"""
response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json')
response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
@ -107,8 +106,8 @@ class SessionAuthTests(TestCase):
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.non_csrf_client = Client(enforce_csrf_checks=False)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.non_csrf_client = APIClient(enforce_csrf_checks=False)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
@ -154,7 +153,7 @@ class TokenAuthTests(TestCase):
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
@ -172,7 +171,7 @@ class TokenAuthTests(TestCase):
def test_post_json_passing_token_auth(self):
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
auth = "Token " + self.key
response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post_form_failing_token_auth(self):
@ -182,7 +181,7 @@ class TokenAuthTests(TestCase):
def test_post_json_failing_token_auth(self):
"""Ensure POSTing json over token auth without correct credentials fails"""
response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json')
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_token_has_auto_assigned_key_if_none_provided(self):
@ -193,33 +192,33 @@ class TokenAuthTests(TestCase):
def test_token_login_json(self):
"""Ensure token login view using JSON POST works."""
client = Client(enforce_csrf_checks=True)
client = APIClient(enforce_csrf_checks=True)
response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
{'username': self.username, 'password': self.password}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
self.assertEqual(response.data['token'], self.key)
def test_token_login_json_bad_creds(self):
"""Ensure token login view using JSON POST fails if bad credentials are used."""
client = Client(enforce_csrf_checks=True)
client = APIClient(enforce_csrf_checks=True)
response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
{'username': self.username, 'password': "badpass"}, format='json')
self.assertEqual(response.status_code, 400)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
client = APIClient(enforce_csrf_checks=True)
response = client.post('/auth-token/',
json.dumps({'username': self.username}), 'application/json')
{'username': self.username}, format='json')
self.assertEqual(response.status_code, 400)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""
client = Client(enforce_csrf_checks=True)
client = APIClient(enforce_csrf_checks=True)
response = client.post('/auth-token/',
{'username': self.username, 'password': self.password})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
self.assertEqual(response.data['token'], self.key)
class IncorrectCredentialsTests(TestCase):
@ -256,7 +255,7 @@ class OAuthTests(TestCase):
self.consts = consts
self.csrf_client = Client(enforce_csrf_checks=True)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
@ -470,12 +469,13 @@ class OAuthTests(TestCase):
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 401)
class OAuth2Tests(TestCase):
"""OAuth 2.0 authentication"""
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'

View File

@ -1,12 +1,13 @@
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import status
from rest_framework.authentication import BasicAuthentication
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rest_framework.authentication import BasicAuthentication
from rest_framework.test import APIRequestFactory
from rest_framework.throttling import UserRateThrottle
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework.decorators import (
api_view,
@ -17,13 +18,11 @@ from rest_framework.decorators import (
permission_classes,
)
from rest_framework.tests.utils import RequestFactory
class DecoratorTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.factory = APIRequestFactory()
def _finalize_response(self, request, response, *args, **kwargs):
response.request = request

View File

@ -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):
"""

View File

@ -4,13 +4,13 @@ from decimal import Decimal
from django.db import models
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters, patterns, url
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel
factory = RequestFactory()
factory = APIRequestFactory()
class FilterableItem(models.Model):

View File

@ -3,12 +3,11 @@ from django.db import models
from django.shortcuts import get_object_or_404
from django.test import TestCase
from rest_framework import generics, renderers, serializers, status
from rest_framework.tests.utils import RequestFactory
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
from rest_framework.compat import six
import json
factory = RequestFactory()
factory = APIRequestFactory()
class RootView(generics.ListCreateAPIView):
@ -71,9 +70,8 @@ class TestRootView(TestCase):
"""
POST requests to ListCreateAPIView should create a new object.
"""
content = {'text': 'foobar'}
request = factory.post('/', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.post('/', data, format='json')
with self.assertNumQueries(1):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -85,9 +83,8 @@ class TestRootView(TestCase):
"""
PUT requests to ListCreateAPIView should not be allowed
"""
content = {'text': 'foobar'}
request = factory.put('/', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.put('/', data, format='json')
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
@ -148,9 +145,8 @@ class TestRootView(TestCase):
"""
POST requests to create a new object should not be able to set the id.
"""
content = {'id': 999, 'text': 'foobar'}
request = factory.post('/', json.dumps(content),
content_type='application/json')
data = {'id': 999, 'text': 'foobar'}
request = factory.post('/', data, format='json')
with self.assertNumQueries(1):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -189,9 +185,8 @@ class TestInstanceView(TestCase):
"""
POST requests to RetrieveUpdateDestroyAPIView should not be allowed
"""
content = {'text': 'foobar'}
request = factory.post('/', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.post('/', data, format='json')
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
@ -201,9 +196,8 @@ class TestInstanceView(TestCase):
"""
PUT requests to RetrieveUpdateDestroyAPIView should update an object.
"""
content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
with self.assertNumQueries(2):
response = self.view(request, pk='1').render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -215,9 +209,8 @@ class TestInstanceView(TestCase):
"""
PATCH requests to RetrieveUpdateDestroyAPIView should update an object.
"""
content = {'text': 'foobar'}
request = factory.patch('/1', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.patch('/1', data, format='json')
with self.assertNumQueries(2):
response = self.view(request, pk=1).render()
@ -293,9 +286,8 @@ class TestInstanceView(TestCase):
"""
PUT requests to create a new object should not be able to set the id.
"""
content = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
content_type='application/json')
data = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', data, format='json')
with self.assertNumQueries(2):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -309,9 +301,8 @@ class TestInstanceView(TestCase):
if it does not currently exist.
"""
self.objects.get(id=1).delete()
content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
with self.assertNumQueries(3):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -324,10 +315,9 @@ class TestInstanceView(TestCase):
PUT requests to RetrieveUpdateDestroyAPIView should create an object
at the requested url if it doesn't exist.
"""
content = {'text': 'foobar'}
data = {'text': 'foobar'}
# pk fields can not be created on demand, only the database can set the pk for a new object
request = factory.put('/5', json.dumps(content),
content_type='application/json')
request = factory.put('/5', data, format='json')
with self.assertNumQueries(3):
response = self.view(request, pk=5).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -339,9 +329,8 @@ class TestInstanceView(TestCase):
PUT requests to RetrieveUpdateDestroyAPIView should create an object
at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
"""
content = {'text': 'foobar'}
request = factory.put('/test_slug', json.dumps(content),
content_type='application/json')
data = {'text': 'foobar'}
request = factory.put('/test_slug', data, format='json')
with self.assertNumQueries(2):
response = self.slug_based_view(request, slug='test_slug').render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -415,9 +404,8 @@ class TestCreateModelWithAutoNowAddField(TestCase):
https://github.com/tomchristie/django-rest-framework/issues/285
"""
content = {'email': 'foobar@example.com', 'content': 'foobar'}
request = factory.post('/', json.dumps(content),
content_type='application/json')
data = {'email': 'foobar@example.com', 'content': 'foobar'}
request = factory.post('/', data, format='json')
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
created = self.objects.get(id=1)

View File

@ -1,12 +1,15 @@
from __future__ import unicode_literals
import json
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import (
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
Album, Photo, OptionalRelationModel
)
factory = RequestFactory()
factory = APIRequestFactory()
class BlogPostCommentSerializer(serializers.ModelSerializer):
@ -21,7 +24,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer):
class PhotoSerializer(serializers.Serializer):
description = serializers.CharField()
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title')
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title')
def restore_object(self, attrs, instance=None):
return Photo(**attrs)

View File

@ -1,12 +1,12 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework.negotiation import DefaultContentNegotiation
from rest_framework.request import Request
from rest_framework.renderers import BaseRenderer
from rest_framework.test import APIRequestFactory
factory = RequestFactory()
factory = APIRequestFactory()
class MockJSONRenderer(BaseRenderer):

View File

@ -4,13 +4,13 @@ from decimal import Decimal
from django.db import models
from django.core.paginator import Paginator
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel
factory = RequestFactory()
factory = APIRequestFactory()
class FilterableItem(models.Model):
@ -369,7 +369,7 @@ class TestCustomPaginationSerializer(TestCase):
self.page = paginator.page(1)
def test_custom_pagination_serializer(self):
request = RequestFactory().get('/foobar')
request = APIRequestFactory().get('/foobar')
serializer = CustomPaginationSerializer(
instance=self.page,
context={'request': request}

View File

@ -3,11 +3,10 @@ from django.contrib.auth.models import User, Permission
from django.db import models
from django.test import TestCase
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
from rest_framework.tests.utils import RequestFactory
from rest_framework.test import APIRequestFactory
import base64
import json
factory = RequestFactory()
factory = APIRequestFactory()
class BasicModel(models.Model):
@ -56,15 +55,13 @@ class ModelPermissionsIntegrationTests(TestCase):
BasicModel(text='foo').save()
def test_has_create_permissions(self):
request = factory.post('/', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.post('/', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = root_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_has_put_permissions(self):
request = factory.put('/1', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.put('/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -75,15 +72,13 @@ class ModelPermissionsIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_does_not_have_create_permissions(self):
request = factory.post('/', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.post('/', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = root_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_does_not_have_put_permissions(self):
request = factory.put('/1', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.put('/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@ -95,28 +90,26 @@ class ModelPermissionsIntegrationTests(TestCase):
def test_has_put_as_create_permissions(self):
# User only has update permissions - should be able to update an entity.
request = factory.put('/1', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.put('/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# But if PUTing to a new entity, permission should be denied.
request = factory.put('/2', json.dumps({'text': 'foobar'}),
content_type='application/json',
request = factory.put('/2', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_options_permitted(self):
request = factory.options('/', content_type='application/json',
request = factory.options('/',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
request = factory.options('/1', content_type='application/json',
request = factory.options('/1',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -124,26 +117,26 @@ class ModelPermissionsIntegrationTests(TestCase):
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
def test_options_disallowed(self):
request = factory.options('/', content_type='application/json',
request = factory.options('/',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
request = factory.options('/1', content_type='application/json',
request = factory.options('/1',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
def test_options_updateonly(self):
request = factory.options('/', content_type='application/json',
request = factory.options('/',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
request = factory.options('/1', content_type='application/json',
request = factory.options('/1',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,15 +1,15 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import serializers
from rest_framework.compat import patterns, url
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import (
BlogPost,
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
)
factory = RequestFactory()
factory = APIRequestFactory()
request = factory.get('/') # Just to ensure we have a request in the serializer context

View File

@ -4,19 +4,17 @@ from __future__ import unicode_literals
from decimal import Decimal
from django.core.cache import cache
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
from rest_framework.compat import yaml, etree, patterns, url, include
from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.compat import StringIO
from rest_framework.compat import six
from rest_framework.test import APIRequestFactory
import datetime
import pickle
import re
@ -121,7 +119,7 @@ class POSTDeniedView(APIView):
class DocumentingRendererTests(TestCase):
def test_only_permitted_forms_are_displayed(self):
view = POSTDeniedView.as_view()
request = RequestFactory().get('/')
request = APIRequestFactory().get('/')
response = view(request).render()
self.assertNotContains(response, '>POST<')
self.assertContains(response, '>PUT<')

View File

@ -5,8 +5,7 @@ from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.test import TestCase
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.compat import patterns
@ -19,12 +18,13 @@ from rest_framework.parsers import (
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
from rest_framework.compat import six
import json
factory = RequestFactory()
factory = APIRequestFactory()
class PlainTextParser(BaseParser):
@ -116,16 +116,7 @@ class TestContentParsing(TestCase):
Ensure request.DATA returns content for PUT request with form content.
"""
data = {'qwerty': 'uiop'}
from django import VERSION
if VERSION >= (1, 5):
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
request = Request(factory.put('/', encode_multipart(BOUNDARY, data),
content_type=MULTIPART_CONTENT))
else:
request = Request(factory.put('/', data))
request = Request(factory.put('/', data))
request.parsers = (FormParser(), MultiPartParser())
self.assertEqual(list(request.DATA.items()), list(data.items()))
@ -257,7 +248,7 @@ class TestContentParsingWithAuthentication(TestCase):
urls = 'rest_framework.tests.test_request'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'

View File

@ -1,10 +1,10 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory
factory = RequestFactory()
factory = APIRequestFactory()
def null_view(request):

View File

@ -1,15 +1,15 @@
from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, viewsets, permissions
from rest_framework.compat import include, patterns, url
from rest_framework.decorators import link, action
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory
factory = RequestFactory()
factory = APIRequestFactory()
urlpatterns = patterns('',)
@ -193,6 +193,7 @@ class TestActionKeywordArgs(TestCase):
{'permission_classes': [permissions.AllowAny]}
)
class TestActionAppliedToExistingRoute(TestCase):
"""
Ensure `@action` decorator raises an except when applied

View File

@ -494,7 +494,7 @@ class CustomValidationTests(TestCase):
}
serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'email': ['Enter a valid e-mail address.']})
self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
class PositiveIntegerAsChoiceTests(TestCase):
@ -1376,6 +1376,18 @@ class FieldLabelTest(TestCase):
self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
# Test for issue #961
class ManyFieldHelpTextTest(TestCase):
def test_help_text_no_hold_down_control_msg(self):
"""
Validate that help_text doesn't contain the 'Hold down "Control" ...'
message that Django appends to choice fields.
"""
rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
self.assertEqual('Some help text.', rel_field.help_text)
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def setUp(self):

View File

@ -0,0 +1,115 @@
# -- coding: utf-8 --
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
@api_view(['GET', 'POST'])
def view(request):
return Response({
'auth': request.META.get('HTTP_AUTHORIZATION', b''),
'user': request.user.username
})
urlpatterns = patterns('',
url(r'^view/$', view),
)
class TestAPITestClient(TestCase):
urls = 'rest_framework.tests.test_testing'
def setUp(self):
self.client = APIClient()
def test_credentials(self):
"""
Setting `.credentials()` adds the required headers to each request.
"""
self.client.credentials(HTTP_AUTHORIZATION='example')
for _ in range(0, 3):
response = self.client.get('/view/')
self.assertEqual(response.data['auth'], 'example')
def test_force_authenticate(self):
"""
Setting `.force_authenticate()` forcibly authenticates each request.
"""
user = User.objects.create_user('example', 'example@example.com')
self.client.force_authenticate(user)
response = self.client.get('/view/')
self.assertEqual(response.data['user'], 'example')
def test_csrf_exempt_by_default(self):
"""
By default, the test client is CSRF exempt.
"""
User.objects.create_user('example', 'example@example.com', 'password')
self.client.login(username='example', password='password')
response = self.client.post('/view/')
self.assertEqual(response.status_code, 200)
def test_explicitly_enforce_csrf_checks(self):
"""
The test client can enforce CSRF checks.
"""
client = APIClient(enforce_csrf_checks=True)
User.objects.create_user('example', 'example@example.com', 'password')
client.login(username='example', password='password')
response = client.post('/view/')
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data, expected)
class TestAPIRequestFactory(TestCase):
def test_csrf_exempt_by_default(self):
"""
By default, the test client is CSRF exempt.
"""
user = User.objects.create_user('example', 'example@example.com', 'password')
factory = APIRequestFactory()
request = factory.post('/view/')
request.user = user
response = view(request)
self.assertEqual(response.status_code, 200)
def test_explicitly_enforce_csrf_checks(self):
"""
The test client can enforce CSRF checks.
"""
user = User.objects.create_user('example', 'example@example.com', 'password')
factory = APIRequestFactory(enforce_csrf_checks=True)
request = factory.post('/view/')
request.user = user
response = view(request)
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data, expected)
def test_invalid_format(self):
"""
Attempting to use a format that is not configured will raise an
assertion error.
"""
factory = APIRequestFactory()
self.assertRaises(AssertionError, factory.post,
path='/view/', data={'example': 1}, format='xml'
)
def test_force_authenticate(self):
"""
Setting `force_authenticate()` forcibly authenticates the request.
"""
user = User.objects.create_user('example', 'example@example.com')
factory = APIRequestFactory()
request = factory.get('/view')
force_authenticate(request, user=user)
response = view(request)
self.assertEqual(response.data['user'], 'example')

View File

@ -5,9 +5,9 @@ from __future__ import unicode_literals
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.cache import cache
from django.test.client import RequestFactory
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,13 +43,20 @@ 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):
"""
Reset the cache so that no throttles will be active
"""
cache.clear()
self.factory = RequestFactory()
self.factory = APIRequestFactory()
def test_requests_are_throttled(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):
"""
@ -173,7 +204,7 @@ class ScopedRateThrottleTests(TestCase):
return Response('y')
self.throttle_class = XYScopedRateThrottle
self.factory = RequestFactory()
self.factory = APIRequestFactory()
self.x_view = XView.as_view()
self.y_view = YView.as_view()
self.unscoped_view = UnscopedView.as_view()

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from collections import namedtuple
from django.core import urlresolvers
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework.test import APIRequestFactory
from rest_framework.compat import patterns, url, include
from rest_framework.urlpatterns import format_suffix_patterns
@ -20,7 +20,7 @@ class FormatSuffixTests(TestCase):
Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters.
"""
def _resolve_urlpatterns(self, urlpatterns, test_paths):
factory = RequestFactory()
factory = APIRequestFactory()
try:
urlpatterns = format_suffix_patterns(urlpatterns)
except Exception:

View File

@ -2,10 +2,9 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import generics, serializers, status
from rest_framework.tests.utils import RequestFactory
import json
from rest_framework.test import APIRequestFactory
factory = RequestFactory()
factory = APIRequestFactory()
# Regression for #666
@ -33,8 +32,7 @@ class TestPreSaveValidationExclusions(TestCase):
validation on read only fields.
"""
obj = ValidationModel.objects.create(blank_validated_field='')
request = factory.put('/', json.dumps({}),
content_type='application/json')
request = factory.put('/', {}, format='json')
view = UpdateValidationModel().as_view()
response = view(request, pk=obj.pk).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,17 +1,15 @@
from __future__ import unicode_literals
import copy
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
factory = RequestFactory()
factory = APIRequestFactory()
class BasicView(APIView):

View File

@ -1,40 +0,0 @@
from __future__ import unicode_literals
from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory
from django.test.client import MULTIPART_CONTENT
from rest_framework.compat import urlparse
class RequestFactory(_RequestFactory):
def __init__(self, **defaults):
super(RequestFactory, self).__init__(**defaults)
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
**extra):
"Construct a PATCH request."
patch_data = self._encode_data(data, content_type)
parsed = urlparse.urlparse(path)
r = {
'CONTENT_LENGTH': len(patch_data),
'CONTENT_TYPE': content_type,
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': parsed[4],
'REQUEST_METHOD': 'PATCH',
'wsgi.input': FakePayload(patch_data),
}
r.update(extra)
return self.request(**r)
class Client(_Client, RequestFactory):
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
follow=False, **extra):
"""
Send a resource to the server using PATCH.
"""
response = super(Client, self).patch(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response

View File

@ -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()

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -55,7 +55,6 @@ setup(
name='djangorestframework',
version=version,
url='http://django-rest-framework.org',
download_url='http://pypi.python.org/pypi/rest_framework/',
license='BSD',
description='Web APIs for Django, made easy.',
author='Tom Christie',