mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Merge master
This commit is contained in:
commit
de00ec95c3
20
.travis.yml
20
.travis.yml
|
@ -7,18 +7,19 @@ python:
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="django==1.5 --use-mirrors"
|
- DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
|
||||||
- DJANGO="django==1.4.3 --use-mirrors"
|
- DJANGO="django==1.5.1 --use-mirrors"
|
||||||
- DJANGO="django==1.3.5 --use-mirrors"
|
- DJANGO="django==1.4.5 --use-mirrors"
|
||||||
|
- DJANGO="django==1.3.7 --use-mirrors"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
- pip install defusedxml==0.3
|
- pip install defusedxml==0.3
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
|
||||||
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
|
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
|
||||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi"
|
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
@ -27,10 +28,11 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.3 --use-mirrors"
|
env: DJANGO="django==1.4.5 --use-mirrors"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.3.5 --use-mirrors"
|
env: DJANGO="django==1.3.7 --use-mirrors"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.3 --use-mirrors"
|
env: DJANGO="django==1.4.5 --use-mirrors"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.3.5 --use-mirrors"
|
env: DJANGO="django==1.3.7 --use-mirrors"
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python (2.6.5+, 2.7, 3.2, 3.3)
|
* Python (2.6.5+, 2.7, 3.2, 3.3)
|
||||||
* Django (1.3, 1.4, 1.5)
|
* Django (1.3, 1.4, 1.5, 1.6)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
@ -102,6 +102,12 @@ For questions and support, use the [REST framework discussion group][group], or
|
||||||
|
|
||||||
You may also want to [follow the author on Twitter][twitter].
|
You may also want to [follow the author on Twitter][twitter].
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||||
|
|
||||||
|
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Copyright (c) 2011-2013, Tom Christie
|
Copyright (c) 2011-2013, Tom Christie
|
||||||
|
@ -149,3 +155,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
||||||
[defusedxml]: https://pypi.python.org/pypi/defusedxml
|
[defusedxml]: https://pypi.python.org/pypi/defusedxml
|
||||||
[django-filter]: http://pypi.python.org/pypi/django-filter
|
[django-filter]: http://pypi.python.org/pypi/django-filter
|
||||||
|
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
||||||
|
|
|
@ -86,7 +86,7 @@ Note that when a request may successfully authenticate, but still be denied perm
|
||||||
|
|
||||||
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
||||||
|
|
||||||
If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`.
|
If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`.
|
||||||
|
|
||||||
# this can go in either server config, virtual host, directory or .htaccess
|
# this can go in either server config, virtual host, directory or .htaccess
|
||||||
WSGIPassAuthorization On
|
WSGIPassAuthorization On
|
||||||
|
@ -216,9 +216,9 @@ If you're using an AJAX style API with SessionAuthentication, you'll need to mak
|
||||||
|
|
||||||
## OAuthAuthentication
|
## OAuthAuthentication
|
||||||
|
|
||||||
This authentication uses [OAuth 1.0a][oauth-1.0a] authentication scheme. OAuth 1.0a provides signature validation which provides a reasonable level of security over plain non-HTTPS connections. However, it may also be considered more complicated than OAuth2, as it requires clients to sign their requests.
|
This authentication uses [OAuth 1.0a][oauth-1.0a] authentication scheme. OAuth 1.0a provides signature validation which provides a reasonable level of security over plain non-HTTPS connections. However, it may also be considered more complicated than OAuth2, as it requires clients to sign their requests.
|
||||||
|
|
||||||
This authentication class depends on the optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must install these packages and add `oauth_provider` to your `INSTALLED_APPS`:
|
This authentication class depends on the optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must install these packages and add `oauth_provider` to your `INSTALLED_APPS`:
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
@ -231,7 +231,7 @@ Don't forget to run `syncdb` once you've added the package.
|
||||||
|
|
||||||
#### Getting started with django-oauth-plus
|
#### Getting started with django-oauth-plus
|
||||||
|
|
||||||
The OAuthAuthentication class only provides token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing tokens.
|
The OAuthAuthentication class only provides token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing tokens.
|
||||||
|
|
||||||
The `django-oauth-plus` package provides simple foundation for classic 'three-legged' oauth flow. Please refer to [the documentation][django-oauth-plus] for more details.
|
The `django-oauth-plus` package provides simple foundation for classic 'three-legged' oauth flow. Please refer to [the documentation][django-oauth-plus] for more details.
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ The `django-oauth-plus` package provides simple foundation for classic 'three-le
|
||||||
|
|
||||||
This authentication uses [OAuth 2.0][rfc6749] authentication scheme. OAuth2 is more simple to work with than OAuth1, and provides much better security than simple token authentication. It is an unauthenticated scheme, and requires you to use an HTTPS connection.
|
This authentication uses [OAuth 2.0][rfc6749] authentication scheme. OAuth2 is more simple to work with than OAuth1, and provides much better security than simple token authentication. It is an unauthenticated scheme, and requires you to use an HTTPS connection.
|
||||||
|
|
||||||
This authentication class depends on the optional [django-oauth2-provider][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS`:
|
This authentication class depends on the optional [django-oauth2-provider][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS`:
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
@ -266,9 +266,9 @@ Finally, sync your database.
|
||||||
|
|
||||||
#### Getting started with django-oauth2-provider
|
#### Getting started with django-oauth2-provider
|
||||||
|
|
||||||
The `OAuth2Authentication` class only provides token verification for requests. It doesn't provide authorization flow for your clients.
|
The `OAuth2Authentication` class only provides token verification for requests. It doesn't provide authorization flow for your clients.
|
||||||
|
|
||||||
The OAuth 2 authorization flow is taken care by the [django-oauth2-provider][django-oauth2-provider] dependency. A walkthrough is given here, but for more details you should refer to [the documentation][django-oauth2-provider-docs].
|
The OAuth 2 authorization flow is taken care by the [django-oauth2-provider][django-oauth2-provider] dependency. A walkthrough is given here, but for more details you should refer to [the documentation][django-oauth2-provider-docs].
|
||||||
|
|
||||||
To get started:
|
To get started:
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ To get started:
|
||||||
|
|
||||||
You can create a client, either through the shell, or by using the Django admin.
|
You can create a client, either through the shell, or by using the Django admin.
|
||||||
|
|
||||||
Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
|
Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
|
||||||
|
|
||||||
##### 2. Request an access token
|
##### 2. Request an access token
|
||||||
|
|
||||||
|
@ -333,7 +333,7 @@ The following example will authenticate any incoming request as the user given b
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(username=username)
|
user = User.objects.get(username=username)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
raise authenticate.AuthenticationFailed('No such user')
|
raise exceptions.AuthenticationFailed('No such user')
|
||||||
|
|
||||||
return (user, None)
|
return (user, None)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ Serializer fields handle converting between primitive values and internal dataty
|
||||||
|
|
||||||
## Core arguments
|
## Core arguments
|
||||||
|
|
||||||
Each serializer field class constructor takes at least these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted:
|
Each serializer field class constructor takes at least these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted:
|
||||||
|
|
||||||
### `source`
|
### `source`
|
||||||
|
|
||||||
|
@ -41,7 +41,9 @@ Defaults to `True`.
|
||||||
|
|
||||||
### `default`
|
### `default`
|
||||||
|
|
||||||
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
|
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||||
|
|
||||||
|
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
|
||||||
|
|
||||||
### `validators`
|
### `validators`
|
||||||
|
|
||||||
|
@ -56,6 +58,13 @@ A dictionary of error codes to error messages.
|
||||||
Used only if rendering the field to HTML.
|
Used only if rendering the field to HTML.
|
||||||
This argument sets the widget that should be used to render the field.
|
This argument sets the widget that should be used to render the field.
|
||||||
|
|
||||||
|
### `label`
|
||||||
|
|
||||||
|
A short text string that may be used as the name of the field in HTML form fields or other descriptive elements.
|
||||||
|
|
||||||
|
### `help_text`
|
||||||
|
|
||||||
|
A text string that may be used as a description of the field in HTML form fields or other descriptive elements.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -108,11 +117,13 @@ A field that supports both read and write operations. By itself `WritableField`
|
||||||
|
|
||||||
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
||||||
|
|
||||||
**Signature:** `ModelField(model_field=<Django ModelField class>)`
|
The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
|
||||||
|
|
||||||
|
**Signature:** `ModelField(model_field=<Django ModelField instance>)`
|
||||||
|
|
||||||
## SerializerMethodField
|
## SerializerMethodField
|
||||||
|
|
||||||
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
|
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -151,7 +162,7 @@ or `django.db.models.fields.TextField`.
|
||||||
|
|
||||||
## URLField
|
## URLField
|
||||||
|
|
||||||
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
|
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
|
||||||
|
|
||||||
**Signature:** `CharField(max_length=200, min_length=None)`
|
**Signature:** `CharField(max_length=200, min_length=None)`
|
||||||
|
|
||||||
|
@ -197,16 +208,16 @@ If you want to override this behavior, you'll need to declare the `DateTimeField
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Comment
|
model = Comment
|
||||||
|
|
||||||
Note that by default, datetime representations are deteremined by the renderer in use, although this can be explicitly overridden as detailed below.
|
Note that by default, datetime representations are determined by the renderer in use, although this can be explicitly overridden as detailed below.
|
||||||
|
|
||||||
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
|
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
|
||||||
|
|
||||||
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
||||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
DateTime format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
||||||
|
|
||||||
## DateField
|
## DateField
|
||||||
|
|
||||||
|
@ -217,9 +228,9 @@ Corresponds to `django.db.models.fields.DateField`
|
||||||
**Signature:** `DateField(format=None, input_formats=None)`
|
**Signature:** `DateField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer.
|
||||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
Date format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
|
Date format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
|
||||||
|
|
||||||
## TimeField
|
## TimeField
|
||||||
|
|
||||||
|
@ -232,9 +243,9 @@ Corresponds to `django.db.models.fields.TimeField`
|
||||||
**Signature:** `TimeField(format=None, input_formats=None)`
|
**Signature:** `TimeField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer.
|
||||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
Time format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
|
Time format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
|
||||||
|
|
||||||
## IntegerField
|
## IntegerField
|
||||||
|
|
||||||
|
@ -256,7 +267,7 @@ Corresponds to `django.db.models.fields.DecimalField`.
|
||||||
|
|
||||||
## FileField
|
## FileField
|
||||||
|
|
||||||
A file representation. Performs Django's standard FileField validation.
|
A file representation. Performs Django's standard FileField validation.
|
||||||
|
|
||||||
Corresponds to `django.forms.fields.FileField`.
|
Corresponds to `django.forms.fields.FileField`.
|
||||||
|
|
||||||
|
@ -285,7 +296,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||||
|
|
||||||
# Custom fields
|
# Custom fields
|
||||||
|
|
||||||
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the intial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
|
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
|
||||||
|
|
||||||
The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
|
The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Filtering
|
# Filtering
|
||||||
|
|
||||||
> The root QuerySet provided by the Manager describes all objects in the database table. Usually, though, you'll need to select only a subset of the complete set of objects.
|
> The root QuerySet provided by the Manager describes all objects in the database table. Usually, though, you'll need to select only a subset of the complete set of objects.
|
||||||
>
|
>
|
||||||
> — [Django documentation][cite]
|
> — [Django documentation][cite]
|
||||||
|
|
||||||
|
@ -77,20 +77,61 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
|
||||||
|
|
||||||
# Generic Filtering
|
# Generic Filtering
|
||||||
|
|
||||||
As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex filters that can be specified by the client using query parameters.
|
As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex searches and filters.
|
||||||
|
|
||||||
|
## Setting filter backends
|
||||||
|
|
||||||
|
The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example.
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
|
||||||
|
}
|
||||||
|
|
||||||
|
You can also set the filter backends on a per-view, or per-viewset basis,
|
||||||
|
using the `GenericAPIView` class based views.
|
||||||
|
|
||||||
|
class UserListView(generics.ListAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer = UserSerializer
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
|
## Filtering and object lookups
|
||||||
|
|
||||||
|
Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object.
|
||||||
|
|
||||||
|
For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance:
|
||||||
|
|
||||||
|
http://example.com/api/products/4675/?category=clothing&max_price=10.00
|
||||||
|
|
||||||
|
## Overriding the initial queryset
|
||||||
|
|
||||||
|
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
|
||||||
|
|
||||||
|
class PurchasedProductsList(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Return a list of all the products that the authenticated
|
||||||
|
user has ever purchased, with optional filtering.
|
||||||
|
"""
|
||||||
|
model = Product
|
||||||
|
serializer_class = ProductSerializer
|
||||||
|
filter_class = ProductFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
return user.purchase_set.all()
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Guide
|
||||||
|
|
||||||
## DjangoFilterBackend
|
## DjangoFilterBackend
|
||||||
|
|
||||||
|
The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter].
|
||||||
|
|
||||||
To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
|
To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
|
||||||
|
|
||||||
pip install django-filter
|
pip install django-filter
|
||||||
|
|
||||||
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'DEFAULT_FILTER_BACKENDS': ['rest_framework.filters.DjangoFilterBackend']
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#### Specifying filter fields
|
#### Specifying filter fields
|
||||||
|
|
||||||
|
@ -134,33 +175,72 @@ For more details on using filter sets see the [django-filter documentation][djan
|
||||||
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
|
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
|
||||||
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
|
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
|
||||||
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
||||||
|
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Filtering and object lookups
|
## SearchFilter
|
||||||
|
|
||||||
Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object.
|
The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
|
||||||
|
|
||||||
For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance:
|
The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
|
||||||
|
|
||||||
http://example.com/api/products/4675/?category=clothing&max_price=10.00
|
class UserListView(generics.ListAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer = UserSerializer
|
||||||
|
filter_backends = (filters.SearchFilter,)
|
||||||
|
search_fields = ('username', 'email')
|
||||||
|
|
||||||
## Overriding the initial queryset
|
This will allow the client to filter the items in the list by making queries such as:
|
||||||
|
|
||||||
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
|
http://example.com/api/users?search=russell
|
||||||
|
|
||||||
|
You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation:
|
||||||
|
|
||||||
|
search_fields = ('username', 'email', 'profile__profession')
|
||||||
|
|
||||||
|
By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma separated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
|
||||||
|
|
||||||
|
The search behavior may be restricted by prepending various characters to the `search_fields`.
|
||||||
|
|
||||||
|
* '^' Starts-with search.
|
||||||
|
* '=' Exact matches.
|
||||||
|
* '@' Full-text search. (Currently only supported Django's MySQL backend.)
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
search_fields = ('=username', '=email')
|
||||||
|
|
||||||
|
For more details, see the [Django documentation][search-django-admin].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OrderingFilter
|
||||||
|
|
||||||
|
The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example:
|
||||||
|
|
||||||
|
http://example.com/api/users?ordering=username
|
||||||
|
|
||||||
|
The client may also specify reverse orderings by prefixing the field name with '-', like so:
|
||||||
|
|
||||||
|
http://example.com/api/users?ordering=-username
|
||||||
|
|
||||||
|
Multiple orderings may also be specified:
|
||||||
|
|
||||||
|
http://example.com/api/users?ordering=account,username
|
||||||
|
|
||||||
|
If an `ordering` attribute is set on the view, this will be used as the default ordering.
|
||||||
|
|
||||||
|
Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
|
||||||
|
|
||||||
|
class UserListView(generics.ListAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer = UserSerializer
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('username',)
|
||||||
|
|
||||||
|
The `ordering` attribute may be either a string or a list/tuple of strings.
|
||||||
|
|
||||||
class PurchasedProductsList(generics.ListAPIView):
|
|
||||||
"""
|
|
||||||
Return a list of all the products that the authenticated
|
|
||||||
user has ever purchased, with optional filtering.
|
|
||||||
"""
|
|
||||||
model = Product
|
|
||||||
serializer_class = ProductSerializer
|
|
||||||
filter_class = ProductFilter
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user
|
|
||||||
return user.purchase_set.all()
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Custom generic filtering
|
# Custom generic filtering
|
||||||
|
@ -169,15 +249,23 @@ You can also provide your own generic filtering backend, or write an installable
|
||||||
|
|
||||||
To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method. The method should return a new, filtered queryset.
|
To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method. The method should return a new, filtered queryset.
|
||||||
|
|
||||||
To install the filter backend, set the `'DEFAULT_FILTER_BACKENDS'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class.
|
As well as allowing clients to perform searches and filtering, generic filter backends can be useful for restricting which objects should be visible to any given request or user.
|
||||||
|
|
||||||
For example:
|
## Example
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
For example, you might need to restrict users to only being able to see objects they created.
|
||||||
'DEFAULT_FILTER_BACKENDS': ['custom_filters.CustomFilterBackend']
|
|
||||||
}
|
class IsOwnerFilterBackend(filters.BaseFilterBackend):
|
||||||
|
"""
|
||||||
|
Filter that only allows users to see their own objects.
|
||||||
|
"""
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
return queryset.filter(owner=request.user)
|
||||||
|
|
||||||
|
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||||
[django-filter]: https://github.com/alex/django-filter
|
[django-filter]: https://github.com/alex/django-filter
|
||||||
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
||||||
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
||||||
|
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
||||||
|
|
|
@ -58,11 +58,11 @@ Also note that `format_suffix_patterns` does not support descending into `includ
|
||||||
|
|
||||||
## Accept headers vs. format suffixes
|
## Accept headers vs. format suffixes
|
||||||
|
|
||||||
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
|
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
|
||||||
|
|
||||||
It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators:
|
It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators:
|
||||||
|
|
||||||
“That's why I always prefer extensions. Neither choice has anything to do with REST.” — Roy Fielding, [REST discuss mailing list][cite2]
|
“That's why I always prefer extensions. Neither choice has anything to do with REST.” — Roy Fielding, [REST discuss mailing list][cite2]
|
||||||
|
|
||||||
The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern.
|
The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern.
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ For more complex cases you might also want to override various methods on the vi
|
||||||
"""
|
"""
|
||||||
Use smaller pagination for HTML representations.
|
Use smaller pagination for HTML representations.
|
||||||
"""
|
"""
|
||||||
self.request.accepted_renderer.format == 'html':
|
if self.request.accepted_renderer.format == 'html':
|
||||||
return 20
|
return 20
|
||||||
return 100
|
return 100
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ The following attributes control the basic view behavior.
|
||||||
The following attibutes are used to control pagination when used with list views.
|
The following attibutes are used to control pagination when used with list views.
|
||||||
|
|
||||||
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
|
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
|
||||||
* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
|
* `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
|
||||||
* `pagination_serializer_class` - The pagination serializer class to use when determining the style of paginated responses. Defaults to the same value as the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting.
|
* `pagination_serializer_class` - The pagination serializer class to use when determining the style of paginated responses. Defaults to the same value as the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting.
|
||||||
* `page_kwarg` - The name of a URL kwarg or URL query parameter which can be used by the client to control which page is requested. Defaults to `'page'`.
|
* `page_kwarg` - The name of a URL kwarg or URL query parameter which can be used by the client to control which page is requested. Defaults to `'page'`.
|
||||||
|
|
||||||
|
@ -164,6 +164,52 @@ You won't typically need to override the following methods, although you might n
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Mixins
|
||||||
|
|
||||||
|
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior.
|
||||||
|
|
||||||
|
## ListModelMixin
|
||||||
|
|
||||||
|
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
||||||
|
|
||||||
|
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
|
||||||
|
|
||||||
|
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
|
||||||
|
|
||||||
|
## CreateModelMixin
|
||||||
|
|
||||||
|
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
||||||
|
|
||||||
|
If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
|
||||||
|
|
||||||
|
If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||||
|
|
||||||
|
## RetrieveModelMixin
|
||||||
|
|
||||||
|
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
||||||
|
|
||||||
|
If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
||||||
|
|
||||||
|
## UpdateModelMixin
|
||||||
|
|
||||||
|
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
||||||
|
|
||||||
|
Also provides a `.partial_update(request, *args, **kwargs)` method, which is similar to the `update` method, except that all fields for the update will be optional. This allows support for HTTP `PATCH` requests.
|
||||||
|
|
||||||
|
If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
|
||||||
|
|
||||||
|
If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
|
||||||
|
|
||||||
|
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||||
|
|
||||||
|
## DestroyModelMixin
|
||||||
|
|
||||||
|
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
||||||
|
|
||||||
|
If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Concrete View Classes
|
# Concrete View Classes
|
||||||
|
|
||||||
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
|
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
|
||||||
|
@ -242,59 +288,49 @@ Extends: [GenericAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyMod
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mixins
|
# Customizing the generic views
|
||||||
|
|
||||||
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior.
|
Often you'll want to use the existing generic views, but use some slightly customized behavior. If you find yourself reusing some bit of customized behavior in multiple places, you might want to refactor the behavior into a common class that you can then just apply to any view or viewset as needed.
|
||||||
|
|
||||||
## ListModelMixin
|
## Creating custom mixins
|
||||||
|
|
||||||
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
For example, if you need to lookup objects based on multiple fields in the URL conf, you could create a mixin class like the following:
|
||||||
|
|
||||||
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
|
class MultipleFieldLookupMixin(object):
|
||||||
|
"""
|
||||||
|
Apply this mixin to any view or viewset to get multiple field filtering
|
||||||
|
based on a `lookup_fields` attribute, instead of the default single field filtering.
|
||||||
|
"""
|
||||||
|
def get_object(self):
|
||||||
|
queryset = self.get_queryset() # Get the base queryset
|
||||||
|
queryset = self.filter_queryset(queryset) # Apply any filter backends
|
||||||
|
filter = {}
|
||||||
|
for field in self.lookup_fields:
|
||||||
|
filter[field] = self.kwargs[field]
|
||||||
|
return get_object_or_404(queryset, **filter) # Lookup the object
|
||||||
|
|
||||||
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
|
You can then simply apply this mixin to a view or viewset anytime you need to apply the custom behavior.
|
||||||
|
|
||||||
Should be mixed in with [MultipleObjectAPIView].
|
class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
lookup_fields = ('account', 'username')
|
||||||
|
|
||||||
## CreateModelMixin
|
Using custom mixins is a good option if you have custom behavior that needs to be used
|
||||||
|
|
||||||
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
## Creating custom base classes
|
||||||
|
|
||||||
If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
|
If you are using a mixin across multiple views, you can take this a step further and create your own set of base views that can then be used throughout your project. For example:
|
||||||
|
|
||||||
If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
class BaseRetrieveView(MultipleFieldLookupMixin,
|
||||||
|
generics.RetrieveAPIView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin,
|
||||||
|
generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
pass
|
||||||
|
|
||||||
Should be mixed in with any [GenericAPIView].
|
Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project.
|
||||||
|
|
||||||
## RetrieveModelMixin
|
|
||||||
|
|
||||||
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
|
||||||
|
|
||||||
If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
|
||||||
|
|
||||||
## UpdateModelMixin
|
|
||||||
|
|
||||||
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
|
||||||
|
|
||||||
Also provides a `.partial_update(request, *args, **kwargs)` method, which is similar to the `update` method, except that all fields for the update will be optional. This allows support for HTTP `PATCH` requests.
|
|
||||||
|
|
||||||
If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
|
|
||||||
|
|
||||||
If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
|
|
||||||
|
|
||||||
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
|
||||||
|
|
||||||
## DestroyModelMixin
|
|
||||||
|
|
||||||
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
|
||||||
|
|
||||||
If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
|
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Permissions
|
# Permissions
|
||||||
|
|
||||||
> Authentication or identification by itself is not usually sufficient to gain access to information or code. For that, the entity requesting access must have authorization.
|
> Authentication or identification by itself is not usually sufficient to gain access to information or code. For that, the entity requesting access must have authorization.
|
||||||
>
|
>
|
||||||
> — [Apple Developer Documentation][cite]
|
> — [Apple Developer Documentation][cite]
|
||||||
|
|
||||||
|
@ -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.
|
**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 seperate 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 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.
|
||||||
|
|
||||||
For more details see the [2.2 release announcement][2.2-announcement].
|
For more details see the [2.2 release announcement][2.2-announcement].
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ By default this field is read-write, although you can change this behavior using
|
||||||
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
|
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
|
||||||
* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
|
* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
|
||||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||||
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
||||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||||
|
|
||||||
## SlugRelatedField
|
## SlugRelatedField
|
||||||
|
@ -196,15 +196,13 @@ Would serialize to a representation like this:
|
||||||
'artist': 'Thom Yorke'
|
'artist': 'Thom Yorke'
|
||||||
'track_listing': 'http://www.example.com/api/track_list/12/',
|
'track_listing': 'http://www.example.com/api/track_list/12/',
|
||||||
}
|
}
|
||||||
|
|
||||||
This field is always read-only.
|
This field is always read-only.
|
||||||
|
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
||||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
|
||||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
|
||||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -239,7 +237,7 @@ Would serialize to a nested representation like this:
|
||||||
'album_name': 'The Grey Album',
|
'album_name': 'The Grey Album',
|
||||||
'artist': 'Danger Mouse'
|
'artist': 'Danger Mouse'
|
||||||
'tracks': [
|
'tracks': [
|
||||||
{'order': 1, 'title': 'Public Service Annoucement'},
|
{'order': 1, 'title': 'Public Service Announcement'},
|
||||||
{'order': 2, 'title': 'What More Can I Say'},
|
{'order': 2, 'title': 'What More Can I Say'},
|
||||||
{'order': 3, 'title': 'Encore'},
|
{'order': 3, 'title': 'Encore'},
|
||||||
...
|
...
|
||||||
|
@ -383,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
|
||||||
|
|
||||||
For more information see [the Django documentation on generic relations][generic-relations].
|
For more information see [the Django documentation on generic relations][generic-relations].
|
||||||
|
|
||||||
|
## ManyToManyFields with a Through Model
|
||||||
|
|
||||||
|
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
|
||||||
|
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||||
|
to ``True``.
|
||||||
|
|
||||||
## Advanced Hyperlinked fields
|
## Advanced Hyperlinked fields
|
||||||
|
|
||||||
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
|
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
|
||||||
|
|
|
@ -14,7 +14,7 @@ The set of valid renderers for a view is always defined as a list of classes. W
|
||||||
|
|
||||||
The basic process of content negotiation involves examining the request's `Accept` header, to determine which media types it expects in the response. Optionally, format suffixes on the URL may be used to explicitly request a particular representation. For example the URL `http://example.com/api/users_count.json` might be an endpoint that always returns JSON data.
|
The basic process of content negotiation involves examining the request's `Accept` header, to determine which media types it expects in the response. Optionally, format suffixes on the URL may be used to explicitly request a particular representation. For example the URL `http://example.com/api/users_count.json` might be an endpoint that always returns JSON data.
|
||||||
|
|
||||||
For more information see the documentation on [content negotation][conneg].
|
For more information see the documentation on [content negotiation][conneg].
|
||||||
|
|
||||||
## Setting the renderers
|
## Setting the renderers
|
||||||
|
|
||||||
|
@ -67,14 +67,46 @@ If your API includes views that can serve both regular webpages and API response
|
||||||
|
|
||||||
## JSONRenderer
|
## JSONRenderer
|
||||||
|
|
||||||
Renders the request data into `JSON`.
|
Renders the request data into `JSON`, using utf-8 encoding.
|
||||||
|
|
||||||
|
Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example:
|
||||||
|
|
||||||
|
{"unicode black star": "\u2605"}
|
||||||
|
|
||||||
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
|
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
|
||||||
|
|
||||||
|
{
|
||||||
|
"unicode black star": "\u2605"
|
||||||
|
}
|
||||||
|
|
||||||
**.media_type**: `application/json`
|
**.media_type**: `application/json`
|
||||||
|
|
||||||
**.format**: `'.json'`
|
**.format**: `'.json'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
|
## UnicodeJSONRenderer
|
||||||
|
|
||||||
|
Renders the request data into `JSON`, using utf-8 encoding.
|
||||||
|
|
||||||
|
Note that non-ascii characters will not be character escaped. For example:
|
||||||
|
|
||||||
|
{"unicode black star": "★"}
|
||||||
|
|
||||||
|
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
|
||||||
|
|
||||||
|
{
|
||||||
|
"unicode black star": "★"
|
||||||
|
}
|
||||||
|
|
||||||
|
Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
|
||||||
|
|
||||||
|
**.media_type**: `application/json`
|
||||||
|
|
||||||
|
**.format**: `'.json'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
## JSONPRenderer
|
## JSONPRenderer
|
||||||
|
|
||||||
Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
|
Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
|
||||||
|
@ -87,6 +119,8 @@ The javascript callback function must be set by the client including a `callback
|
||||||
|
|
||||||
**.format**: `'.jsonp'`
|
**.format**: `'.jsonp'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
## YAMLRenderer
|
## YAMLRenderer
|
||||||
|
|
||||||
Renders the request data into `YAML`.
|
Renders the request data into `YAML`.
|
||||||
|
@ -97,6 +131,8 @@ Requires the `pyyaml` package to be installed.
|
||||||
|
|
||||||
**.format**: `'.yaml'`
|
**.format**: `'.yaml'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
## XMLRenderer
|
## XMLRenderer
|
||||||
|
|
||||||
Renders REST framework's default style of `XML` response content.
|
Renders REST framework's default style of `XML` response content.
|
||||||
|
@ -109,6 +145,8 @@ If you are considering using `XML` for your API, you may want to consider implem
|
||||||
|
|
||||||
**.format**: `'.xml'`
|
**.format**: `'.xml'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
## TemplateHTMLRenderer
|
## TemplateHTMLRenderer
|
||||||
|
|
||||||
Renders data to HTML, using Django's standard template rendering.
|
Renders data to HTML, using Django's standard template rendering.
|
||||||
|
@ -143,6 +181,8 @@ If you're building websites that use `TemplateHTMLRenderer` along with other ren
|
||||||
|
|
||||||
**.format**: `'.html'`
|
**.format**: `'.html'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
See also: `StaticHTMLRenderer`
|
See also: `StaticHTMLRenderer`
|
||||||
|
|
||||||
## StaticHTMLRenderer
|
## StaticHTMLRenderer
|
||||||
|
@ -163,6 +203,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
|
||||||
|
|
||||||
**.format**: `'.html'`
|
**.format**: `'.html'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
See also: `TemplateHTMLRenderer`
|
See also: `TemplateHTMLRenderer`
|
||||||
|
|
||||||
## BrowsableAPIRenderer
|
## BrowsableAPIRenderer
|
||||||
|
@ -173,12 +215,16 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
|
||||||
|
|
||||||
**.format**: `'.api'`
|
**.format**: `'.api'`
|
||||||
|
|
||||||
|
**.charset**: `utf-8`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Custom renderers
|
# 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.
|
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 arguments passed to the `.render()` method are:
|
The arguments passed to the `.render()` method are:
|
||||||
|
|
||||||
### `data`
|
### `data`
|
||||||
|
@ -187,13 +233,13 @@ The request data, as set by the `Response()` instantiation.
|
||||||
|
|
||||||
### `media_type=None`
|
### `media_type=None`
|
||||||
|
|
||||||
Optional. If provided, this is the accepted media type, as determined by the content negotiation stage.
|
Optional. If provided, this is the accepted media type, as determined by the content negotiation stage.
|
||||||
|
|
||||||
Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"application/json; nested=true"`.
|
Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"application/json; nested=true"`.
|
||||||
|
|
||||||
### `renderer_context=None`
|
### `renderer_context=None`
|
||||||
|
|
||||||
Optional. If provided, this is a dictionary of contextual information provided by the view.
|
Optional. If provided, this is a dictionary of contextual information provided by the view.
|
||||||
|
|
||||||
By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`.
|
By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`.
|
||||||
|
|
||||||
|
@ -205,14 +251,36 @@ The following is an example plaintext renderer that will return a response with
|
||||||
from rest_framework import renderers
|
from rest_framework import renderers
|
||||||
|
|
||||||
|
|
||||||
class PlainText(renderers.BaseRenderer):
|
class PlainTextRenderer(renderers.BaseRenderer):
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
format = 'txt'
|
format = 'txt'
|
||||||
|
|
||||||
def render(self, data, media_type=None, renderer_context=None):
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
if isinstance(data, basestring):
|
return data.encode(self.charset)
|
||||||
return data
|
|
||||||
return smart_unicode(data)
|
## Setting the character set
|
||||||
|
|
||||||
|
By default renderer classes are assumed to be using the `UTF-8` encoding. To use a different encoding, set the `charset` attribute on the renderer.
|
||||||
|
|
||||||
|
class PlainTextRenderer(renderers.BaseRenderer):
|
||||||
|
media_type = 'text/plain'
|
||||||
|
format = 'txt'
|
||||||
|
charset = 'iso-8859-1'
|
||||||
|
|
||||||
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
return data.encode(self.charset)
|
||||||
|
|
||||||
|
Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding.
|
||||||
|
|
||||||
|
If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
|
||||||
|
|
||||||
|
class JPEGRenderer(renderers.BaseRenderer):
|
||||||
|
media_type = 'image/jpeg'
|
||||||
|
format = 'jpg'
|
||||||
|
charset = None
|
||||||
|
|
||||||
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
return data
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -252,6 +320,15 @@ For example:
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
## Underspecifying the media type
|
||||||
|
|
||||||
|
In some cases you might want a renderer to serve a range of media types.
|
||||||
|
In this case you can underspecify the media types it should respond to, by using a `media_type` value such as `image/*`, or `*/*`.
|
||||||
|
|
||||||
|
If you underspecify the renderer's media type, you should make sure to specify the media type explicitly when you return the response, using the `content_type` attribute. For example:
|
||||||
|
|
||||||
|
return Response(data, content_type='image/png')
|
||||||
|
|
||||||
## Designing your media types
|
## Designing your media types
|
||||||
|
|
||||||
For the purposes of many Web APIs, simple `JSON` responses with hyperlinked relations may be sufficient. If you want to fully embrace RESTful design and [HATEOAS] you'll need to consider the design and usage of your media types in more detail.
|
For the purposes of many Web APIs, simple `JSON` responses with hyperlinked relations may be sufficient. If you want to fully embrace RESTful design and [HATEOAS] you'll need to consider the design and usage of your media types in more detail.
|
||||||
|
@ -274,6 +351,8 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
|
||||||
|
|
||||||
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
||||||
|
|
||||||
|
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Third party packages
|
# Third party packages
|
||||||
|
@ -291,6 +370,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||||
[conneg]: content-negotiation.md
|
[conneg]: content-negotiation.md
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
|
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||||
[cors]: http://www.w3.org/TR/cors/
|
[cors]: http://www.w3.org/TR/cors/
|
||||||
[cors-docs]: ../topics/ajax-csrf-cors.md
|
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Responses
|
# Responses
|
||||||
|
|
||||||
> Unlike basic HttpResponse objects, TemplateResponse objects retain the details of the context that was provided by the view to compute the response. The final output of the response is not computed until it is needed, later in the response process.
|
> Unlike basic HttpResponse objects, TemplateResponse objects retain the details of the context that was provided by the view to compute the response. The final output of the response is not computed until it is needed, later in the response process.
|
||||||
>
|
>
|
||||||
> — [Django documentation][cite]
|
> — [Django documentation][cite]
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ Unless you want to heavily customize REST framework for some reason, you should
|
||||||
|
|
||||||
## Response()
|
## Response()
|
||||||
|
|
||||||
**Signature:** `Response(data, status=None, template_name=None, headers=None)`
|
**Signature:** `Response(data, status=None, template_name=None, headers=None, content_type=None)`
|
||||||
|
|
||||||
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 primatives.
|
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 primatives.
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ Arguments:
|
||||||
* `status`: A status code for the response. Defaults to 200. See also [status codes][statuscodes].
|
* `status`: A status code for the response. Defaults to 200. See also [status codes][statuscodes].
|
||||||
* `template_name`: A template name to use if `HTMLRenderer` is selected.
|
* `template_name`: A template name to use if `HTMLRenderer` is selected.
|
||||||
* `headers`: A dictionary of HTTP headers to use in the response.
|
* `headers`: A dictionary of HTTP headers to use in the response.
|
||||||
|
* `content_type`: The content type of the response. Typically, this will be set automatically by the renderer as determined by content negotiation, but there may be some cases where you need to specify the content type explicitly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Routers
|
# Routers
|
||||||
|
|
||||||
> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code.
|
> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code.
|
||||||
>
|
>
|
||||||
> — [Ruby on Rails Documentation][cite]
|
> — [Ruby on Rails Documentation][cite]
|
||||||
|
|
||||||
|
@ -66,6 +66,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
|
||||||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
By default the URLs created by `SimpleRouter` are appending 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)
|
||||||
|
|
||||||
|
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
||||||
|
|
||||||
## DefaultRouter
|
## DefaultRouter
|
||||||
|
|
||||||
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
||||||
|
@ -83,15 +90,19 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
|
||||||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||||
</table>
|
</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.
|
||||||
|
|
||||||
|
router = DefaultRouter(trailing_slash=False)
|
||||||
|
|
||||||
# Custom Routers
|
# Custom Routers
|
||||||
|
|
||||||
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specfic 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 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.
|
||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The following example will only route to the `list` and `retrieve` actions, and unlike the routers included by REST framework, it does not use the trailing slash convention.
|
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
|
||||||
|
|
||||||
class ReadOnlyRouter(SimpleRouter):
|
class ReadOnlyRouter(SimpleRouter):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# Serializers
|
# Serializers
|
||||||
|
|
||||||
> Expanding the usefulness of the serializers is something that we would
|
> Expanding the usefulness of the serializers is something that we would
|
||||||
like to address. However, it's not a trivial problem, and it
|
like to address. However, it's not a trivial problem, and it
|
||||||
will take some serious design work.
|
will take some serious design work.
|
||||||
>
|
>
|
||||||
> — Russell Keith-Magee, [Django users group][cite]
|
> — Russell Keith-Magee, [Django users group][cite]
|
||||||
|
@ -104,11 +104,11 @@ When deserializing a list of items, errors will be returned as a list of diction
|
||||||
|
|
||||||
#### Field-level validation
|
#### Field-level validation
|
||||||
|
|
||||||
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
|
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analogous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
|
||||||
|
|
||||||
They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided).
|
They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided).
|
||||||
|
|
||||||
Your `validate_<fieldname>` methods should either just return the `attrs` dictionary or raise a `ValidationError`. For example:
|
Your `validate_<fieldname>` methods should either just return the `attrs` dictionary or raise a `ValidationError`. For example:
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ Your `validate_<fieldname>` methods should either just return the `attrs` dictio
|
||||||
|
|
||||||
#### Object-level validation
|
#### Object-level validation
|
||||||
|
|
||||||
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example:
|
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example:
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@ To map the incoming data items to their corresponding object instances, the `.ge
|
||||||
|
|
||||||
There are some cases where you need to provide extra context to the serializer in addition to the object being serialized. One common case is if you're using a serializer that includes hyperlinked relations, which requires the serializer to have access to the current request so that it can properly generate fully qualified URLs.
|
There are some cases where you need to provide extra context to the serializer in addition to the object being serialized. One common case is if you're using a serializer that includes hyperlinked relations, which requires the serializer to have access to the current request so that it can properly generate fully qualified URLs.
|
||||||
|
|
||||||
You can provide arbitrary additional context by passing a `context` argument when instantiating the serializer. For example:
|
You can provide arbitrary additional context by passing a `context` argument when instantiating the serializer. For example:
|
||||||
|
|
||||||
serializer = AccountSerializer(account, context={'request': request})
|
serializer = AccountSerializer(account, context={'request': request})
|
||||||
serializer.data
|
serializer.data
|
||||||
|
@ -333,7 +333,7 @@ The `depth` option should be set to an integer value that indicates the depth of
|
||||||
|
|
||||||
## Specifying which fields should be read-only
|
## Specifying which fields should be read-only
|
||||||
|
|
||||||
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
||||||
|
|
||||||
class AccountSerializer(serializers.ModelSerializer):
|
class AccountSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -425,7 +425,7 @@ Doing so should be considered advanced usage, and will only be needed if you hav
|
||||||
|
|
||||||
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
|
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
|
||||||
|
|
||||||
For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`.
|
For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`.
|
||||||
|
|
||||||
### get_pk_field
|
### get_pk_field
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,7 @@ Default: `None`
|
||||||
|
|
||||||
#### PAGINATE_BY_PARAM
|
#### PAGINATE_BY_PARAM
|
||||||
|
|
||||||
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
|
The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size.
|
||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Status Codes
|
# Status Codes
|
||||||
|
|
||||||
> 418 I'm a teapot - Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.
|
> 418 I'm a teapot - Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.
|
||||||
>
|
>
|
||||||
> — [RFC 2324][rfc2324], Hyper Text Coffee Pot Control Protocol
|
> — [RFC 2324][rfc2324], Hyper Text Coffee Pot Control Protocol
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ This class of status code indicates that further action needs to be taken by the
|
||||||
|
|
||||||
## Client Error - 4xx
|
## Client Error - 4xx
|
||||||
|
|
||||||
The 4xx class of status code is intended for cases in which the client seems to have erred. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
|
The 4xx class of status code is intended for cases in which the client seems to have erred. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
|
||||||
|
|
||||||
HTTP_400_BAD_REQUEST
|
HTTP_400_BAD_REQUEST
|
||||||
HTTP_401_UNAUTHORIZED
|
HTTP_401_UNAUTHORIZED
|
||||||
|
@ -79,7 +79,7 @@ The 4xx class of status code is intended for cases in which the client seems to
|
||||||
|
|
||||||
## Server Error - 5xx
|
## Server Error - 5xx
|
||||||
|
|
||||||
Response status codes beginning with the digit "5" indicate cases in which the server is aware that it has erred or is incapable of performing the request. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
|
Response status codes beginning with the digit "5" indicate cases in which the server is aware that it has erred or is incapable of performing the request. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition.
|
||||||
|
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR
|
HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
HTTP_501_NOT_IMPLEMENTED
|
HTTP_501_NOT_IMPLEMENTED
|
||||||
|
|
|
@ -122,13 +122,13 @@ You won't typically need to override this method.
|
||||||
>
|
>
|
||||||
> — [Nick Coghlan][cite2]
|
> — [Nick Coghlan][cite2]
|
||||||
|
|
||||||
REST framework also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of `Request` (rather than the usual Django `HttpRequest`) and allows them to return a `Response` (instead of a Django `HttpResponse`), and allow you to configure how the request is processed.
|
REST framework also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of `Request` (rather than the usual Django `HttpRequest`) and allows them to return a `Response` (instead of a Django `HttpResponse`), and allow you to configure how the request is processed.
|
||||||
|
|
||||||
## @api_view()
|
## @api_view()
|
||||||
|
|
||||||
**Signature:** `@api_view(http_method_names)`
|
**Signature:** `@api_view(http_method_names)`
|
||||||
|
|
||||||
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
|
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
|
||||||
|
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ This view will use the default renderers, parsers, authentication classes etc sp
|
||||||
|
|
||||||
## API policy decorators
|
## 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.decorators import api_view, throttle_classes
|
||||||
from rest_framework.throttling import UserRateThrottle
|
from rest_framework.throttling import UserRateThrottle
|
||||||
|
|
|
@ -13,11 +13,11 @@ A `ViewSet` class is simply **a type of class-based View, that does not provide
|
||||||
|
|
||||||
The method handlers for a `ViewSet` are only bound to the corresponding actions at the point of finalizing the view, using the `.as_view()` method.
|
The method handlers for a `ViewSet` are only bound to the corresponding actions at the point of finalizing the view, using the `.as_view()` method.
|
||||||
|
|
||||||
Typically, rather than exlicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.
|
Typically, rather than explicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Let's define a simple viewset that can be used to listing or retrieving all the users in the system.
|
Let's define a simple viewset that can be used to list or retrieve all the users in the system.
|
||||||
|
|
||||||
class UserViewSet(viewsets.ViewSet):
|
class UserViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -34,7 +34,7 @@ Let's define a simple viewset that can be used to listing or retrieving all the
|
||||||
serializer = UserSerializer(user)
|
serializer = UserSerializer(user)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
If we need to, we can bind this viewset into two seperate views, like so:
|
If we need to, we can bind this viewset into two separate views, like so:
|
||||||
|
|
||||||
user_list = UserViewSet.as_view({'get': 'list'})
|
user_list = UserViewSet.as_view({'get': 'list'})
|
||||||
user_detail = UserViewSet.as_view({'get': 'retrieve'})
|
user_detail = UserViewSet.as_view({'get': 'retrieve'})
|
||||||
|
@ -65,7 +65,7 @@ Both of these come with a trade-off. Using regular views and URL confs is more
|
||||||
|
|
||||||
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below:
|
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below:
|
||||||
|
|
||||||
class UserViewSet(viewsets.VietSet):
|
class UserViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
Example empty viewset demonstrating the standard
|
Example empty viewset demonstrating the standard
|
||||||
actions that will be handled by a router class.
|
actions that will be handled by a router class.
|
||||||
|
@ -92,7 +92,7 @@ The default routers included with REST framework will provide routes for a stand
|
||||||
def destroy(self, request, pk=None):
|
def destroy(self, request, pk=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decroator will route `POST` requests.
|
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -126,6 +126,11 @@ The `@action` and `@link` decorators can additionally take extra arguments that
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
|
||||||
|
|
||||||
|
@action(methods=['POST', 'DELETE'])
|
||||||
|
def unset_password(self, request, pk=None):
|
||||||
|
...
|
||||||
---
|
---
|
||||||
|
|
||||||
# API Reference
|
# API Reference
|
||||||
|
@ -136,9 +141,15 @@ The `ViewSet` class inherits from `APIView`. You can use any of the standard at
|
||||||
|
|
||||||
The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly.
|
The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly.
|
||||||
|
|
||||||
|
## GenericViewSet
|
||||||
|
|
||||||
|
The `GenericViewSet` class inherits from `GenericAPIView`, and provides the default set of `get_object`, `get_queryset` methods and other generic view base behavior, but does not include any actions by default.
|
||||||
|
|
||||||
|
In order to use a `GenericViewSet` class you'll override the class and either mixin the required mixin classes, or define the action implementations explicitly.
|
||||||
|
|
||||||
## ModelViewSet
|
## ModelViewSet
|
||||||
|
|
||||||
The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the
|
The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
|
||||||
|
|
||||||
The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`.
|
The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`.
|
||||||
|
|
||||||
|
@ -188,17 +199,18 @@ Again, as with `ModelViewSet`, you can use any of the standard attributes and me
|
||||||
|
|
||||||
# Custom ViewSet base classes
|
# Custom ViewSet base classes
|
||||||
|
|
||||||
Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes.
|
You may need to provide custom `ViewSet` classes that do not have the full set of `ModelViewSet` actions, or that customize the behavior in some other way.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
For example, we can create a base viewset class that provides `retrieve`, `update` and `list` operations:
|
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,
|
||||||
|
viewsets.GenericViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
class RetrieveUpdateListViewSet(mixins.RetrieveModelMixin,
|
|
||||||
mixins.UpdateModelMixin,
|
|
||||||
mixins.ListModelMixin,
|
|
||||||
viewsets.ViewSetMixin,
|
|
||||||
generics.GenericAPIView):
|
|
||||||
"""
|
"""
|
||||||
A viewset that provides `retrieve`, `update`, and `list` actions.
|
A viewset that provides `retrieve`, `update`, and `list` actions.
|
||||||
|
|
||||||
|
@ -207,6 +219,6 @@ For example, we can create a base viewset class that provides `retrieve`, `updat
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API.
|
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
|
||||||
|
|
||||||
[cite]: http://guides.rubyonrails.org/routing.html
|
[cite]: http://guides.rubyonrails.org/routing.html
|
||||||
|
|
|
@ -103,6 +103,10 @@ pre {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-list > li > a {
|
||||||
|
padding: 2px 15px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Set the table of contents to static so it flows back into the content when
|
/* Set the table of contents to static so it flows back into the content when
|
||||||
viewed on tablets and smaller. */
|
viewed on tablets and smaller. */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
@ -297,4 +301,5 @@ td, th {
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<p class="badges">
|
<p class="badges">
|
||||||
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
|
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
|
||||||
|
|
||||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none">Tweet</a>
|
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
|
||||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||||
|
|
||||||
<img alt="Travis build image" src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
|
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Django REST framework
|
# Django REST framework
|
||||||
|
@ -15,7 +15,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
|
||||||
|
|
||||||
Some reasons you might want to use REST framework:
|
Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
* The Web browseable API is a huge useability win for your developers.
|
* The Web browseable API is a huge usability win for your developers.
|
||||||
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
||||||
* Serialization that supports both ORM and non-ORM data sources.
|
* Serialization that supports both ORM and non-ORM data sources.
|
||||||
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
||||||
|
@ -32,7 +32,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
||||||
REST framework requires the following:
|
REST framework requires the following:
|
||||||
|
|
||||||
* Python (2.6.5+, 2.7, 3.2, 3.3)
|
* Python (2.6.5+, 2.7, 3.2, 3.3)
|
||||||
* Django (1.3, 1.4, 1.5)
|
* Django (1.3, 1.4, 1.5, 1.6)
|
||||||
|
|
||||||
The following packages are optional:
|
The following packages are optional:
|
||||||
|
|
||||||
|
@ -113,8 +113,8 @@ Here's our project's root `urls.py` module:
|
||||||
|
|
||||||
# Routers provide an easy way of automatically determining the URL conf
|
# Routers provide an easy way of automatically determining the URL conf
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'users', views.UserViewSet)
|
router.register(r'users', UserViewSet)
|
||||||
router.register(r'groups', views.GroupViewSet)
|
router.register(r'groups', GroupViewSet)
|
||||||
|
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
|
@ -206,7 +206,13 @@ For updates on REST framework development, you may also want to follow [the auth
|
||||||
|
|
||||||
<a style="padding-top: 10px" href="https://twitter.com/_tomchristie" class="twitter-follow-button" data-show-count="false">Follow @_tomchristie</a>
|
<a style="padding-top: 10px" href="https://twitter.com/_tomchristie" class="twitter-follow-button" data-show-count="false">Follow @_tomchristie</a>
|
||||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||||
|
|
||||||
|
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2011-2013, Tom Christie
|
Copyright (c) 2011-2013, Tom Christie
|
||||||
|
@ -294,6 +300,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[stack-overflow]: http://stackoverflow.com/
|
[stack-overflow]: http://stackoverflow.com/
|
||||||
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
|
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
|
||||||
[django-tag]: http://stackoverflow.com/questions/tagged/django
|
[django-tag]: http://stackoverflow.com/questions/tagged/django
|
||||||
|
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
||||||
[paid-support]: http://dabapps.com/services/build/api-development/
|
[paid-support]: http://dabapps.com/services/build/api-development/
|
||||||
[dabapps]: http://dabapps.com
|
[dabapps]: http://dabapps.com
|
||||||
[contact-dabapps]: http://dabapps.com/contact/
|
[contact-dabapps]: http://dabapps.com/contact/
|
||||||
|
|
|
@ -75,7 +75,7 @@ This more explicit behavior on serializing and deserializing data [makes integra
|
||||||
|
|
||||||
The implicit to-many behavior on serializers, and the `ManyRelatedField` style classes will continue to function, but will raise a `PendingDeprecationWarning`, which can be made visible using the `-Wd` flag.
|
The implicit to-many behavior on serializers, and the `ManyRelatedField` style classes will continue to function, but will raise a `PendingDeprecationWarning`, which can be made visible using the `-Wd` flag.
|
||||||
|
|
||||||
**Note**: If you need to forcibly turn off the implict "`many=True` for `__iter__` objects" behavior, you can now do so by specifying `many=False`. This will become the default (instead of the current default of `None`) once the deprecation of the implicit behavior is finalised in version 2.4.
|
**Note**: If you need to forcibly turn off the implicit "`many=True` for `__iter__` objects" behavior, you can now do so by specifying `many=False`. This will become the default (instead of the current default of `None`) once the deprecation of the implicit behavior is finalised in version 2.4.
|
||||||
|
|
||||||
### Cleaner optional relationships
|
### Cleaner optional relationships
|
||||||
|
|
||||||
|
@ -103,9 +103,9 @@ The `blank` keyword argument will continue to function, but will raise a `Pendin
|
||||||
|
|
||||||
### Simpler object-level permissions
|
### Simpler object-level permissions
|
||||||
|
|
||||||
Custom permissions classes previously used the signatute `.has_permission(self, request, view, obj=None)`. This method would be called twice, firstly for the global permissions check, with the `obj` parameter set to `None`, and again for the object-level permissions check when appropriate, with the `obj` parameter set to the relevant model instance.
|
Custom permissions classes previously used the signature `.has_permission(self, request, view, obj=None)`. This method would be called twice, firstly for the global permissions check, with the `obj` parameter set to `None`, and again for the object-level permissions check when appropriate, with the `obj` parameter set to the relevant model instance.
|
||||||
|
|
||||||
The global permissions check and object-level permissions check are now seperated into two seperate methods, which gives a cleaner, more obvious API.
|
The global permissions check and object-level permissions check are now separated into two separate methods, which gives a cleaner, more obvious API.
|
||||||
|
|
||||||
* Global permission checks now use the `.has_permission(self, request, view)` signature.
|
* Global permission checks now use the `.has_permission(self, request, view)` signature.
|
||||||
* Object-level permission checks use a new method `.has_object_permission(self, request, view, obj)`.
|
* Object-level permission checks use a new method `.has_object_permission(self, request, view, obj)`.
|
||||||
|
|
|
@ -30,8 +30,8 @@ As an example of just how simple REST framework APIs can now be, here's an API w
|
||||||
|
|
||||||
# Routers provide an easy way of automatically determining the URL conf
|
# Routers provide an easy way of automatically determining the URL conf
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'users', views.UserViewSet)
|
router.register(r'users', UserViewSet)
|
||||||
router.register(r'groups', views.GroupViewSet)
|
router.register(r'groups', GroupViewSet)
|
||||||
|
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
|
@ -45,13 +45,13 @@ The best place to get started with ViewSets and Routers is to take a look at the
|
||||||
|
|
||||||
## Simpler views
|
## Simpler views
|
||||||
|
|
||||||
This release rationalises the API and implementation of the generic views, dropping the dependancy on Django's `SingleObjectMixin` and `MultipleObjectMixin` classes, removing a number of unneeded attributes, and generally making the implementation more obvious and easy to work with.
|
This release rationalises the API and implementation of the generic views, dropping the dependency on Django's `SingleObjectMixin` and `MultipleObjectMixin` classes, removing a number of unneeded attributes, and generally making the implementation more obvious and easy to work with.
|
||||||
|
|
||||||
This improvement is reflected in improved documentation for the `GenericAPIView` base class, and should make it easier to determine how to override methods on the base class if you need to write customized subclasses.
|
This improvement is reflected in improved documentation for the `GenericAPIView` base class, and should make it easier to determine how to override methods on the base class if you need to write customized subclasses.
|
||||||
|
|
||||||
## Easier Serializers
|
## Easier Serializers
|
||||||
|
|
||||||
REST framework lets you be totally explict regarding how you want to represent relationships, allowing you to choose between styles such as hyperlinking or primary key relationships.
|
REST framework lets you be totally explicit regarding how you want to represent relationships, allowing you to choose between styles such as hyperlinking or primary key relationships.
|
||||||
|
|
||||||
The ability to specify exactly how you want to represent relationships is powerful, but it also introduces complexity. In order to keep things more simple, REST framework now allows you to include reverse relationships simply by including the field name in the `fields` metadata of the serializer class.
|
The ability to specify exactly how you want to represent relationships is powerful, but it also introduces complexity. In order to keep things more simple, REST framework now allows you to include reverse relationships simply by including the field name in the `fields` metadata of the serializer class.
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ Using the `SingleObjectAPIView` and `MultipleObjectAPIView` base classes continu
|
||||||
|
|
||||||
### Removed attributes
|
### Removed attributes
|
||||||
|
|
||||||
The following attributes and methods, were previously present as part of Django's generic view implementations, but were unneeded and unusedand have now been entirely removed.
|
The following attributes and methods, were previously present as part of Django's generic view implementations, but were unneeded and unused and have now been entirely removed.
|
||||||
|
|
||||||
* context_object_name
|
* context_object_name
|
||||||
* get_context_data()
|
* get_context_data()
|
||||||
|
@ -173,7 +173,7 @@ For example:
|
||||||
raise Http404
|
raise Http404
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
In our opinion removing lesser-used attributes like `allow_empty` helps us move towards simpler generic view implementations, making them more obvious to use and override, and re-inforcing the preferred style of developers writing their own base classes and mixins for custom behavior rather than relying on the configurability of the generic views.
|
In our opinion removing lesser-used attributes like `allow_empty` helps us move towards simpler generic view implementations, making them more obvious to use and override, and re-enforcing the preferred style of developers writing their own base classes and mixins for custom behavior rather than relying on the configurability of the generic views.
|
||||||
|
|
||||||
## Simpler URL lookups
|
## Simpler URL lookups
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Working with AJAX, CSRF & CORS
|
# Working with AJAX, CSRF & CORS
|
||||||
|
|
||||||
> "Take a close look at possible CSRF / XSRF vulnerabilities on your own websites. They're the worst kind of vulnerability — very easy to exploit by attackers, yet not so intuitively easy to understand for software developers, at least until you've been bitten by one."
|
> "Take a close look at possible CSRF / XSRF vulnerabilities on your own websites. They're the worst kind of vulnerability — very easy to exploit by attackers, yet not so intuitively easy to understand for software developers, at least until you've been bitten by one."
|
||||||
>
|
>
|
||||||
> — [Jeff Atwood][cite]
|
> — [Jeff Atwood][cite]
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
# The Browsable API
|
# The Browsable API
|
||||||
|
|
||||||
> It is a profoundly erroneous truism... that we should cultivate the habit of thinking of what we are doing. The precise opposite is the case. Civilization advances by extending the number of important operations which we can perform without thinking about them.
|
> It is a profoundly erroneous truism... that we should cultivate the habit of thinking of what we are doing. The precise opposite is the case. Civilization advances by extending the number of important operations which we can perform without thinking about them.
|
||||||
>
|
>
|
||||||
> — [Alfred North Whitehead][cite], An Introduction to Mathematics (1911)
|
> — [Alfred North Whitehead][cite], An Introduction to Mathematics (1911)
|
||||||
|
|
||||||
|
|
||||||
API may stand for Application *Programming* Interface, but humans have to be able to read the APIs, too; someone has to do the programming. Django REST Framework supports generating human-friendly HTML output for each resource when the `HTML` format is requested. These pages allow for easy browsing of resources, as well as forms for submitting data to the resources using `POST`, `PUT`, and `DELETE`.
|
API may stand for Application *Programming* Interface, but humans have to be able to read the APIs, too; someone has to do the programming. Django REST Framework supports generating human-friendly HTML output for each resource when the `HTML` format is requested. These pages allow for easy browsing of resources, as well as forms for submitting data to the resources using `POST`, `PUT`, and `DELETE`.
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
If you include fully-qualified URLs in your resource output, they will be 'urlized' and made clickable for easy browsing by humans. The `rest_framework` package includes a [`reverse`][drfreverse] helper for this purpose.
|
If you include fully-qualified URLs in your resource output, they will be 'urlized' and made clickable for easy browsing by humans. The `rest_framework` package includes a [`reverse`][drfreverse] helper for this purpose.
|
||||||
|
|
||||||
|
|
||||||
## Formats
|
## Formats
|
||||||
|
|
||||||
By default, the API will return the format specified by the headers, which in the case of the browser is HTML. The format can be specified using `?format=` in the request, so you can look at the raw JSON response in a browser by adding `?format=json` to the URL. There are helpful extensions for viewing JSON in [Firefox][ffjsonview] and [Chrome][chromejsonview].
|
By default, the API will return the format specified by the headers, which in the case of the browser is HTML. The format can be specified using `?format=` in the request, so you can look at the raw JSON response in a browser by adding `?format=json` to the URL. There are helpful extensions for viewing JSON in [Firefox][ffjsonview] and [Chrome][chromejsonview].
|
||||||
|
|
||||||
|
|
||||||
## Customizing
|
## Customizing
|
||||||
|
@ -25,15 +25,26 @@ The included browsable API template is built with [Bootstrap (2.1.1)][bootstrap]
|
||||||
|
|
||||||
### Theme
|
### Theme
|
||||||
|
|
||||||
To replace the theme wholesale, add a `bootstrap_theme` block to your `api.html` and insert a `link` to the desired Bootstrap theme css file. This will completely replace the included theme.
|
To replace the theme wholesale, add a `bootstrap_theme` block to your `api.html` and insert a `link` to the desired Bootstrap theme css file. This will completely replace the included theme.
|
||||||
|
|
||||||
{% block bootstrap_theme %}
|
{% block bootstrap_theme %}
|
||||||
<link rel="stylesheet" href="/path/to/my/bootstrap.css" type="text/css">
|
<link rel="stylesheet" href="/path/to/my/bootstrap.css" type="text/css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
A suitable replacement theme can be generated using Bootstrap's [Customize Tool][bcustomize]. Also, there are pre-made themes available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above.
|
A suitable replacement theme can be generated using Bootstrap's [Customize Tool][bcustomize]. Also, there are pre-made themes available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above.
|
||||||
|
|
||||||
|
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
||||||
|
|
||||||
|
Full Example
|
||||||
|
|
||||||
|
{% extends "rest_framework/base.html" %}
|
||||||
|
|
||||||
|
{% block bootstrap_theme %}
|
||||||
|
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bootstrap_navbar_variant %}{% endblock %}
|
||||||
|
|
||||||
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
|
||||||
|
|
||||||
For more specific CSS tweaks, use the `style` block instead.
|
For more specific CSS tweaks, use the `style` block instead.
|
||||||
|
|
||||||
|
@ -46,11 +57,11 @@ All of the blocks available in the browsable API base template that can be used
|
||||||
* `bootstrap_theme` - CSS for the Bootstrap theme
|
* `bootstrap_theme` - CSS for the Bootstrap theme
|
||||||
* `bootstrap_navbar_variant` - CSS class for the navbar
|
* `bootstrap_navbar_variant` - CSS class for the navbar
|
||||||
* `branding` - section of the navbar, see [Bootstrap components][bcomponentsnav]
|
* `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.
|
* `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)
|
* `footer` - Any copyright notices or similar footer materials can go here (by default right-aligned)
|
||||||
* `style` - CSS stylesheets for the page
|
* `style` - CSS stylesheets for the page
|
||||||
* `title` - title of 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.
|
* `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.
|
||||||
|
|
||||||
#### Components
|
#### Components
|
||||||
|
|
||||||
|
@ -58,7 +69,7 @@ All of the [Bootstrap components][bcomponents] are available.
|
||||||
|
|
||||||
##### Tooltips
|
##### Tooltips
|
||||||
|
|
||||||
The browsable API makes use of the Bootstrap tooltips component. Any element with the `js-tooltip` class and a `title` attribute has that title content displayed in a tooltip on hover after a 1000ms delay.
|
The browsable API makes use of the Bootstrap tooltips component. Any element with the `js-tooltip` class and a `title` attribute has that title content displayed in a tooltip on hover after a 1000ms delay.
|
||||||
|
|
||||||
### Login Template
|
### Login Template
|
||||||
|
|
||||||
|
@ -96,7 +107,7 @@ The context that's available to the template:
|
||||||
|
|
||||||
#### Not using base.html
|
#### Not using base.html
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead
|
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead
|
||||||
[drfreverse]: ../api-guide/reverse.md
|
[drfreverse]: ../api-guide/reverse.md
|
||||||
|
|
|
@ -21,7 +21,7 @@ For example, given the following form:
|
||||||
|
|
||||||
## HTTP header based method overriding
|
## HTTP header based method overriding
|
||||||
|
|
||||||
REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb].
|
REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb].
|
||||||
|
|
||||||
To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header.
|
To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header.
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ have any control over what is sent in the `Accept` header.
|
||||||
## URL based format suffixes
|
## URL based format suffixes
|
||||||
|
|
||||||
REST framework can take `?format=json` style URL parameters, which can be a
|
REST framework can take `?format=json` style URL parameters, which can be a
|
||||||
useful shortcut for determing which content type should be returned from
|
useful shortcut for determining which content type should be returned from
|
||||||
the view.
|
the view.
|
||||||
|
|
||||||
This is a more concise than using the `accept` override, but it also gives
|
This is a more concise than using the `accept` override, but it also gives
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Contributing to REST framework
|
# Contributing to REST framework
|
||||||
|
|
||||||
> The world can only really be changed one piece at a time. The art is picking that piece.
|
> The world can only really be changed one piece at a time. The art is picking that piece.
|
||||||
>
|
>
|
||||||
> — [Tim Berners-Lee][cite]
|
> — [Tim Berners-Lee][cite]
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ It's really helpful if you make sure you address issues to the correct channel.
|
||||||
|
|
||||||
Some tips on good issue reporting:
|
Some tips on good issue reporting:
|
||||||
|
|
||||||
* When decribing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
||||||
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
||||||
* If reporting a bug, then try to include a pull request with a failing test case. This'll help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
|
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ Some tips on good issue reporting:
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
|
||||||
* git clone & PYTHONPATH
|
* git clone & PYTHONPATH
|
||||||
* Pep8
|
* Pep8
|
||||||
* Recommend editor that runs pep8
|
* Recommend editor that runs pep8
|
||||||
|
|
|
@ -120,6 +120,25 @@ The following people have helped make REST framework great.
|
||||||
* Jerome Chen - [chenjyw]
|
* Jerome Chen - [chenjyw]
|
||||||
* Andrew Hughes - [eyepulp]
|
* Andrew Hughes - [eyepulp]
|
||||||
* Daniel Hepper - [dhepper]
|
* Daniel Hepper - [dhepper]
|
||||||
|
* Hamish Campbell - [hamishcampbell]
|
||||||
|
* Marlon Bailey - [avinash240]
|
||||||
|
* James Summerfield - [jsummerfield]
|
||||||
|
* Andy Freeland - [rouge8]
|
||||||
|
* Craig de Stigter - [craigds]
|
||||||
|
* Pablo Recio - [pyriku]
|
||||||
|
* Brian Zambrano - [brianz]
|
||||||
|
* Òscar Vilaplana - [grimborg]
|
||||||
|
* Ryan Kaskel - [ryankask]
|
||||||
|
* Andy McKay - [andymckay]
|
||||||
|
* Matteo Suppo - [matteosuppo]
|
||||||
|
* Karol Majta - [lolek09]
|
||||||
|
* David Jones - [commonorgarden]
|
||||||
|
* Andrew Tarzwell - [atarzwell]
|
||||||
|
* Michal Dvořák - [mikee2185]
|
||||||
|
* Markus Törnqvist - [mjtorn]
|
||||||
|
* Pascal Borreli - [pborreli]
|
||||||
|
* Alex Burgel - [aburgel]
|
||||||
|
* David Medina - [copitux]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -133,7 +152,7 @@ Continuous integration testing is managed with [Travis CI][travis-ci].
|
||||||
|
|
||||||
The [live sandbox][sandbox] is hosted on [Heroku].
|
The [live sandbox][sandbox] is hosted on [Heroku].
|
||||||
|
|
||||||
Various inspiration taken from the [Rails], [Piston], [Tastypie] and [Dagny] projects.
|
Various inspiration taken from the [Rails], [Piston], [Tastypie], [Dagny] and [django-viewsets] projects.
|
||||||
|
|
||||||
Development of REST framework 2.0 was sponsored by [DabApps].
|
Development of REST framework 2.0 was sponsored by [DabApps].
|
||||||
|
|
||||||
|
@ -152,6 +171,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[piston]: https://bitbucket.org/jespern/django-piston
|
[piston]: https://bitbucket.org/jespern/django-piston
|
||||||
[tastypie]: https://github.com/toastdriven/django-tastypie
|
[tastypie]: https://github.com/toastdriven/django-tastypie
|
||||||
[dagny]: https://github.com/zacharyvoase/dagny
|
[dagny]: https://github.com/zacharyvoase/dagny
|
||||||
|
[django-viewsets]: https://github.com/BertrandBordage/django-viewsets
|
||||||
[dabapps]: http://lab.dabapps.com
|
[dabapps]: http://lab.dabapps.com
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
[heroku]: http://www.heroku.com/
|
[heroku]: http://www.heroku.com/
|
||||||
|
@ -275,3 +295,22 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[chenjyw]: https://github.com/chenjyw
|
[chenjyw]: https://github.com/chenjyw
|
||||||
[eyepulp]: https://github.com/eyepulp
|
[eyepulp]: https://github.com/eyepulp
|
||||||
[dhepper]: https://github.com/dhepper
|
[dhepper]: https://github.com/dhepper
|
||||||
|
[hamishcampbell]: https://github.com/hamishcampbell
|
||||||
|
[avinash240]: https://github.com/avinash240
|
||||||
|
[jsummerfield]: https://github.com/jsummerfield
|
||||||
|
[rouge8]: https://github.com/rouge8
|
||||||
|
[craigds]: https://github.com/craigds
|
||||||
|
[pyriku]: https://github.com/pyriku
|
||||||
|
[brianz]: https://github.com/brianz
|
||||||
|
[grimborg]: https://github.com/grimborg
|
||||||
|
[ryankask]: https://github.com/ryankask
|
||||||
|
[andymckay]: https://github.com/andymckay
|
||||||
|
[matteosuppo]: https://github.com/matteosuppo
|
||||||
|
[lolek09]: https://github.com/lolek09
|
||||||
|
[commonorgarden]: https://github.com/commonorgarden
|
||||||
|
[atarzwell]: https://github.com/atarzwell
|
||||||
|
[mikee2185]: https://github.com/mikee2185
|
||||||
|
[mjtorn]: https://github.com/mjtorn
|
||||||
|
[pborreli]: https://github.com/pborreli
|
||||||
|
[aburgel]: https://github.com/aburgel
|
||||||
|
[copitux]: https://github.com/copitux
|
||||||
|
|
|
@ -40,6 +40,46 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
||||||
|
### 2.3.5
|
||||||
|
|
||||||
|
**Date**: 3rd June 2013
|
||||||
|
|
||||||
|
* Added `get_url` hook to `HyperlinkedIdentityField`.
|
||||||
|
* Serializer field `default` argument may be a callable.
|
||||||
|
* `@action` decorator now accepts a `methods` argument.
|
||||||
|
* Bugfix: `request.user` should be still be accessible in renderer context if authentication fails.
|
||||||
|
* Bugfix: The `lookup_field` option on `HyperlinkedIdentityField` should apply by default to the url field on the serializer.
|
||||||
|
* Bugfix: `HyperlinkedIdentityField` should continue to support `pk_url_kwarg`, `slug_url_kwarg`, `slug_field`, in a pending deprecation state.
|
||||||
|
* Bugfix: Ensure we always return 404 instead of 500 if a lookup field cannot be converted to the correct lookup type. (Eg non-numeric `AutoInteger` pk lookup)
|
||||||
|
|
||||||
|
### 2.3.4
|
||||||
|
|
||||||
|
**Date**: 24th May 2013
|
||||||
|
|
||||||
|
* Serializer fields now support `label` and `help_text`.
|
||||||
|
* Added `UnicodeJSONRenderer`.
|
||||||
|
* `OPTIONS` requests now return metadata about fields for `POST` and `PUT` requests.
|
||||||
|
* Bugfix: `charset` now properly included in `Content-Type` of responses.
|
||||||
|
* Bugfix: Blank choice now added in browsable API on nullable relationships.
|
||||||
|
* Bugfix: Many to many relationships with `through` tables are now read-only.
|
||||||
|
* Bugfix: Serializer fields now respect model field args such as `max_length`.
|
||||||
|
* Bugfix: SlugField now performs slug validation.
|
||||||
|
* Bugfix: Lazy-translatable strings now properly serialized.
|
||||||
|
* Bugfix: Browsable API now supports bootswatch styles properly.
|
||||||
|
* Bugfix: HyperlinkedIdentityField now uses `lookup_field` kwarg.
|
||||||
|
|
||||||
|
**Note**: Responses now correctly include an appropriate charset on the `Content-Type` header. For example: `application/json; charset=utf-8`. If you have tests that check the content type of responses, you may need to update these accordingly.
|
||||||
|
|
||||||
|
### 2.3.3
|
||||||
|
|
||||||
|
**Date**: 16th May 2013
|
||||||
|
|
||||||
|
* Added SearchFilter
|
||||||
|
* Added OrderingFilter
|
||||||
|
* Added GenericViewSet
|
||||||
|
* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
|
||||||
|
* Bugfix: Fix API Root view issue with DjangoModelPermissions
|
||||||
|
|
||||||
### 2.3.2
|
### 2.3.2
|
||||||
|
|
||||||
**Date**: 8th May 2013
|
**Date**: 8th May 2013
|
||||||
|
@ -78,14 +118,14 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
**Date**: 17th April 2013
|
**Date**: 17th April 2013
|
||||||
|
|
||||||
* Loud failure when view does not return a `Response` or `HttpResponse`.
|
* Loud failure when view does not return a `Response` or `HttpResponse`.
|
||||||
* Bugfix: Fix for Django 1.3 compatiblity.
|
* Bugfix: Fix for Django 1.3 compatibility.
|
||||||
* Bugfix: Allow overridden `get_object()` to work correctly.
|
* Bugfix: Allow overridden `get_object()` to work correctly.
|
||||||
|
|
||||||
### 2.2.6
|
### 2.2.6
|
||||||
|
|
||||||
**Date**: 4th April 2013
|
**Date**: 4th April 2013
|
||||||
|
|
||||||
* OAuth2 authentication no longer requires unneccessary URL parameters in addition to the token.
|
* OAuth2 authentication no longer requires unnecessary URL parameters in addition to the token.
|
||||||
* URL hyperlinking in browsable API now handles more cases correctly.
|
* URL hyperlinking in browsable API now handles more cases correctly.
|
||||||
* Long HTTP headers in browsable API are broken in multiple lines when possible.
|
* Long HTTP headers in browsable API are broken in multiple lines when possible.
|
||||||
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
|
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
|
||||||
|
@ -197,7 +237,7 @@ The security vulnerabilities only affect APIs which use the `XMLParser` class, b
|
||||||
* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
|
* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
|
||||||
* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
|
* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
|
||||||
|
|
||||||
**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details.
|
**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details.
|
||||||
|
|
||||||
### 2.1.15
|
### 2.1.15
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
What it is, and why you should care.
|
What it is, and why you should care.
|
||||||
|
|
||||||
> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result.
|
> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result.
|
||||||
>
|
>
|
||||||
> — [Roy Fielding][cite]
|
> — [Roy Fielding][cite]
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ This article is intended to give you a flavor of what REST framework 2 is, and w
|
||||||
|
|
||||||
Before we get cracking, let's start with the hard sell, with a few bits of feedback from some early adopters…
|
Before we get cracking, let's start with the hard sell, with a few bits of feedback from some early adopters…
|
||||||
|
|
||||||
"Django REST framework 2 is beautiful. Some of the API design is worthy of @kennethreitz." - [Kit La Touche][quote1]
|
"Django REST framework 2 is beautiful. Some of the API design is worthy of @kennethreitz." - [Kit La Touche][quote1]
|
||||||
|
|
||||||
"Since it's pretty much just Django, controlling things like URLs has been a breeze... I think [REST framework 2] has definitely got the right approach here; even simple things like being able to override a function called post to do custom work during rather than having to intimately know what happens during a post make a huge difference to your productivity." - [Ian Strachan][quote2]
|
"Since it's pretty much just Django, controlling things like URLs has been a breeze... I think [REST framework 2] has definitely got the right approach here; even simple things like being able to override a function called post to do custom work during rather than having to intimately know what happens during a post make a huge difference to your productivity." - [Ian Strachan][quote2]
|
||||||
|
|
||||||
"I switched to the 2.0 branch and I don't regret it - fully refactored my code in another ½ day and it's *much* more to my tastes" - [Bruno Desthuilliers][quote3]
|
"I switched to the 2.0 branch and I don't regret it - fully refactored my code in another ½ day and it's *much* more to my tastes" - [Bruno Desthuilliers][quote3]
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ REST framework 2 also allows you to work with both function-based and class-base
|
||||||
|
|
||||||
## API Design
|
## API Design
|
||||||
|
|
||||||
Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independantly of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions.
|
Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independently of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions.
|
||||||
|
|
||||||
## The Browsable API
|
## The Browsable API
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ Browsable Web APIs are easier to work with, visualize and debug, and generally m
|
||||||
|
|
||||||
With REST framework 2, the browsable API gets a snazzy new bootstrap-based theme that looks great and is even nicer to work with.
|
With REST framework 2, the browsable API gets a snazzy new bootstrap-based theme that looks great and is even nicer to work with.
|
||||||
|
|
||||||
There are also some functionality improvments - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions.
|
There are also some functionality improvements - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions.
|
||||||
|
|
||||||
![Browsable API][image]
|
![Browsable API][image]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# REST, Hypermedia & HATEOAS
|
# REST, Hypermedia & HATEOAS
|
||||||
|
|
||||||
> You keep using that word "REST". I do not think it means what you think it means.
|
> You keep using that word "REST". I do not think it means what you think it means.
|
||||||
>
|
>
|
||||||
> — Mike Amundsen, [REST fest 2012 keynote][cite].
|
> — Mike Amundsen, [REST fest 2012 keynote][cite].
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ What REST framework doesn't do is give you is machine readable hypermedia format
|
||||||
[cite]: http://vimeo.com/channels/restfest/page:2
|
[cite]: http://vimeo.com/channels/restfest/page:2
|
||||||
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
||||||
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[restful-web-services]:
|
[restful-web-services]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
|
||||||
[building-hypermedia-apis]: …
|
[building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
|
||||||
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
|
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
|
||||||
[restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
|
[restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
|
||||||
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
|
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together.
|
This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together.
|
||||||
|
|
||||||
The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead.
|
The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ Okay, we're ready to roll.
|
||||||
|
|
||||||
## Creating a model to work with
|
## Creating a model to work with
|
||||||
|
|
||||||
For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
|
For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from pygments.lexers import get_all_lexers
|
from pygments.lexers import get_all_lexers
|
||||||
|
@ -146,6 +146,8 @@ The first thing we need to get started on our Web API is provide a way of serial
|
||||||
|
|
||||||
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||||
|
|
||||||
|
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Testarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||||
|
|
||||||
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
||||||
|
|
||||||
## Working with Serializers
|
## Working with Serializers
|
||||||
|
@ -293,7 +295,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us
|
||||||
snippet.delete()
|
snippet.delete()
|
||||||
return HttpResponse(status=204)
|
return HttpResponse(status=204)
|
||||||
|
|
||||||
Finally we need to wire these views up. Create the `snippets/urls.py` file:
|
Finally we need to wire these views up. Create the `snippets/urls.py` file:
|
||||||
|
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Let's introduce a couple of essential building blocks.
|
||||||
REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
|
REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
|
||||||
|
|
||||||
request.POST # Only handles form data. Only works for 'POST' method.
|
request.POST # Only handles form data. Only works for 'POST' method.
|
||||||
request.DATA # Handles arbitrary data. Works any HTTP request with content.
|
request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
|
||||||
|
|
||||||
## Response objects
|
## Response objects
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ Notice that we're no longer explicitly tying our requests or responses to a give
|
||||||
|
|
||||||
## Adding optional format suffixes to our URLs
|
## Adding optional format suffixes to our URLs
|
||||||
|
|
||||||
To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url].
|
To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url].
|
||||||
|
|
||||||
Start by adding a `format` keyword argument to both of the views, like so.
|
Start by adding a `format` keyword argument to both of the views, like so.
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/sni
|
||||||
|
|
||||||
### Browsability
|
### Browsability
|
||||||
|
|
||||||
Because the API chooses the content type of the response based on the client request, it will, by default, return an HTML-formatted representation of the resource when that resource is requested by a web browser. This allows for the API to return a fully web-browsable HTML representation.
|
Because the API chooses the content type of the response based on the client request, it will, by default, return an HTML-formatted representation of the resource when that resource is requested by a web browser. This allows for the API to return a fully web-browsable HTML representation.
|
||||||
|
|
||||||
Having a web-browsable API is a huge usability win, and makes developing and using your API much easier. It also dramatically lowers the barrier-to-entry for other developers wanting to inspect and work with your API.
|
Having a web-browsable API is a huge usability win, and makes developing and using your API much easier. It also dramatically lowers the barrier-to-entry for other developers wanting to inspect and work with your API.
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return self.create(request, *args, **kwargs)
|
return self.create(request, *args, **kwargs)
|
||||||
|
|
||||||
We'll take a moment to examine exactly what's happening here. We're building our view using `GenericAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
|
We'll take a moment to examine exactly what's happening here. We're building our view using `GenericAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
|
||||||
|
|
||||||
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
|
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ Add the following two fields to the model.
|
||||||
owner = models.ForeignKey('auth.User', related_name='snippets')
|
owner = models.ForeignKey('auth.User', related_name='snippets')
|
||||||
highlighted = models.TextField()
|
highlighted = models.TextField()
|
||||||
|
|
||||||
We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code higlighting library.
|
We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code highlighting library.
|
||||||
|
|
||||||
We'll need some extra imports:
|
We'll need some extra imports:
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin
|
||||||
|
|
||||||
## Updating our serializer
|
## Updating our serializer
|
||||||
|
|
||||||
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
|
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
|
||||||
|
|
||||||
owner = serializers.Field(source='owner.username')
|
owner = serializers.Field(source='owner.username')
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
|
||||||
|
|
||||||
## Adding login to the Browsable API
|
## Adding login to the Browsable API
|
||||||
|
|
||||||
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
||||||
|
|
||||||
We can add a login view for use with the browsable API, by editing our URLconf once more.
|
We can add a login view for use with the browsable API, by editing our URLconf once more.
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ And, at the end of the file, add a pattern to include the login and logout views
|
||||||
|
|
||||||
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
|
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
|
||||||
|
|
||||||
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earier, you'll be able to create code snippets again.
|
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earlier, you'll be able to create code snippets again.
|
||||||
|
|
||||||
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field.
|
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field.
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ Unlike all our other API endpoints, we don't want to use JSON, but instead just
|
||||||
|
|
||||||
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
||||||
|
|
||||||
Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
|
Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
|
||||||
|
|
||||||
from rest_framework import renderers
|
from rest_framework import renderers
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Tutorial 6 - ViewSets & Routers
|
# Tutorial 6: ViewSets & Routers
|
||||||
|
|
||||||
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
|
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ To see what's going on under the hood let's first explicitly create a set of vie
|
||||||
|
|
||||||
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
|
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
|
||||||
|
|
||||||
from snippets.resources import SnippetResource, UserResource
|
from snippets.views import SnippetViewSet, UserViewSet
|
||||||
|
|
||||||
snippet_list = SnippetViewSet.as_view({
|
snippet_list = SnippetViewSet.as_view({
|
||||||
'get': 'list',
|
'get': 'list',
|
||||||
|
@ -119,7 +119,7 @@ Registering the viewsets with the router is similar to providing a urlpattern.
|
||||||
|
|
||||||
The `DefaultRouter` class we're using also automatically creates the API root view for us, so we can now delete the `api_root` method from our `views` module.
|
The `DefaultRouter` class we're using also automatically creates the API root view for us, so we can now delete the `api_root` method from our `views` module.
|
||||||
|
|
||||||
## Trade-offs between views vs viewsets.
|
## Trade-offs between views vs viewsets
|
||||||
|
|
||||||
Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
|
Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,43 @@
|
||||||
|
|
||||||
We're going to create a simple API to allow admin users to view and edit the users and groups in the system.
|
We're going to create a simple API to allow admin users to view and edit the users and groups in the system.
|
||||||
|
|
||||||
Create a new Django project, and start a new app called `quickstart`. Once you've set up a database and got everything synced and ready to go open up the app's directory and we'll get coding...
|
## Project setup
|
||||||
|
|
||||||
|
Create a new Django project named `tutorial`, then start a new app called `quickstart`.
|
||||||
|
|
||||||
|
# Set up a new project
|
||||||
|
django-admin.py startproject tutorial
|
||||||
|
cd tutorial
|
||||||
|
|
||||||
|
# Create a virtualenv to isolate our package dependencies locally
|
||||||
|
virtualenv env
|
||||||
|
source env/bin/activate
|
||||||
|
|
||||||
|
# Install Django and Django REST framework into the virtualenv
|
||||||
|
pip install django
|
||||||
|
pip install djangorestframework
|
||||||
|
|
||||||
|
# Create a new app
|
||||||
|
python manage.py startapp quickstart
|
||||||
|
|
||||||
|
Next you'll need to get a database set up and synced. If you just want to use SQLite for now, then you'll want to edit your `tutorial/settings.py` module to include something like this:
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': 'database.sql',
|
||||||
|
'USER': '',
|
||||||
|
'PASSWORD': '',
|
||||||
|
'HOST': '',
|
||||||
|
'PORT': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The run `syncdb` like so:
|
||||||
|
|
||||||
|
python manage.py syncdb
|
||||||
|
|
||||||
|
Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
|
||||||
|
|
||||||
## Serializers
|
## Serializers
|
||||||
|
|
||||||
|
@ -55,7 +91,7 @@ We can easily break these down into individual views if we need to, but using vi
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
Okay, now let's wire up the API URLs. On to `quickstart/urls.py`...
|
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||||
|
|
||||||
from django.conf.urls import patterns, url, include
|
from django.conf.urls import patterns, url, include
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
@ -80,7 +116,7 @@ Finally, we're including default login and logout views for use with the browsab
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
We'd also like to set a few global settings. We'd like to turn on pagination, and we want our API to only be accessible to admin users.
|
We'd also like to set a few global settings. We'd like to turn on pagination, and we want our API to only be accessible to admin users. The settings module will be in `tutorial/settings.py`
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
@ -98,6 +134,10 @@ Okay, we're done.
|
||||||
|
|
||||||
## Testing our API
|
## Testing our API
|
||||||
|
|
||||||
|
We're now ready to test the API we've built. Let's fire up the server from the command line.
|
||||||
|
|
||||||
|
python ./manage.py runserver
|
||||||
|
|
||||||
We can now access our API, both from the command-line, using tools like `curl`...
|
We can now access our API, both from the command-line, using tools like `curl`...
|
||||||
|
|
||||||
bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
|
bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
|
||||||
|
|
|
@ -4,4 +4,4 @@ defusedxml>=0.3
|
||||||
django-filter>=0.5.4
|
django-filter>=0.5.4
|
||||||
django-oauth-plus>=2.0
|
django-oauth-plus>=2.0
|
||||||
oauth2>=1.5.211
|
oauth2>=1.5.211
|
||||||
django-oauth2-provider>=0.2.3
|
django-oauth2-provider>=0.2.4
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '2.3.2'
|
__version__ = '2.3.5'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
||||||
|
|
|
@ -495,3 +495,16 @@ except ImportError:
|
||||||
oauth2_provider_forms = None
|
oauth2_provider_forms = None
|
||||||
oauth2_provider_scope = None
|
oauth2_provider_scope = None
|
||||||
oauth2_constants = None
|
oauth2_constants = None
|
||||||
|
|
||||||
|
# Handle lazy strings
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
def is_non_str_iterable(obj):
|
||||||
|
if (isinstance(obj, str) or
|
||||||
|
(isinstance(obj, Promise) and obj._delegate_text)):
|
||||||
|
return False
|
||||||
|
return hasattr(obj, '__iter__')
|
||||||
|
else:
|
||||||
|
def is_non_str_iterable(obj):
|
||||||
|
return hasattr(obj, '__iter__')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
The most imporant decorator in this module is `@api_view`, which is used
|
The most important decorator in this module is `@api_view`, which is used
|
||||||
for writing function-based views with REST framework.
|
for writing function-based views with REST framework.
|
||||||
|
|
||||||
There are also various decorators for setting the API policies on function
|
There are also various decorators for setting the API policies on function
|
||||||
|
@ -40,7 +40,7 @@ def api_view(http_method_names):
|
||||||
|
|
||||||
# api_view applied with eg. string instead of list of strings
|
# api_view applied with eg. string instead of list of strings
|
||||||
assert isinstance(http_method_names, (list, tuple)), \
|
assert isinstance(http_method_names, (list, tuple)), \
|
||||||
'@api_view expected a list of strings, recieved %s' % type(http_method_names).__name__
|
'@api_view expected a list of strings, received %s' % type(http_method_names).__name__
|
||||||
|
|
||||||
allowed_methods = set(http_method_names) | set(('options',))
|
allowed_methods = set(http_method_names) | set(('options',))
|
||||||
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
|
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
|
||||||
|
@ -112,18 +112,18 @@ def link(**kwargs):
|
||||||
Used to mark a method on a ViewSet that should be routed for GET requests.
|
Used to mark a method on a ViewSet that should be routed for GET requests.
|
||||||
"""
|
"""
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.bind_to_method = 'get'
|
func.bind_to_methods = ['get']
|
||||||
func.kwargs = kwargs
|
func.kwargs = kwargs
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def action(**kwargs):
|
def action(methods=['post'], **kwargs):
|
||||||
"""
|
"""
|
||||||
Used to mark a method on a ViewSet that should be routed for POST requests.
|
Used to mark a method on a ViewSet that should be routed for POST requests.
|
||||||
"""
|
"""
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.bind_to_method = 'post'
|
func.bind_to_methods = methods
|
||||||
func.kwargs = kwargs
|
func.kwargs = kwargs
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
@ -11,20 +11,21 @@ from decimal import Decimal, DecimalException
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.encoding import is_protected_type
|
from django.utils.encoding import is_protected_type
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
|
from rest_framework.compat import (timezone, parse_date, parse_datetime,
|
||||||
|
parse_time)
|
||||||
from rest_framework.compat import BytesIO
|
from rest_framework.compat import BytesIO
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
from rest_framework.compat import smart_text
|
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ def get_component(obj, attr_name):
|
||||||
return that attribute on the object.
|
return that attribute on the object.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
val = obj[attr_name]
|
val = obj.get(attr_name)
|
||||||
else:
|
else:
|
||||||
val = getattr(obj, attr_name)
|
val = getattr(obj, attr_name)
|
||||||
|
|
||||||
|
@ -60,7 +61,8 @@ def get_component(obj, attr_name):
|
||||||
|
|
||||||
|
|
||||||
def readable_datetime_formats(formats):
|
def readable_datetime_formats(formats):
|
||||||
format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
|
format = ', '.join(formats).replace(ISO_8601,
|
||||||
|
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
|
||||||
return humanize_strptime(format)
|
return humanize_strptime(format)
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,8 +109,9 @@ class Field(object):
|
||||||
partial = False
|
partial = False
|
||||||
use_files = False
|
use_files = False
|
||||||
form_field_class = forms.CharField
|
form_field_class = forms.CharField
|
||||||
|
type_label = 'field'
|
||||||
|
|
||||||
def __init__(self, source=None):
|
def __init__(self, source=None, label=None, help_text=None):
|
||||||
self.parent = None
|
self.parent = None
|
||||||
|
|
||||||
self.creation_counter = Field.creation_counter
|
self.creation_counter = Field.creation_counter
|
||||||
|
@ -116,6 +119,12 @@ class Field(object):
|
||||||
|
|
||||||
self.source = source
|
self.source = source
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
self.label = smart_text(label)
|
||||||
|
|
||||||
|
if help_text is not None:
|
||||||
|
self.help_text = smart_text(help_text)
|
||||||
|
|
||||||
def initialize(self, parent, field_name):
|
def initialize(self, parent, field_name):
|
||||||
"""
|
"""
|
||||||
Called to set up a field prior to field_to_native or field_from_native.
|
Called to set up a field prior to field_to_native or field_from_native.
|
||||||
|
@ -167,11 +176,16 @@ class Field(object):
|
||||||
|
|
||||||
if is_protected_type(value):
|
if is_protected_type(value):
|
||||||
return value
|
return value
|
||||||
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
|
elif (is_non_str_iterable(value) and
|
||||||
|
not isinstance(value, (dict, six.string_types))):
|
||||||
return [self.to_native(item) for item in value]
|
return [self.to_native(item) for item in value]
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
return dict(map(self.to_native, (k, v)) for k, v in value.items())
|
# Make sure we preserve field ordering, if it exists
|
||||||
return smart_text(value)
|
ret = SortedDict()
|
||||||
|
for key, val in value.items():
|
||||||
|
ret[key] = self.to_native(val)
|
||||||
|
return ret
|
||||||
|
return force_text(value)
|
||||||
|
|
||||||
def attributes(self):
|
def attributes(self):
|
||||||
"""
|
"""
|
||||||
|
@ -181,6 +195,18 @@ class Field(object):
|
||||||
return {'type': self.type_name}
|
return {'type': self.type_name}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def metadata(self):
|
||||||
|
metadata = SortedDict()
|
||||||
|
metadata['type'] = self.type_label
|
||||||
|
metadata['required'] = getattr(self, 'required', False)
|
||||||
|
optional_attrs = ['read_only', 'label', 'help_text',
|
||||||
|
'min_length', 'max_length']
|
||||||
|
for attr in optional_attrs:
|
||||||
|
value = getattr(self, attr, None)
|
||||||
|
if value is not None and value != '':
|
||||||
|
metadata[attr] = force_text(value, strings_only=True)
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
class WritableField(Field):
|
class WritableField(Field):
|
||||||
"""
|
"""
|
||||||
|
@ -194,7 +220,8 @@ class WritableField(Field):
|
||||||
widget = widgets.TextInput
|
widget = widgets.TextInput
|
||||||
default = None
|
default = None
|
||||||
|
|
||||||
def __init__(self, source=None, read_only=False, required=None,
|
def __init__(self, source=None, label=None, help_text=None,
|
||||||
|
read_only=False, required=None,
|
||||||
validators=[], error_messages=None, widget=None,
|
validators=[], error_messages=None, widget=None,
|
||||||
default=None, blank=None):
|
default=None, blank=None):
|
||||||
|
|
||||||
|
@ -205,7 +232,7 @@ class WritableField(Field):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
required = not(blank)
|
required = not(blank)
|
||||||
|
|
||||||
super(WritableField, self).__init__(source=source)
|
super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
|
||||||
|
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
if required is None:
|
if required is None:
|
||||||
|
@ -268,7 +295,10 @@ class WritableField(Field):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.default is not None and not self.partial:
|
if self.default is not None and not self.partial:
|
||||||
# Note: partial updates shouldn't set defaults
|
# Note: partial updates shouldn't set defaults
|
||||||
native = self.default
|
if is_simple_callable(self.default):
|
||||||
|
native = self.default()
|
||||||
|
else:
|
||||||
|
native = self.default
|
||||||
else:
|
else:
|
||||||
if self.required:
|
if self.required:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
|
@ -335,6 +365,7 @@ class ModelField(WritableField):
|
||||||
|
|
||||||
class BooleanField(WritableField):
|
class BooleanField(WritableField):
|
||||||
type_name = 'BooleanField'
|
type_name = 'BooleanField'
|
||||||
|
type_label = 'boolean'
|
||||||
form_field_class = forms.BooleanField
|
form_field_class = forms.BooleanField
|
||||||
widget = widgets.CheckboxInput
|
widget = widgets.CheckboxInput
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -357,6 +388,7 @@ class BooleanField(WritableField):
|
||||||
|
|
||||||
class CharField(WritableField):
|
class CharField(WritableField):
|
||||||
type_name = 'CharField'
|
type_name = 'CharField'
|
||||||
|
type_label = 'string'
|
||||||
form_field_class = forms.CharField
|
form_field_class = forms.CharField
|
||||||
|
|
||||||
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
||||||
|
@ -375,23 +407,38 @@ class CharField(WritableField):
|
||||||
|
|
||||||
class URLField(CharField):
|
class URLField(CharField):
|
||||||
type_name = 'URLField'
|
type_name = 'URLField'
|
||||||
|
type_label = 'url'
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 200)
|
|
||||||
kwargs['validators'] = [validators.URLValidator()]
|
kwargs['validators'] = [validators.URLValidator()]
|
||||||
super(URLField, self).__init__(**kwargs)
|
super(URLField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SlugField(CharField):
|
class SlugField(CharField):
|
||||||
type_name = 'SlugField'
|
type_name = 'SlugField'
|
||||||
|
type_label = 'slug'
|
||||||
|
form_field_class = forms.SlugField
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
|
||||||
|
" underscores or hyphens."),
|
||||||
|
}
|
||||||
|
default_validators = [validators.validate_slug]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 50)
|
|
||||||
super(SlugField, self).__init__(*args, **kwargs)
|
super(SlugField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
result = copy.copy(self)
|
||||||
|
memo[id(self)] = result
|
||||||
|
#result.widget = copy.deepcopy(self.widget, memo)
|
||||||
|
result.validators = self.validators[:]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
|
type_label = 'multiple choice'
|
||||||
form_field_class = forms.ChoiceField
|
form_field_class = forms.ChoiceField
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -402,6 +449,8 @@ class ChoiceField(WritableField):
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), *args, **kwargs):
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
if not self.required:
|
||||||
|
self.choices = BLANK_CHOICE_DASH + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
@ -440,6 +489,7 @@ class ChoiceField(WritableField):
|
||||||
|
|
||||||
class EmailField(CharField):
|
class EmailField(CharField):
|
||||||
type_name = 'EmailField'
|
type_name = 'EmailField'
|
||||||
|
type_label = 'email'
|
||||||
form_field_class = forms.EmailField
|
form_field_class = forms.EmailField
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -463,6 +513,7 @@ class EmailField(CharField):
|
||||||
|
|
||||||
class RegexField(CharField):
|
class RegexField(CharField):
|
||||||
type_name = 'RegexField'
|
type_name = 'RegexField'
|
||||||
|
type_label = 'regex'
|
||||||
form_field_class = forms.RegexField
|
form_field_class = forms.RegexField
|
||||||
|
|
||||||
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
||||||
|
@ -492,6 +543,7 @@ class RegexField(CharField):
|
||||||
|
|
||||||
class DateField(WritableField):
|
class DateField(WritableField):
|
||||||
type_name = 'DateField'
|
type_name = 'DateField'
|
||||||
|
type_label = 'date'
|
||||||
widget = widgets.DateInput
|
widget = widgets.DateInput
|
||||||
form_field_class = forms.DateField
|
form_field_class = forms.DateField
|
||||||
|
|
||||||
|
@ -555,6 +607,7 @@ class DateField(WritableField):
|
||||||
|
|
||||||
class DateTimeField(WritableField):
|
class DateTimeField(WritableField):
|
||||||
type_name = 'DateTimeField'
|
type_name = 'DateTimeField'
|
||||||
|
type_label = 'datetime'
|
||||||
widget = widgets.DateTimeInput
|
widget = widgets.DateTimeInput
|
||||||
form_field_class = forms.DateTimeField
|
form_field_class = forms.DateTimeField
|
||||||
|
|
||||||
|
@ -624,6 +677,7 @@ class DateTimeField(WritableField):
|
||||||
|
|
||||||
class TimeField(WritableField):
|
class TimeField(WritableField):
|
||||||
type_name = 'TimeField'
|
type_name = 'TimeField'
|
||||||
|
type_label = 'time'
|
||||||
widget = widgets.TimeInput
|
widget = widgets.TimeInput
|
||||||
form_field_class = forms.TimeField
|
form_field_class = forms.TimeField
|
||||||
|
|
||||||
|
@ -680,6 +734,7 @@ class TimeField(WritableField):
|
||||||
|
|
||||||
class IntegerField(WritableField):
|
class IntegerField(WritableField):
|
||||||
type_name = 'IntegerField'
|
type_name = 'IntegerField'
|
||||||
|
type_label = 'integer'
|
||||||
form_field_class = forms.IntegerField
|
form_field_class = forms.IntegerField
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -710,6 +765,7 @@ class IntegerField(WritableField):
|
||||||
|
|
||||||
class FloatField(WritableField):
|
class FloatField(WritableField):
|
||||||
type_name = 'FloatField'
|
type_name = 'FloatField'
|
||||||
|
type_label = 'float'
|
||||||
form_field_class = forms.FloatField
|
form_field_class = forms.FloatField
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -729,6 +785,7 @@ class FloatField(WritableField):
|
||||||
|
|
||||||
class DecimalField(WritableField):
|
class DecimalField(WritableField):
|
||||||
type_name = 'DecimalField'
|
type_name = 'DecimalField'
|
||||||
|
type_label = 'decimal'
|
||||||
form_field_class = forms.DecimalField
|
form_field_class = forms.DecimalField
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -799,6 +856,7 @@ class DecimalField(WritableField):
|
||||||
class FileField(WritableField):
|
class FileField(WritableField):
|
||||||
use_files = True
|
use_files = True
|
||||||
type_name = 'FileField'
|
type_name = 'FileField'
|
||||||
|
type_label = 'file upload'
|
||||||
form_field_class = forms.FileField
|
form_field_class = forms.FileField
|
||||||
widget = widgets.FileInput
|
widget = widgets.FileInput
|
||||||
|
|
||||||
|
@ -842,6 +900,8 @@ class FileField(WritableField):
|
||||||
|
|
||||||
class ImageField(FileField):
|
class ImageField(FileField):
|
||||||
use_files = True
|
use_files = True
|
||||||
|
type_name = 'ImageField'
|
||||||
|
type_label = 'image upload'
|
||||||
form_field_class = forms.ImageField
|
form_field_class = forms.ImageField
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
|
|
@ -3,9 +3,9 @@ Provides generic filtering backends that can be used to filter the results
|
||||||
returned by list views.
|
returned by list views.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from rest_framework.compat import django_filters
|
from rest_framework.compat import django_filters, six
|
||||||
|
from functools import reduce
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
FilterSet = django_filters and django_filters.FilterSet or None
|
FilterSet = django_filters and django_filters.FilterSet or None
|
||||||
|
@ -32,40 +32,33 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
|
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
|
||||||
|
|
||||||
def get_filter_class(self, view):
|
def get_filter_class(self, view, queryset=None):
|
||||||
"""
|
"""
|
||||||
Return the django-filters `FilterSet` used to filter the queryset.
|
Return the django-filters `FilterSet` used to filter the queryset.
|
||||||
"""
|
"""
|
||||||
filter_class = getattr(view, 'filter_class', None)
|
filter_class = getattr(view, 'filter_class', None)
|
||||||
filter_fields = getattr(view, 'filter_fields', None)
|
filter_fields = getattr(view, 'filter_fields', None)
|
||||||
model_cls = getattr(view, 'model', None)
|
|
||||||
queryset = getattr(view, 'queryset', None)
|
|
||||||
if model_cls is None and queryset is not None:
|
|
||||||
model_cls = queryset.model
|
|
||||||
|
|
||||||
if filter_class:
|
if filter_class:
|
||||||
filter_model = filter_class.Meta.model
|
filter_model = filter_class.Meta.model
|
||||||
|
|
||||||
assert issubclass(filter_model, model_cls), \
|
assert issubclass(filter_model, queryset.model), \
|
||||||
'FilterSet model %s does not match view model %s' % \
|
'FilterSet model %s does not match queryset model %s' % \
|
||||||
(filter_model, model_cls)
|
(filter_model, queryset.model)
|
||||||
|
|
||||||
return filter_class
|
return filter_class
|
||||||
|
|
||||||
if filter_fields:
|
if filter_fields:
|
||||||
assert model_cls is not None, 'Cannot use DjangoFilterBackend ' \
|
|
||||||
'on a view which does not have a .model or .queryset attribute.'
|
|
||||||
|
|
||||||
class AutoFilterSet(self.default_filter_set):
|
class AutoFilterSet(self.default_filter_set):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = model_cls
|
model = queryset.model
|
||||||
fields = filter_fields
|
fields = filter_fields
|
||||||
return AutoFilterSet
|
return AutoFilterSet
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
filter_class = self.get_filter_class(view)
|
filter_class = self.get_filter_class(view, queryset)
|
||||||
|
|
||||||
if filter_class:
|
if filter_class:
|
||||||
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
|
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
|
||||||
|
@ -74,6 +67,16 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
|
|
||||||
|
|
||||||
class SearchFilter(BaseFilterBackend):
|
class SearchFilter(BaseFilterBackend):
|
||||||
|
search_param = 'search' # The URL query parameter used for the search.
|
||||||
|
|
||||||
|
def get_search_terms(self, request):
|
||||||
|
"""
|
||||||
|
Search terms are set by a ?search=... query parameter,
|
||||||
|
and may be comma and/or whitespace delimited.
|
||||||
|
"""
|
||||||
|
params = request.QUERY_PARAMS.get(self.search_param, '')
|
||||||
|
return params.replace(',', ' ').split()
|
||||||
|
|
||||||
def construct_search(self, field_name):
|
def construct_search(self, field_name):
|
||||||
if field_name.startswith('^'):
|
if field_name.startswith('^'):
|
||||||
return "%s__istartswith" % field_name[1:]
|
return "%s__istartswith" % field_name[1:]
|
||||||
|
@ -88,12 +91,53 @@ class SearchFilter(BaseFilterBackend):
|
||||||
search_fields = getattr(view, 'search_fields', None)
|
search_fields = getattr(view, 'search_fields', None)
|
||||||
|
|
||||||
if not search_fields:
|
if not search_fields:
|
||||||
return None
|
return queryset
|
||||||
|
|
||||||
orm_lookups = [self.construct_search(str(search_field))
|
orm_lookups = [self.construct_search(str(search_field))
|
||||||
for search_field in self.search_fields]
|
for search_field in search_fields]
|
||||||
for bit in self.query.split():
|
|
||||||
or_queries = [models.Q(**{orm_lookup: bit})
|
for search_term in self.get_search_terms(request):
|
||||||
|
or_queries = [models.Q(**{orm_lookup: search_term})
|
||||||
for orm_lookup in orm_lookups]
|
for orm_lookup in orm_lookups]
|
||||||
queryset = queryset.filter(reduce(operator.or_, or_queries))
|
queryset = queryset.filter(reduce(operator.or_, or_queries))
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingFilter(BaseFilterBackend):
|
||||||
|
ordering_param = 'ordering' # The URL query parameter used for the ordering.
|
||||||
|
|
||||||
|
def get_ordering(self, request):
|
||||||
|
"""
|
||||||
|
Search terms are set by a ?search=... query parameter,
|
||||||
|
and may be comma and/or whitespace delimited.
|
||||||
|
"""
|
||||||
|
params = request.QUERY_PARAMS.get(self.ordering_param)
|
||||||
|
if params:
|
||||||
|
return [param.strip() for param in params.split(',')]
|
||||||
|
|
||||||
|
def get_default_ordering(self, view):
|
||||||
|
ordering = getattr(view, 'ordering', None)
|
||||||
|
if isinstance(ordering, six.string_types):
|
||||||
|
return (ordering,)
|
||||||
|
return ordering
|
||||||
|
|
||||||
|
def remove_invalid_fields(self, queryset, ordering):
|
||||||
|
field_names = [field.name for field in queryset.model._meta.fields]
|
||||||
|
return [term for term in ordering if term.lstrip('-') in field_names]
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
ordering = self.get_ordering(request)
|
||||||
|
|
||||||
|
if ordering:
|
||||||
|
# Skip any incorrect parameters
|
||||||
|
ordering = self.remove_invalid_fields(queryset, ordering)
|
||||||
|
|
||||||
|
if not ordering:
|
||||||
|
# Use 'ordering' attribtue by default
|
||||||
|
ordering = self.get_default_ordering(view)
|
||||||
|
|
||||||
|
if ordering:
|
||||||
|
return queryset.order_by(*ordering)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -3,17 +3,28 @@ Generic views that provide commonly needed behaviour.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
from django.core.paginator import Paginator, InvalidPage
|
from django.core.paginator import Paginator, InvalidPage
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework import views, mixins
|
from rest_framework import views, mixins, exceptions
|
||||||
from rest_framework.exceptions import ConfigurationError
|
from rest_framework.request import clone_request
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_or_404(queryset, **filter_kwargs):
|
||||||
|
"""
|
||||||
|
Same as Django's standard shortcut, but make sure to raise 404
|
||||||
|
if the filter_kwargs don't match the required types.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _get_object_or_404(queryset, **filter_kwargs)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
class GenericAPIView(views.APIView):
|
class GenericAPIView(views.APIView):
|
||||||
"""
|
"""
|
||||||
Base class for all other generic views.
|
Base class for all other generic views.
|
||||||
|
@ -274,7 +285,7 @@ class GenericAPIView(views.APIView):
|
||||||
)
|
)
|
||||||
filter_kwargs = {self.slug_field: slug}
|
filter_kwargs = {self.slug_field: slug}
|
||||||
else:
|
else:
|
||||||
raise ConfigurationError(
|
raise exceptions.ConfigurationError(
|
||||||
'Expected view %s to be called with a URL keyword argument '
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
'attribute on the view correctly.' %
|
'attribute on the view correctly.' %
|
||||||
|
@ -310,6 +321,41 @@ class GenericAPIView(views.APIView):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def metadata(self, request):
|
||||||
|
"""
|
||||||
|
Return a dictionary of metadata about the view.
|
||||||
|
Used to return responses for OPTIONS requests.
|
||||||
|
|
||||||
|
We override the default behavior, and add some extra information
|
||||||
|
about the required request body for POST and PUT operations.
|
||||||
|
"""
|
||||||
|
ret = super(GenericAPIView, self).metadata(request)
|
||||||
|
|
||||||
|
actions = {}
|
||||||
|
for method in ('PUT', 'POST'):
|
||||||
|
if method not in self.allowed_methods:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cloned_request = clone_request(request, method)
|
||||||
|
try:
|
||||||
|
# Test global permissions
|
||||||
|
self.check_permissions(cloned_request)
|
||||||
|
# Test object permissions
|
||||||
|
if method == 'PUT':
|
||||||
|
self.get_object()
|
||||||
|
except (exceptions.APIException, PermissionDenied, Http404):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# If user has appropriate permissions for the view, include
|
||||||
|
# appropriate metadata about the fields that should be supplied.
|
||||||
|
serializer = self.get_serializer()
|
||||||
|
actions[method] = serializer.metadata()
|
||||||
|
|
||||||
|
if actions:
|
||||||
|
ret['actions'] = actions
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
### Concrete view classes that provide method handlers ###
|
### Concrete view classes that provide method handlers ###
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.http import Http404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
||||||
|
@ -42,7 +43,6 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)
|
||||||
class CreateModelMixin(object):
|
class CreateModelMixin(object):
|
||||||
"""
|
"""
|
||||||
Create a model instance.
|
Create a model instance.
|
||||||
Should be mixed in with any `GenericAPIView`.
|
|
||||||
"""
|
"""
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||||
|
@ -67,7 +67,6 @@ class CreateModelMixin(object):
|
||||||
class ListModelMixin(object):
|
class ListModelMixin(object):
|
||||||
"""
|
"""
|
||||||
List a queryset.
|
List a queryset.
|
||||||
Should be mixed in with `MultipleObjectAPIView`.
|
|
||||||
"""
|
"""
|
||||||
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
|
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
|
||||||
|
|
||||||
|
@ -77,6 +76,12 @@ class ListModelMixin(object):
|
||||||
# Default is to allow empty querysets. This can be altered by setting
|
# Default is to allow empty querysets. This can be altered by setting
|
||||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||||
if not self.allow_empty and not self.object_list:
|
if not self.allow_empty and not self.object_list:
|
||||||
|
warnings.warn(
|
||||||
|
'The `allow_empty` parameter is due to be deprecated. '
|
||||||
|
'To use `allow_empty=False` style behavior, You should override '
|
||||||
|
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
|
||||||
|
PendingDeprecationWarning
|
||||||
|
)
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
error_msg = self.empty_error % {'class_name': class_name}
|
error_msg = self.empty_error % {'class_name': class_name}
|
||||||
raise Http404(error_msg)
|
raise Http404(error_msg)
|
||||||
|
@ -94,7 +99,6 @@ class ListModelMixin(object):
|
||||||
class RetrieveModelMixin(object):
|
class RetrieveModelMixin(object):
|
||||||
"""
|
"""
|
||||||
Retrieve a model instance.
|
Retrieve a model instance.
|
||||||
Should be mixed in with `SingleObjectAPIView`.
|
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
@ -105,17 +109,12 @@ class RetrieveModelMixin(object):
|
||||||
class UpdateModelMixin(object):
|
class UpdateModelMixin(object):
|
||||||
"""
|
"""
|
||||||
Update a model instance.
|
Update a model instance.
|
||||||
Should be mixed in with `SingleObjectAPIView`.
|
|
||||||
"""
|
"""
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
partial = kwargs.pop('partial', False)
|
partial = kwargs.pop('partial', False)
|
||||||
self.object = None
|
self.object = self.get_object_or_none()
|
||||||
try:
|
|
||||||
self.object = self.get_object()
|
if self.object is None:
|
||||||
except Http404:
|
|
||||||
# If this is a PUT-as-create operation, we need to ensure that
|
|
||||||
# we have relevant permissions, as if this was a POST request.
|
|
||||||
self.check_permissions(clone_request(request, 'POST'))
|
|
||||||
created = True
|
created = True
|
||||||
save_kwargs = {'force_insert': True}
|
save_kwargs = {'force_insert': True}
|
||||||
success_status_code = status.HTTP_201_CREATED
|
success_status_code = status.HTTP_201_CREATED
|
||||||
|
@ -139,6 +138,16 @@ class UpdateModelMixin(object):
|
||||||
kwargs['partial'] = True
|
kwargs['partial'] = True
|
||||||
return self.update(request, *args, **kwargs)
|
return self.update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_object_or_none(self):
|
||||||
|
try:
|
||||||
|
return self.get_object()
|
||||||
|
except Http404:
|
||||||
|
# If this is a PUT-as-create operation, we need to ensure that
|
||||||
|
# we have relevant permissions, as if this was a POST request.
|
||||||
|
# This will either raise a PermissionDenied exception,
|
||||||
|
# or simply return None
|
||||||
|
self.check_permissions(clone_request(self.request, 'POST'))
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
"""
|
"""
|
||||||
Set any attributes on the object that are implicit in the request.
|
Set any attributes on the object that are implicit in the request.
|
||||||
|
@ -168,7 +177,6 @@ class UpdateModelMixin(object):
|
||||||
class DestroyModelMixin(object):
|
class DestroyModelMixin(object):
|
||||||
"""
|
"""
|
||||||
Destroy a model instance.
|
Destroy a model instance.
|
||||||
Should be mixed in with `SingleObjectAPIView`.
|
|
||||||
"""
|
"""
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
|
|
@ -126,6 +126,11 @@ class DjangoModelPermissions(BasePermission):
|
||||||
if model_cls is None and queryset is not None:
|
if model_cls is None and queryset is not None:
|
||||||
model_cls = queryset.model
|
model_cls = queryset.model
|
||||||
|
|
||||||
|
# Workaround to ensure DjangoModelPermissions are not applied
|
||||||
|
# to the root view when using DefaultRouter.
|
||||||
|
if model_cls is None and getattr(view, '_ignore_model_permissions'):
|
||||||
|
return True
|
||||||
|
|
||||||
assert model_cls, ('Cannot apply DjangoModelPermissions on a view that'
|
assert model_cls, ('Cannot apply DjangoModelPermissions on a view that'
|
||||||
' does not have `.model` or `.queryset` property.')
|
' does not have `.model` or `.queryset` property.')
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,11 @@ from __future__ import unicode_literals
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.forms.models import ModelChoiceIterator
|
from django.forms.models import ModelChoiceIterator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.fields import Field, WritableField, get_component
|
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.compat import urlparse
|
from rest_framework.compat import urlparse
|
||||||
from rest_framework.compat import smart_text
|
from rest_framework.compat import smart_text
|
||||||
|
@ -47,7 +48,7 @@ class RelatedField(WritableField):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs['required'] = not kwargs.pop('null')
|
kwargs['required'] = not kwargs.pop('null')
|
||||||
|
|
||||||
self.queryset = kwargs.pop('queryset', None)
|
queryset = kwargs.pop('queryset', None)
|
||||||
self.many = kwargs.pop('many', self.many)
|
self.many = kwargs.pop('many', self.many)
|
||||||
if self.many:
|
if self.many:
|
||||||
self.widget = self.many_widget
|
self.widget = self.many_widget
|
||||||
|
@ -56,6 +57,11 @@ class RelatedField(WritableField):
|
||||||
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
||||||
super(RelatedField, self).__init__(*args, **kwargs)
|
super(RelatedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not self.required:
|
||||||
|
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||||
|
|
||||||
|
self.queryset = queryset
|
||||||
|
|
||||||
def initialize(self, parent, field_name):
|
def initialize(self, parent, field_name):
|
||||||
super(RelatedField, self).initialize(parent, field_name)
|
super(RelatedField, self).initialize(parent, field_name)
|
||||||
if self.queryset is None and not self.read_only:
|
if self.queryset is None and not self.read_only:
|
||||||
|
@ -66,7 +72,6 @@ class RelatedField(WritableField):
|
||||||
else: # Reverse
|
else: # Reverse
|
||||||
self.queryset = manager.field.rel.to._default_manager.all()
|
self.queryset = manager.field.rel.to._default_manager.all()
|
||||||
except Exception:
|
except Exception:
|
||||||
raise
|
|
||||||
msg = ('Serializer related fields must include a `queryset`' +
|
msg = ('Serializer related fields must include a `queryset`' +
|
||||||
' argument or set `read_only=True')
|
' argument or set `read_only=True')
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
@ -139,7 +144,12 @@ class RelatedField(WritableField):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.many:
|
if self.many:
|
||||||
return [self.to_native(item) for item in value.all()]
|
if is_simple_callable(getattr(value, 'all', None)):
|
||||||
|
return [self.to_native(item) for item in value.all()]
|
||||||
|
else:
|
||||||
|
# Also support non-queryset iterables.
|
||||||
|
# This allows us to also support plain lists of related items.
|
||||||
|
return [self.to_native(item) for item in value]
|
||||||
return self.to_native(value)
|
return self.to_native(value)
|
||||||
|
|
||||||
def field_from_native(self, data, files, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
|
@ -221,15 +231,28 @@ class PrimaryKeyRelatedField(RelatedField):
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
if self.many:
|
if self.many:
|
||||||
# To-many relationship
|
# To-many relationship
|
||||||
try:
|
|
||||||
|
queryset = None
|
||||||
|
if not self.source:
|
||||||
# Prefer obj.serializable_value for performance reasons
|
# Prefer obj.serializable_value for performance reasons
|
||||||
queryset = obj.serializable_value(self.source or field_name)
|
try:
|
||||||
except AttributeError:
|
queryset = obj.serializable_value(field_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if queryset is None:
|
||||||
# RelatedManager (reverse relationship)
|
# RelatedManager (reverse relationship)
|
||||||
queryset = getattr(obj, self.source or field_name)
|
source = self.source or field_name
|
||||||
|
queryset = obj
|
||||||
|
for component in source.split('.'):
|
||||||
|
queryset = get_component(queryset, component)
|
||||||
|
|
||||||
# Forward relationship
|
# Forward relationship
|
||||||
return [self.to_native(item.pk) for item in queryset.all()]
|
if is_simple_callable(getattr(queryset, 'all', None)):
|
||||||
|
return [self.to_native(item.pk) for item in queryset.all()]
|
||||||
|
else:
|
||||||
|
# Also support non-queryset iterables.
|
||||||
|
# This allows us to also support plain lists of related items.
|
||||||
|
return [self.to_native(item.pk) for item in queryset]
|
||||||
|
|
||||||
# To-one relationship
|
# To-one relationship
|
||||||
try:
|
try:
|
||||||
|
@ -434,7 +457,7 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
raise Exception('Writable related fields must include a `queryset` argument')
|
raise Exception('Writable related fields must include a `queryset` argument')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_prefix = value.startswith('http:') or value.startswith('https:')
|
http_prefix = value.startswith(('http:', 'https:'))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = self.error_messages['incorrect_type']
|
msg = self.error_messages['incorrect_type']
|
||||||
raise ValidationError(msg % type(value).__name__)
|
raise ValidationError(msg % type(value).__name__)
|
||||||
|
@ -465,17 +488,35 @@ class HyperlinkedIdentityField(Field):
|
||||||
"""
|
"""
|
||||||
Represents the instance, or a property on the instance, using hyperlinking.
|
Represents the instance, or a property on the instance, using hyperlinking.
|
||||||
"""
|
"""
|
||||||
|
lookup_field = 'pk'
|
||||||
|
read_only = True
|
||||||
|
|
||||||
|
# These are all pending deprecation
|
||||||
pk_url_kwarg = 'pk'
|
pk_url_kwarg = 'pk'
|
||||||
slug_field = 'slug'
|
slug_field = 'slug'
|
||||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||||
read_only = True
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# TODO: Make view_name mandatory, and have the
|
try:
|
||||||
# HyperlinkedModelSerializer set it on-the-fly
|
self.view_name = kwargs.pop('view_name')
|
||||||
self.view_name = kwargs.pop('view_name', None)
|
except KeyError:
|
||||||
# Optionally the format of the target hyperlink may be specified
|
msg = "HyperlinkedIdentityField requires 'view_name' argument"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
self.format = kwargs.pop('format', None)
|
self.format = kwargs.pop('format', None)
|
||||||
|
lookup_field = kwargs.pop('lookup_field', None)
|
||||||
|
self.lookup_field = lookup_field or self.lookup_field
|
||||||
|
|
||||||
|
# These are pending deprecation
|
||||||
|
if 'pk_url_kwarg' in kwargs:
|
||||||
|
msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if 'slug_url_kwarg' in kwargs:
|
||||||
|
msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if 'slug_field' in kwargs:
|
||||||
|
msg = 'slug_field is pending deprecation. Use lookup_field instead.'
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||||
|
@ -487,8 +528,7 @@ class HyperlinkedIdentityField(Field):
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
request = self.context.get('request', None)
|
request = self.context.get('request', None)
|
||||||
format = self.context.get('format', None)
|
format = self.context.get('format', None)
|
||||||
view_name = self.view_name or self.parent.opts.view_name
|
view_name = self.view_name
|
||||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
warnings.warn("Using `HyperlinkedIdentityField` without including the "
|
warnings.warn("Using `HyperlinkedIdentityField` without including the "
|
||||||
|
@ -508,29 +548,51 @@ class HyperlinkedIdentityField(Field):
|
||||||
if format and self.format and self.format != format:
|
if format and self.format and self.format != format:
|
||||||
format = self.format
|
format = self.format
|
||||||
|
|
||||||
|
# Return the hyperlink, or error if incorrectly configured.
|
||||||
|
try:
|
||||||
|
return self.get_url(obj, view_name, request, format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
msg = (
|
||||||
|
'Could not resolve URL for hyperlinked relationship using '
|
||||||
|
'view name "%s". You may have failed to include the related '
|
||||||
|
'model in your API, or incorrectly configured the '
|
||||||
|
'`lookup_field` attribute on this field.'
|
||||||
|
)
|
||||||
|
raise Exception(msg % view_name)
|
||||||
|
|
||||||
|
def get_url(self, obj, view_name, request, format):
|
||||||
|
"""
|
||||||
|
Given an object, return the URL that hyperlinks to the object.
|
||||||
|
|
||||||
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||||
|
attributes are not configured to correctly match the URL conf.
|
||||||
|
"""
|
||||||
|
lookup_field = getattr(obj, self.lookup_field)
|
||||||
|
kwargs = {self.lookup_field: lookup_field}
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if self.pk_url_kwarg != 'pk':
|
||||||
|
# Only try pk lookup if it has been explicitly set.
|
||||||
|
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
||||||
|
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
slug = getattr(obj, self.slug_field, None)
|
slug = getattr(obj, self.slug_field, None)
|
||||||
|
if slug:
|
||||||
|
# Only use slug lookup if a slug field exists on the model
|
||||||
|
kwargs = {self.slug_url_kwarg: slug}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
if not slug:
|
raise NoReverseMatch()
|
||||||
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
|
||||||
|
|
||||||
kwargs = {self.slug_url_kwarg: slug}
|
|
||||||
try:
|
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
|
||||||
try:
|
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
|
||||||
|
|
||||||
|
|
||||||
### Old-style many classes for backwards compat
|
### Old-style many classes for backwards compat
|
||||||
|
|
|
@ -9,7 +9,6 @@ REST framework also provides an HTML renderer the renders the browsable API.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import string
|
|
||||||
import json
|
import json
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
|
@ -36,6 +35,7 @@ class BaseRenderer(object):
|
||||||
|
|
||||||
media_type = None
|
media_type = None
|
||||||
format = None
|
format = None
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
raise NotImplemented('Renderer class requires .render() to be implemented')
|
raise NotImplemented('Renderer class requires .render() to be implemented')
|
||||||
|
@ -43,16 +43,21 @@ class BaseRenderer(object):
|
||||||
|
|
||||||
class JSONRenderer(BaseRenderer):
|
class JSONRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
Renderer which serializes to json.
|
Renderer which serializes to JSON.
|
||||||
|
Applies JSON's backslash-u character escaping for non-ascii characters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/json'
|
media_type = 'application/json'
|
||||||
format = 'json'
|
format = 'json'
|
||||||
encoder_class = encoders.JSONEncoder
|
encoder_class = encoders.JSONEncoder
|
||||||
|
ensure_ascii = True
|
||||||
|
charset = 'utf-8'
|
||||||
|
# Note that JSON encodings must be utf-8, utf-16 or utf-32.
|
||||||
|
# See: http://www.ietf.org/rfc/rfc4627.txt
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
Render `obj` into json.
|
Render `data` into JSON.
|
||||||
"""
|
"""
|
||||||
if data is None:
|
if data is None:
|
||||||
return ''
|
return ''
|
||||||
|
@ -72,7 +77,25 @@ class JSONRenderer(BaseRenderer):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
indent = None
|
indent = None
|
||||||
|
|
||||||
return json.dumps(data, cls=self.encoder_class, indent=indent)
|
ret = json.dumps(data, cls=self.encoder_class,
|
||||||
|
indent=indent, ensure_ascii=self.ensure_ascii)
|
||||||
|
|
||||||
|
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||||
|
# but if ensure_ascii=False, the return type is underspecified,
|
||||||
|
# and may (or may not) be unicode.
|
||||||
|
# On python 3.x json.dumps() returns unicode strings.
|
||||||
|
if isinstance(ret, six.text_type):
|
||||||
|
return bytes(ret.encode(self.charset))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeJSONRenderer(JSONRenderer):
|
||||||
|
ensure_ascii = False
|
||||||
|
charset = 'utf-8'
|
||||||
|
"""
|
||||||
|
Renderer which serializes to JSON.
|
||||||
|
Does *not* apply JSON's character escaping for non-ascii characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class JSONPRenderer(JSONRenderer):
|
class JSONPRenderer(JSONRenderer):
|
||||||
|
@ -105,7 +128,7 @@ class JSONPRenderer(JSONRenderer):
|
||||||
callback = self.get_callback(renderer_context)
|
callback = self.get_callback(renderer_context)
|
||||||
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
||||||
renderer_context)
|
renderer_context)
|
||||||
return "%s(%s);" % (callback, json)
|
return callback.encode(self.charset) + b'(' + json + b');'
|
||||||
|
|
||||||
|
|
||||||
class XMLRenderer(BaseRenderer):
|
class XMLRenderer(BaseRenderer):
|
||||||
|
@ -115,6 +138,7 @@ class XMLRenderer(BaseRenderer):
|
||||||
|
|
||||||
media_type = 'application/xml'
|
media_type = 'application/xml'
|
||||||
format = 'xml'
|
format = 'xml'
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -125,7 +149,7 @@ class XMLRenderer(BaseRenderer):
|
||||||
|
|
||||||
stream = StringIO()
|
stream = StringIO()
|
||||||
|
|
||||||
xml = SimplerXMLGenerator(stream, "utf-8")
|
xml = SimplerXMLGenerator(stream, self.charset)
|
||||||
xml.startDocument()
|
xml.startDocument()
|
||||||
xml.startElement("root", {})
|
xml.startElement("root", {})
|
||||||
|
|
||||||
|
@ -164,6 +188,7 @@ class YAMLRenderer(BaseRenderer):
|
||||||
media_type = 'application/yaml'
|
media_type = 'application/yaml'
|
||||||
format = 'yaml'
|
format = 'yaml'
|
||||||
encoder = encoders.SafeDumper
|
encoder = encoders.SafeDumper
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -174,7 +199,7 @@ class YAMLRenderer(BaseRenderer):
|
||||||
if data is None:
|
if data is None:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
return yaml.dump(data, stream=None, Dumper=self.encoder)
|
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
|
||||||
|
|
||||||
|
|
||||||
class TemplateHTMLRenderer(BaseRenderer):
|
class TemplateHTMLRenderer(BaseRenderer):
|
||||||
|
@ -204,6 +229,7 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
'%(status_code)s.html',
|
'%(status_code)s.html',
|
||||||
'api_exception.html'
|
'api_exception.html'
|
||||||
]
|
]
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -275,6 +301,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
"""
|
"""
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
format = 'html'
|
format = 'html'
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
renderer_context = renderer_context or {}
|
renderer_context = renderer_context or {}
|
||||||
|
@ -296,6 +323,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
format = 'api'
|
format = 'api'
|
||||||
template = 'rest_framework/api.html'
|
template = 'rest_framework/api.html'
|
||||||
|
charset = 'utf-8'
|
||||||
|
|
||||||
def get_default_renderer(self, view):
|
def get_default_renderer(self, view):
|
||||||
"""
|
"""
|
||||||
|
@ -320,8 +348,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
renderer_context['indent'] = 4
|
renderer_context['indent'] = 4
|
||||||
content = renderer.render(data, accepted_media_type, renderer_context)
|
content = renderer.render(data, accepted_media_type, renderer_context)
|
||||||
|
|
||||||
if not all(char in string.printable for char in content):
|
if renderer.charset is None:
|
||||||
return '[%d bytes of binary content]'
|
return '[%d bytes of binary content]' % len(content)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
@ -336,7 +364,9 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
return # Cannot use form overloading
|
return # Cannot use form overloading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
view.check_permissions(clone_request(request, method))
|
view.check_permissions(request)
|
||||||
|
if obj is not None:
|
||||||
|
view.check_object_permissions(request, obj)
|
||||||
except exceptions.APIException:
|
except exceptions.APIException:
|
||||||
return False # Doesn't have permissions
|
return False # Doesn't have permissions
|
||||||
return True
|
return True
|
||||||
|
@ -366,12 +396,40 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
if getattr(v, 'default', None) is not None:
|
if getattr(v, 'default', None) is not None:
|
||||||
kwargs['initial'] = v.default
|
kwargs['initial'] = v.default
|
||||||
|
|
||||||
kwargs['label'] = k
|
if getattr(v, 'label', None) is not None:
|
||||||
|
kwargs['label'] = v.label
|
||||||
|
|
||||||
|
if getattr(v, 'help_text', None) is not None:
|
||||||
|
kwargs['help_text'] = v.help_text
|
||||||
|
|
||||||
fields[k] = v.form_field_class(**kwargs)
|
fields[k] = v.form_field_class(**kwargs)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
def _get_form(self, view, method, request):
|
||||||
|
# We need to impersonate a request with the correct method,
|
||||||
|
# so that eg. any dynamic get_serializer_class methods return the
|
||||||
|
# correct form for each method.
|
||||||
|
restore = view.request
|
||||||
|
request = clone_request(request, method)
|
||||||
|
view.request = request
|
||||||
|
try:
|
||||||
|
return self.get_form(view, method, request)
|
||||||
|
finally:
|
||||||
|
view.request = restore
|
||||||
|
|
||||||
|
def _get_raw_data_form(self, view, method, request, media_types):
|
||||||
|
# We need to impersonate a request with the correct method,
|
||||||
|
# so that eg. any dynamic get_serializer_class methods return the
|
||||||
|
# correct form for each method.
|
||||||
|
restore = view.request
|
||||||
|
request = clone_request(request, method)
|
||||||
|
view.request = request
|
||||||
|
try:
|
||||||
|
return self.get_raw_data_form(view, method, request, media_types)
|
||||||
|
finally:
|
||||||
|
view.request = restore
|
||||||
|
|
||||||
def get_form(self, view, method, request):
|
def get_form(self, view, method, request):
|
||||||
"""
|
"""
|
||||||
Get a form, possibly bound to either the input or output data.
|
Get a form, possibly bound to either the input or output data.
|
||||||
|
@ -449,10 +507,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
Renders *obj* using the :attr:`template` set on the class.
|
Render the HTML for the browsable API representation.
|
||||||
|
|
||||||
The context used in the template contains all the information
|
|
||||||
needed to self-document the response to this request.
|
|
||||||
"""
|
"""
|
||||||
accepted_media_type = accepted_media_type or ''
|
accepted_media_type = accepted_media_type or ''
|
||||||
renderer_context = renderer_context or {}
|
renderer_context = renderer_context or {}
|
||||||
|
@ -465,15 +520,15 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
renderer = self.get_default_renderer(view)
|
renderer = self.get_default_renderer(view)
|
||||||
content = self.get_content(renderer, data, accepted_media_type, renderer_context)
|
content = self.get_content(renderer, data, accepted_media_type, renderer_context)
|
||||||
|
|
||||||
put_form = self.get_form(view, 'PUT', request)
|
put_form = self._get_form(view, 'PUT', request)
|
||||||
post_form = self.get_form(view, 'POST', request)
|
post_form = self._get_form(view, 'POST', request)
|
||||||
patch_form = self.get_form(view, 'PATCH', request)
|
patch_form = self._get_form(view, 'PATCH', request)
|
||||||
delete_form = self.get_form(view, 'DELETE', request)
|
delete_form = self._get_form(view, 'DELETE', request)
|
||||||
options_form = self.get_form(view, 'OPTIONS', request)
|
options_form = self._get_form(view, 'OPTIONS', request)
|
||||||
|
|
||||||
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types)
|
raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
|
||||||
raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types)
|
raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
|
||||||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types)
|
raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
|
||||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||||
|
|
||||||
name = self.get_name(view)
|
name = self.get_name(view)
|
||||||
|
|
|
@ -173,7 +173,7 @@ class Request(object):
|
||||||
by the authentication classes provided to the request.
|
by the authentication classes provided to the request.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_user'):
|
if not hasattr(self, '_user'):
|
||||||
self._authenticator, self._user, self._auth = self._authenticate()
|
self._authenticate()
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
@user.setter
|
@user.setter
|
||||||
|
@ -192,7 +192,7 @@ class Request(object):
|
||||||
request, such as an authentication token.
|
request, such as an authentication token.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_auth'):
|
if not hasattr(self, '_auth'):
|
||||||
self._authenticator, self._user, self._auth = self._authenticate()
|
self._authenticate()
|
||||||
return self._auth
|
return self._auth
|
||||||
|
|
||||||
@auth.setter
|
@auth.setter
|
||||||
|
@ -210,7 +210,7 @@ class Request(object):
|
||||||
to authenticate the request, or `None`.
|
to authenticate the request, or `None`.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_authenticator'):
|
if not hasattr(self, '_authenticator'):
|
||||||
self._authenticator, self._user, self._auth = self._authenticate()
|
self._authenticate()
|
||||||
return self._authenticator
|
return self._authenticator
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
def _load_data_and_files(self):
|
||||||
|
@ -330,11 +330,18 @@ class Request(object):
|
||||||
Returns a three-tuple of (authenticator, user, authtoken).
|
Returns a three-tuple of (authenticator, user, authtoken).
|
||||||
"""
|
"""
|
||||||
for authenticator in self.authenticators:
|
for authenticator in self.authenticators:
|
||||||
user_auth_tuple = authenticator.authenticate(self)
|
try:
|
||||||
|
user_auth_tuple = authenticator.authenticate(self)
|
||||||
|
except exceptions.APIException:
|
||||||
|
self._not_authenticated()
|
||||||
|
raise
|
||||||
|
|
||||||
if not user_auth_tuple is None:
|
if not user_auth_tuple is None:
|
||||||
user, auth = user_auth_tuple
|
self._authenticator = authenticator
|
||||||
return (authenticator, user, auth)
|
self._user, self._auth = user_auth_tuple
|
||||||
return self._not_authenticated()
|
return
|
||||||
|
|
||||||
|
self._not_authenticated()
|
||||||
|
|
||||||
def _not_authenticated(self):
|
def _not_authenticated(self):
|
||||||
"""
|
"""
|
||||||
|
@ -343,17 +350,17 @@ class Request(object):
|
||||||
|
|
||||||
By default this will be (None, AnonymousUser, None).
|
By default this will be (None, AnonymousUser, None).
|
||||||
"""
|
"""
|
||||||
|
self._authenticator = None
|
||||||
|
|
||||||
if api_settings.UNAUTHENTICATED_USER:
|
if api_settings.UNAUTHENTICATED_USER:
|
||||||
user = api_settings.UNAUTHENTICATED_USER()
|
self._user = api_settings.UNAUTHENTICATED_USER()
|
||||||
else:
|
else:
|
||||||
user = None
|
self._user = None
|
||||||
|
|
||||||
if api_settings.UNAUTHENTICATED_TOKEN:
|
if api_settings.UNAUTHENTICATED_TOKEN:
|
||||||
auth = api_settings.UNAUTHENTICATED_TOKEN()
|
self._auth = api_settings.UNAUTHENTICATED_TOKEN()
|
||||||
else:
|
else:
|
||||||
auth = None
|
self._auth = None
|
||||||
|
|
||||||
return (None, user, auth)
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
The Response class in REST framework is similiar to HTTPResponse, except that
|
The Response class in REST framework is similar to HTTPResponse, except that
|
||||||
it is initialized with unrendered data, instead of a pre-rendered string.
|
it is initialized with unrendered data, instead of a pre-rendered string.
|
||||||
|
|
||||||
The appropriate renderer is called during Django's template response rendering.
|
The appropriate renderer is called during Django's template response rendering.
|
||||||
|
@ -12,13 +12,13 @@ from rest_framework.compat import six
|
||||||
|
|
||||||
class Response(SimpleTemplateResponse):
|
class Response(SimpleTemplateResponse):
|
||||||
"""
|
"""
|
||||||
An HttpResponse that allows it's data to be rendered into
|
An HttpResponse that allows its data to be rendered into
|
||||||
arbitrary media types.
|
arbitrary media types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data=None, status=200,
|
def __init__(self, data=None, status=200,
|
||||||
template_name=None, headers=None,
|
template_name=None, headers=None,
|
||||||
exception=False):
|
exception=False, content_type=None):
|
||||||
"""
|
"""
|
||||||
Alters the init arguments slightly.
|
Alters the init arguments slightly.
|
||||||
For example, drop 'template_name', and instead use 'data'.
|
For example, drop 'template_name', and instead use 'data'.
|
||||||
|
@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.template_name = template_name
|
self.template_name = template_name
|
||||||
self.exception = exception
|
self.exception = exception
|
||||||
|
self.content_type = content_type
|
||||||
|
|
||||||
if headers:
|
if headers:
|
||||||
for name, value in six.iteritems(headers):
|
for name, value in six.iteritems(headers):
|
||||||
|
@ -46,8 +47,21 @@ class Response(SimpleTemplateResponse):
|
||||||
assert context, ".renderer_context not set on Response"
|
assert context, ".renderer_context not set on Response"
|
||||||
context['response'] = self
|
context['response'] = self
|
||||||
|
|
||||||
self['Content-Type'] = media_type
|
charset = renderer.charset
|
||||||
return renderer.render(self.data, media_type, context)
|
content_type = self.content_type
|
||||||
|
|
||||||
|
if content_type is None and charset is not None:
|
||||||
|
content_type = "{0}; charset={1}".format(media_type, charset)
|
||||||
|
elif content_type is None:
|
||||||
|
content_type = media_type
|
||||||
|
self['Content-Type'] = content_type
|
||||||
|
|
||||||
|
ret = renderer.render(self.data, media_type, context)
|
||||||
|
if isinstance(ret, six.text_type):
|
||||||
|
assert charset, 'renderer returned unicode, and did not specify ' \
|
||||||
|
'a charset value.'
|
||||||
|
return bytes(ret.encode(charset))
|
||||||
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
|
|
|
@ -16,8 +16,8 @@ For example, you might have a `urls.py` that looks something like this:
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from django.conf.urls import url, patterns
|
from rest_framework import views
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
@ -71,7 +71,7 @@ class SimpleRouter(BaseRouter):
|
||||||
routes = [
|
routes = [
|
||||||
# List route.
|
# List route.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/$',
|
url=r'^{prefix}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'get': 'list',
|
'get': 'list',
|
||||||
'post': 'create'
|
'post': 'create'
|
||||||
|
@ -81,7 +81,7 @@ class SimpleRouter(BaseRouter):
|
||||||
),
|
),
|
||||||
# Detail route.
|
# Detail route.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/{lookup}/$',
|
url=r'^{prefix}/{lookup}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'get': 'retrieve',
|
'get': 'retrieve',
|
||||||
'put': 'update',
|
'put': 'update',
|
||||||
|
@ -94,7 +94,7 @@ class SimpleRouter(BaseRouter):
|
||||||
# Dynamically generated routes.
|
# Dynamically generated routes.
|
||||||
# Generated using @action or @link decorators on methods of the viewset.
|
# Generated using @action or @link decorators on methods of the viewset.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/{lookup}/{methodname}/$',
|
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'{httpmethod}': '{methodname}',
|
'{httpmethod}': '{methodname}',
|
||||||
},
|
},
|
||||||
|
@ -103,6 +103,10 @@ class SimpleRouter(BaseRouter):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, trailing_slash=True):
|
||||||
|
self.trailing_slash = trailing_slash and '/' or ''
|
||||||
|
super(SimpleRouter, self).__init__()
|
||||||
|
|
||||||
def get_default_base_name(self, viewset):
|
def get_default_base_name(self, viewset):
|
||||||
"""
|
"""
|
||||||
If `base_name` is not specified, attempt to automatically determine
|
If `base_name` is not specified, attempt to automatically determine
|
||||||
|
@ -127,23 +131,23 @@ class SimpleRouter(BaseRouter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||||
dynamic_routes = {}
|
dynamic_routes = []
|
||||||
for methodname in dir(viewset):
|
for methodname in dir(viewset):
|
||||||
attr = getattr(viewset, methodname)
|
attr = getattr(viewset, methodname)
|
||||||
httpmethod = getattr(attr, 'bind_to_method', None)
|
httpmethods = getattr(attr, 'bind_to_methods', None)
|
||||||
if httpmethod:
|
if httpmethods:
|
||||||
dynamic_routes[httpmethod] = methodname
|
dynamic_routes.append((httpmethods, methodname))
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for route in self.routes:
|
for route in self.routes:
|
||||||
if route.mapping == {'{httpmethod}': '{methodname}'}:
|
if route.mapping == {'{httpmethod}': '{methodname}'}:
|
||||||
# Dynamic routes (@link or @action decorator)
|
# Dynamic routes (@link or @action decorator)
|
||||||
for httpmethod, methodname in dynamic_routes.items():
|
for httpmethods, methodname in dynamic_routes:
|
||||||
initkwargs = route.initkwargs.copy()
|
initkwargs = route.initkwargs.copy()
|
||||||
initkwargs.update(getattr(viewset, methodname).kwargs)
|
initkwargs.update(getattr(viewset, methodname).kwargs)
|
||||||
ret.append(Route(
|
ret.append(Route(
|
||||||
url=replace_methodname(route.url, methodname),
|
url=replace_methodname(route.url, methodname),
|
||||||
mapping={httpmethod: methodname},
|
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
||||||
name=replace_methodname(route.name, methodname),
|
name=replace_methodname(route.name, methodname),
|
||||||
initkwargs=initkwargs,
|
initkwargs=initkwargs,
|
||||||
))
|
))
|
||||||
|
@ -192,7 +196,11 @@ class SimpleRouter(BaseRouter):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build the url pattern
|
# Build the url pattern
|
||||||
regex = route.url.format(prefix=prefix, lookup=lookup)
|
regex = route.url.format(
|
||||||
|
prefix=prefix,
|
||||||
|
lookup=lookup,
|
||||||
|
trailing_slash=self.trailing_slash
|
||||||
|
)
|
||||||
view = viewset.as_view(mapping, **route.initkwargs)
|
view = viewset.as_view(mapping, **route.initkwargs)
|
||||||
name = route.name.format(basename=basename)
|
name = route.name.format(basename=basename)
|
||||||
ret.append(url(regex, view, name=name))
|
ret.append(url(regex, view, name=name))
|
||||||
|
@ -217,14 +225,16 @@ class DefaultRouter(SimpleRouter):
|
||||||
for prefix, viewset, basename in self.registry:
|
for prefix, viewset, basename in self.registry:
|
||||||
api_root_dict[prefix] = list_name.format(basename=basename)
|
api_root_dict[prefix] = list_name.format(basename=basename)
|
||||||
|
|
||||||
@api_view(('GET',))
|
class APIRoot(views.APIView):
|
||||||
def api_root(request, format=None):
|
_ignore_model_permissions = True
|
||||||
ret = {}
|
|
||||||
for key, url_name in api_root_dict.items():
|
|
||||||
ret[key] = reverse(url_name, request=request, format=format)
|
|
||||||
return Response(ret)
|
|
||||||
|
|
||||||
return api_root
|
def get(self, request, format=None):
|
||||||
|
ret = {}
|
||||||
|
for key, url_name in api_root_dict.items():
|
||||||
|
ret[key] = reverse(url_name, request=request, format=format)
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
return APIRoot.as_view()
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -10,6 +10,7 @@ import sys
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
|
||||||
|
|
||||||
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test.utils import get_runner
|
from django.test.utils import get_runner
|
||||||
|
|
||||||
|
@ -35,7 +36,11 @@ def main():
|
||||||
else:
|
else:
|
||||||
print(usage())
|
print(usage())
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
failures = test_runner.run_tests(['tests' + test_case])
|
test_module_name = 'rest_framework.tests'
|
||||||
|
if django.VERSION[0] == 1 and django.VERSION[1] < 6:
|
||||||
|
test_module_name = 'tests'
|
||||||
|
|
||||||
|
failures = test_runner.run_tests([test_module_name + test_case])
|
||||||
|
|
||||||
sys.exit(failures)
|
sys.exit(failures)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
DEBUG_PROPAGATE_EXCEPTIONS = True
|
DEBUG_PROPAGATE_EXCEPTIONS = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Your Name', 'your_email@domain.com'),
|
# ('Your Name', 'your_email@domain.com'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ from rest_framework.compat import get_concrete_model, six
|
||||||
#
|
#
|
||||||
# example_field = serializers.CharField(...)
|
# example_field = serializers.CharField(...)
|
||||||
#
|
#
|
||||||
# This helps keep the seperation between model fields, form fields, and
|
# This helps keep the separation between model fields, form fields, and
|
||||||
# serializer fields more explicit.
|
# serializer fields more explicit.
|
||||||
|
|
||||||
from rest_framework.relations import *
|
from rest_framework.relations import *
|
||||||
|
@ -61,7 +61,7 @@ class DictWithMetadata(dict):
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
"""
|
"""
|
||||||
Used by pickle (e.g., caching).
|
Used by pickle (e.g., caching).
|
||||||
Overriden to remove the metadata from the dict, since it shouldn't be
|
Overridden to remove the metadata from the dict, since it shouldn't be
|
||||||
pickled and may in some instances be unpickleable.
|
pickled and may in some instances be unpickleable.
|
||||||
"""
|
"""
|
||||||
return dict(self)
|
return dict(self)
|
||||||
|
@ -202,7 +202,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
# If 'fields' is specified, use those fields, in that order.
|
# If 'fields' is specified, use those fields, in that order.
|
||||||
if self.opts.fields:
|
if self.opts.fields:
|
||||||
assert isinstance(self.opts.fields, (list, tuple)), '`include` must be a list or tuple'
|
assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
|
||||||
new = SortedDict()
|
new = SortedDict()
|
||||||
for key in self.opts.fields:
|
for key in self.opts.fields:
|
||||||
new[key] = ret[key]
|
new[key] = ret[key]
|
||||||
|
@ -210,7 +210,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
# Remove anything in 'exclude'
|
# Remove anything in 'exclude'
|
||||||
if self.opts.exclude:
|
if self.opts.exclude:
|
||||||
assert isinstance(self.opts.fields, (list, tuple)), '`exclude` must be a list or tuple'
|
assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
|
||||||
for key in self.opts.exclude:
|
for key in self.opts.exclude:
|
||||||
ret.pop(key, None)
|
ret.pop(key, None)
|
||||||
|
|
||||||
|
@ -317,7 +317,8 @@ class BaseSerializer(WritableField):
|
||||||
self._errors = {}
|
self._errors = {}
|
||||||
if data is not None or files is not None:
|
if data is not None or files is not None:
|
||||||
attrs = self.restore_fields(data, files)
|
attrs = self.restore_fields(data, files)
|
||||||
attrs = self.perform_validation(attrs)
|
if attrs is not None:
|
||||||
|
attrs = self.perform_validation(attrs)
|
||||||
else:
|
else:
|
||||||
self._errors['non_field_errors'] = ['No input provided']
|
self._errors['non_field_errors'] = ['No input provided']
|
||||||
|
|
||||||
|
@ -381,24 +382,28 @@ class BaseSerializer(WritableField):
|
||||||
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
||||||
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
|
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
|
||||||
|
|
||||||
if value in (None, ''):
|
if self.source == '*':
|
||||||
into[(self.source or field_name)] = None
|
if value:
|
||||||
|
into.update(value)
|
||||||
else:
|
else:
|
||||||
kwargs = {
|
if value in (None, ''):
|
||||||
'instance': obj,
|
into[(self.source or field_name)] = None
|
||||||
'data': value,
|
|
||||||
'context': self.context,
|
|
||||||
'partial': self.partial,
|
|
||||||
'many': self.many,
|
|
||||||
'allow_add_remove': self.allow_add_remove
|
|
||||||
}
|
|
||||||
serializer = self.__class__(**kwargs)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
into[self.source or field_name] = serializer.object
|
|
||||||
else:
|
else:
|
||||||
# Propagate errors up to our parent
|
kwargs = {
|
||||||
raise NestedValidationError(serializer.errors)
|
'instance': obj,
|
||||||
|
'data': value,
|
||||||
|
'context': self.context,
|
||||||
|
'partial': self.partial,
|
||||||
|
'many': self.many,
|
||||||
|
'allow_add_remove': self.allow_add_remove
|
||||||
|
}
|
||||||
|
serializer = self.__class__(**kwargs)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
into[self.source or field_name] = serializer.object
|
||||||
|
else:
|
||||||
|
# Propagate errors up to our parent
|
||||||
|
raise NestedValidationError(serializer.errors)
|
||||||
|
|
||||||
def get_identity(self, data):
|
def get_identity(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -521,6 +526,17 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
|
def metadata(self):
|
||||||
|
"""
|
||||||
|
Return a dictionary of metadata about the fields on the serializer.
|
||||||
|
Useful for things like responding to OPTIONS requests, or generating
|
||||||
|
API schemas for auto-documentation.
|
||||||
|
"""
|
||||||
|
return SortedDict(
|
||||||
|
[(field_name, field.metadata())
|
||||||
|
for field_name, field in six.iteritems(self.fields)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
|
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
|
||||||
pass
|
pass
|
||||||
|
@ -591,11 +607,16 @@ class ModelSerializer(Serializer):
|
||||||
forward_rels += [field for field in opts.many_to_many if field.serialize]
|
forward_rels += [field for field in opts.many_to_many if field.serialize]
|
||||||
|
|
||||||
for model_field in forward_rels:
|
for model_field in forward_rels:
|
||||||
|
has_through_model = False
|
||||||
|
|
||||||
if model_field.rel:
|
if model_field.rel:
|
||||||
to_many = isinstance(model_field,
|
to_many = isinstance(model_field,
|
||||||
models.fields.related.ManyToManyField)
|
models.fields.related.ManyToManyField)
|
||||||
related_model = model_field.rel.to
|
related_model = model_field.rel.to
|
||||||
|
|
||||||
|
if to_many and not model_field.rel.through._meta.auto_created:
|
||||||
|
has_through_model = True
|
||||||
|
|
||||||
if model_field.rel and nested:
|
if model_field.rel and nested:
|
||||||
if len(inspect.getargspec(self.get_nested_field).args) == 2:
|
if len(inspect.getargspec(self.get_nested_field).args) == 2:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -624,6 +645,9 @@ class ModelSerializer(Serializer):
|
||||||
field = self.get_field(model_field)
|
field = self.get_field(model_field)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
|
if has_through_model:
|
||||||
|
field.read_only = True
|
||||||
|
|
||||||
ret[model_field.name] = field
|
ret[model_field.name] = field
|
||||||
|
|
||||||
# Deal with reverse relationships
|
# Deal with reverse relationships
|
||||||
|
@ -641,6 +665,12 @@ class ModelSerializer(Serializer):
|
||||||
continue
|
continue
|
||||||
related_model = relation.model
|
related_model = relation.model
|
||||||
to_many = relation.field.rel.multiple
|
to_many = relation.field.rel.multiple
|
||||||
|
has_through_model = False
|
||||||
|
is_m2m = isinstance(relation.field,
|
||||||
|
models.fields.related.ManyToManyField)
|
||||||
|
|
||||||
|
if is_m2m and not relation.field.rel.through._meta.auto_created:
|
||||||
|
has_through_model = True
|
||||||
|
|
||||||
if nested:
|
if nested:
|
||||||
field = self.get_nested_field(None, related_model, to_many)
|
field = self.get_nested_field(None, related_model, to_many)
|
||||||
|
@ -648,13 +678,22 @@ class ModelSerializer(Serializer):
|
||||||
field = self.get_related_field(None, related_model, to_many)
|
field = self.get_related_field(None, related_model, to_many)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
|
if has_through_model:
|
||||||
|
field.read_only = True
|
||||||
|
|
||||||
ret[accessor_name] = field
|
ret[accessor_name] = field
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have bee specified
|
# Add the `read_only` flag to any fields that have bee specified
|
||||||
# in the `read_only_fields` option
|
# in the `read_only_fields` option
|
||||||
for field_name in self.opts.read_only_fields:
|
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 " \
|
||||||
|
"`read_only_fields`, but also added " \
|
||||||
|
"as an explict field. Remove it from `read_only_fields`." % \
|
||||||
|
(field_name, self.__class__.__name__)
|
||||||
assert field_name in ret, \
|
assert field_name in ret, \
|
||||||
"read_only_fields on '%s' included invalid item '%s'" % \
|
"Noexistant field '%s' specified in `read_only_fields` " \
|
||||||
|
"on serializer '%s'." % \
|
||||||
(self.__class__.__name__, field_name)
|
(self.__class__.__name__, field_name)
|
||||||
ret[field_name].read_only = True
|
ret[field_name].read_only = True
|
||||||
|
|
||||||
|
@ -703,25 +742,51 @@ class ModelSerializer(Serializer):
|
||||||
Creates a default instance of a basic non-relational field.
|
Creates a default instance of a basic non-relational field.
|
||||||
"""
|
"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
has_default = model_field.has_default()
|
|
||||||
|
|
||||||
if model_field.null or model_field.blank or has_default:
|
if model_field.null or model_field.blank:
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
|
|
||||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||||
kwargs['read_only'] = True
|
kwargs['read_only'] = True
|
||||||
|
|
||||||
if has_default:
|
if model_field.has_default():
|
||||||
kwargs['default'] = model_field.get_default()
|
kwargs['default'] = model_field.get_default()
|
||||||
|
|
||||||
if issubclass(model_field.__class__, models.TextField):
|
if issubclass(model_field.__class__, models.TextField):
|
||||||
kwargs['widget'] = widgets.Textarea
|
kwargs['widget'] = widgets.Textarea
|
||||||
|
|
||||||
|
if model_field.verbose_name is not None:
|
||||||
|
kwargs['label'] = model_field.verbose_name
|
||||||
|
|
||||||
|
if model_field.help_text is not None:
|
||||||
|
kwargs['help_text'] = model_field.help_text
|
||||||
|
|
||||||
# TODO: TypedChoiceField?
|
# TODO: TypedChoiceField?
|
||||||
if model_field.flatchoices: # This ModelField contains choices
|
if model_field.flatchoices: # This ModelField contains choices
|
||||||
kwargs['choices'] = model_field.flatchoices
|
kwargs['choices'] = model_field.flatchoices
|
||||||
return ChoiceField(**kwargs)
|
return ChoiceField(**kwargs)
|
||||||
|
|
||||||
|
# put this below the ChoiceField because min_value isn't a valid initializer
|
||||||
|
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
|
||||||
|
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
|
||||||
|
kwargs['min_value'] = 0
|
||||||
|
|
||||||
|
attribute_dict = {
|
||||||
|
models.CharField: ['max_length'],
|
||||||
|
models.CommaSeparatedIntegerField: ['max_length'],
|
||||||
|
models.DecimalField: ['max_digits', 'decimal_places'],
|
||||||
|
models.EmailField: ['max_length'],
|
||||||
|
models.FileField: ['max_length'],
|
||||||
|
models.ImageField: ['max_length'],
|
||||||
|
models.SlugField: ['max_length'],
|
||||||
|
models.URLField: ['max_length'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if model_field.__class__ in attribute_dict:
|
||||||
|
attributes = attribute_dict[model_field.__class__]
|
||||||
|
for attribute in attributes:
|
||||||
|
kwargs.update({attribute: getattr(model_field, attribute)})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.field_mapping[model_field.__class__](**kwargs)
|
return self.field_mapping[model_field.__class__](**kwargs)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -867,7 +932,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
||||||
def __init__(self, meta):
|
def __init__(self, meta):
|
||||||
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
|
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
|
||||||
self.view_name = getattr(meta, 'view_name', None)
|
self.view_name = getattr(meta, 'view_name', None)
|
||||||
self.lookup_field = getattr(meta, 'slug_field', None)
|
self.lookup_field = getattr(meta, 'lookup_field', None)
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedModelSerializer(ModelSerializer):
|
class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
|
@ -879,13 +944,24 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
_default_view_name = '%(model_name)s-detail'
|
_default_view_name = '%(model_name)s-detail'
|
||||||
_hyperlink_field_class = HyperlinkedRelatedField
|
_hyperlink_field_class = HyperlinkedRelatedField
|
||||||
|
|
||||||
url = HyperlinkedIdentityField()
|
# Just a placeholder to ensure 'url' is the first field
|
||||||
|
# The field itself is actually created on initialization,
|
||||||
|
# when the view_name and lookup_field arguments are available.
|
||||||
|
url = Field()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
|
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.opts.view_name is None:
|
if self.opts.view_name is None:
|
||||||
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
||||||
|
|
||||||
|
url_field = HyperlinkedIdentityField(
|
||||||
|
view_name=self.opts.view_name,
|
||||||
|
lookup_field=self.opts.lookup_field
|
||||||
|
)
|
||||||
|
url_field.initialize(self, 'url')
|
||||||
|
self.fields['url'] = url_field
|
||||||
|
|
||||||
def _get_default_view_name(self, model):
|
def _get_default_view_name(self, model):
|
||||||
"""
|
"""
|
||||||
Return the view name to use if 'view_name' is not specified in 'Meta'
|
Return the view name to use if 'view_name' is not specified in 'Meta'
|
||||||
|
|
|
@ -19,4 +19,167 @@ a single block in the template.
|
||||||
.navbar-inverse .brand:hover a {
|
.navbar-inverse .brand:hover a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* custom navigation styles */
|
||||||
|
.wrapper .navbar{
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner{
|
||||||
|
background: #2C2C2C;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-top: 5px solid #A30000;
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== dabapps bootstrap styles ====*/
|
||||||
|
|
||||||
|
html{
|
||||||
|
width:100%;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, .navbar .navbar-inner .container-fluid {
|
||||||
|
max-width: 1150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background: url("../img/grid.png") repeat-x;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sticky footer and footer */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switcher {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well .form-actions {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well form .help-block {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > li {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs li a {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbable.first-tab-active .tab-content
|
||||||
|
{
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer, #push {
|
||||||
|
height: 60px; /* .push must be the same height as .footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer p {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a {
|
||||||
|
color: gray;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom general page styles */
|
||||||
|
.hero-unit h2, .hero-unit h1{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a, body a{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a:hover{
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content a span{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-info {
|
||||||
|
clear:both;
|
||||||
|
}
|
||||||
|
|
|
@ -69,152 +69,3 @@ pre {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*=== dabapps bootstrap styles ====*/
|
|
||||||
|
|
||||||
html{
|
|
||||||
width:100%;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body, .navbar .navbar-inner .container-fluid {
|
|
||||||
max-width: 1150px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
background: url("../img/grid.png") repeat-x;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
/* custom navigation styles */
|
|
||||||
.wrapper .navbar{
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner{
|
|
||||||
background: #2C2C2C;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-top: 5px solid #A30000;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-list > .active > a, .nav-list > .active > a:hover {
|
|
||||||
background: #2c2c2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
.navbar .navbar-inner .dropdown-menu li a:hover{
|
|
||||||
background: #eeeeee;
|
|
||||||
color: #c20000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom general page styles */
|
|
||||||
.hero-unit h2, .hero-unit h1{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body a, body a{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body a:hover{
|
|
||||||
color: #c20000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content a span{
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sticky footer and footer */
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
height: auto !important;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 auto -60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-switcher {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well {
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well .form-actions {
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well form {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs li a {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > .active > a {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > .active > a:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbable.first-tab-active .tab-content
|
|
||||||
{
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer, #push {
|
|
||||||
height: 60px; /* .push must be the same height as .footer */
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer{
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer p {
|
|
||||||
text-align: center;
|
|
||||||
color: gray;
|
|
||||||
border-top: 1px solid #DDD;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer a {
|
|
||||||
color: gray;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer a:hover {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
{% block bootstrap_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -30,8 +32,8 @@
|
||||||
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<span class="brand" href="/">
|
<span href="/">
|
||||||
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||||
</span>
|
</span>
|
||||||
<ul class="nav pull-right">
|
<ul class="nav pull-right">
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
|
@ -109,8 +111,7 @@
|
||||||
<div class="content-main">
|
<div class="content-main">
|
||||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
<div class="request-info" style="clear: both" >
|
||||||
<div class="request-info">
|
|
||||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{{ field.label_tag|add_class:"control-label" }}
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
<span class="help-inline">{{ field.help_text }}</span>
|
<span class="help-block">{{ field.help_text }}</span>
|
||||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
<!--{{ field.errors|add_class:"help-block" }}-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,52 +4,50 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
{% block style %}
|
{% block style %}
|
||||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
{% block bootstrap_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="container">
|
<body class="container">
|
||||||
|
|
||||||
<div class="container-fluid" style="margin-top: 30px">
|
<div class="container-fluid" style="margin-top: 30px">
|
||||||
<div class="row-fluid">
|
|
||||||
|
|
||||||
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div>
|
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
|
||||||
{% block branding %}<h3 style="margin: 0 0 20px;">Django REST framework</h3>{% endblock %}
|
<div class="row-fluid">
|
||||||
</div>
|
<div>
|
||||||
</div><!-- /row fluid -->
|
{% block branding %}<h3 style="margin: 0 0 20px;">Django REST framework</h3>{% endblock %}
|
||||||
|
|
||||||
<div class="row-fluid">
|
|
||||||
<div>
|
|
||||||
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div id="div_id_username" class="clearfix control-group">
|
|
||||||
<div class="controls">
|
|
||||||
<Label class="span4">Username:</label>
|
|
||||||
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="div_id_password" class="clearfix control-group">
|
</div><!-- /row fluid -->
|
||||||
<div class="controls">
|
|
||||||
<Label class="span4">Password:</label>
|
<div class="row-fluid">
|
||||||
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
<div>
|
||||||
</div>
|
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div id="div_id_username" class="clearfix control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<Label class="span4">Username:</label>
|
||||||
|
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="div_id_password" class="clearfix control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<Label class="span4">Password:</label>
|
||||||
|
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
<div class="form-actions-no-box">
|
||||||
|
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
</div><!-- /.row-fluid -->
|
||||||
<div class="form-actions-no-box">
|
</div><!--/.well-->
|
||||||
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
</div><!-- /.row-fluid -->
|
||||||
</div>
|
</div><!-- /.container-fluid -->
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div><!-- /row fluid -->
|
|
||||||
</div><!--/span-->
|
|
||||||
|
|
||||||
</div><!-- /.row-fluid -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -15,7 +15,7 @@ register = template.Library()
|
||||||
|
|
||||||
# When 1.3 becomes unsupported by REST framework, we can instead start to
|
# When 1.3 becomes unsupported by REST framework, we can instead start to
|
||||||
# use the {% load staticfiles %} tag, remove the following code,
|
# use the {% load staticfiles %} tag, remove the following code,
|
||||||
# and add a dependancy that `django.contrib.staticfiles` must be installed.
|
# and add a dependency that `django.contrib.staticfiles` must be installed.
|
||||||
|
|
||||||
# Note: We can't put this into the `compat` module because the compat import
|
# Note: We can't put this into the `compat` module because the compat import
|
||||||
# from rest_framework.compat import ...
|
# from rest_framework.compat import ...
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
def foobar():
|
def foobar():
|
||||||
|
@ -32,7 +34,7 @@ class Anchor(RESTFrameworkModel):
|
||||||
|
|
||||||
|
|
||||||
class BasicModel(RESTFrameworkModel):
|
class BasicModel(RESTFrameworkModel):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
|
||||||
|
|
||||||
|
|
||||||
class SlugBasedModel(RESTFrameworkModel):
|
class SlugBasedModel(RESTFrameworkModel):
|
||||||
|
@ -58,13 +60,6 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel):
|
||||||
rel = models.ManyToManyField(Anchor)
|
rel = models.ManyToManyField(Anchor)
|
||||||
|
|
||||||
|
|
||||||
# Model to test filtering.
|
|
||||||
class FilterableItem(RESTFrameworkModel):
|
|
||||||
text = models.CharField(max_length=100)
|
|
||||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
|
||||||
date = models.DateField()
|
|
||||||
|
|
||||||
|
|
||||||
# Model for regression test for #285
|
# Model for regression test for #285
|
||||||
|
|
||||||
class Comment(RESTFrameworkModel):
|
class Comment(RESTFrameworkModel):
|
||||||
|
@ -166,3 +161,9 @@ class NullableOneToOneSource(RESTFrameworkModel):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
|
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
|
||||||
related_name='nullable_source')
|
related_name='nullable_source')
|
||||||
|
|
||||||
|
|
||||||
|
# Serializer used to test BasicModel
|
||||||
|
class BasicModelSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BasicModel
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
"""
|
|
||||||
General tests for relational fields.
|
|
||||||
"""
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from django.db import models
|
|
||||||
from django.test import TestCase
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class NullModel(models.Model):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FieldTests(TestCase):
|
|
||||||
def test_pk_related_field_with_empty_string(self):
|
|
||||||
"""
|
|
||||||
Regression test for #446
|
|
||||||
|
|
||||||
https://github.com/tomchristie/django-rest-framework/issues/446
|
|
||||||
"""
|
|
||||||
field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
|
||||||
|
|
||||||
def test_hyperlinked_related_field_with_empty_string(self):
|
|
||||||
field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
|
||||||
|
|
||||||
def test_slug_related_field_with_empty_string(self):
|
|
||||||
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
|
||||||
|
|
||||||
|
|
||||||
class TestManyRelateMixin(TestCase):
|
|
||||||
def test_missing_many_to_many_related_field(self):
|
|
||||||
'''
|
|
||||||
Regression test for #632
|
|
||||||
|
|
||||||
https://github.com/tomchristie/django-rest-framework/pull/632
|
|
||||||
'''
|
|
||||||
field = serializers.RelatedField(many=True, read_only=False)
|
|
||||||
|
|
||||||
into = {}
|
|
||||||
field.field_from_native({}, None, 'field_name', into)
|
|
||||||
self.assertEqual(into['field_name'], [])
|
|
|
@ -6,6 +6,8 @@ from django.utils import unittest
|
||||||
from rest_framework import HTTP_HEADER_ENCODING
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
from rest_framework import renderers
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authentication import (
|
from rest_framework.authentication import (
|
||||||
BaseAuthentication,
|
BaseAuthentication,
|
||||||
|
@ -48,7 +50,7 @@ urlpatterns = patterns('',
|
||||||
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
|
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
|
||||||
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
|
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
|
||||||
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
|
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
|
||||||
(r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
|
(r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
|
||||||
permission_classes=[permissions.TokenHasReadWriteScope]))
|
permission_classes=[permissions.TokenHasReadWriteScope]))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,14 +58,14 @@ if oauth2_provider is not None:
|
||||||
urlpatterns += patterns('',
|
urlpatterns += patterns('',
|
||||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
||||||
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
|
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
|
||||||
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
|
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
|
||||||
permission_classes=[permissions.TokenHasReadWriteScope])),
|
permission_classes=[permissions.TokenHasReadWriteScope])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BasicAuthTests(TestCase):
|
class BasicAuthTests(TestCase):
|
||||||
"""Basic authentication"""
|
"""Basic authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = 'rest_framework.tests.test_authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
@ -102,7 +104,7 @@ class BasicAuthTests(TestCase):
|
||||||
|
|
||||||
class SessionAuthTests(TestCase):
|
class SessionAuthTests(TestCase):
|
||||||
"""User session authentication"""
|
"""User session authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = 'rest_framework.tests.test_authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
@ -149,7 +151,7 @@ class SessionAuthTests(TestCase):
|
||||||
|
|
||||||
class TokenAuthTests(TestCase):
|
class TokenAuthTests(TestCase):
|
||||||
"""Token authentication"""
|
"""Token authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = 'rest_framework.tests.test_authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
@ -243,7 +245,7 @@ class IncorrectCredentialsTests(TestCase):
|
||||||
|
|
||||||
class OAuthTests(TestCase):
|
class OAuthTests(TestCase):
|
||||||
"""OAuth 1.0a authentication"""
|
"""OAuth 1.0a authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = 'rest_framework.tests.test_authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# these imports are here because oauth is optional and hiding them in try..except block or compat
|
# these imports are here because oauth is optional and hiding them in try..except block or compat
|
||||||
|
@ -429,7 +431,7 @@ class OAuthTests(TestCase):
|
||||||
|
|
||||||
class OAuth2Tests(TestCase):
|
class OAuth2Tests(TestCase):
|
||||||
"""OAuth 2.0 authentication"""
|
"""OAuth 2.0 authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = 'rest_framework.tests.test_authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
@ -553,3 +555,40 @@ class OAuth2Tests(TestCase):
|
||||||
auth = self._create_authorization_header(token=read_write_access_token.token)
|
auth = self._create_authorization_header(token=read_write_access_token.token)
|
||||||
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
|
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class FailingAuthAccessedInRenderer(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class AuthAccessingRenderer(renderers.BaseRenderer):
|
||||||
|
media_type = 'text/plain'
|
||||||
|
format = 'txt'
|
||||||
|
|
||||||
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
request = renderer_context['request']
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
return b'authenticated'
|
||||||
|
return b'not authenticated'
|
||||||
|
|
||||||
|
class FailingAuth(BaseAuthentication):
|
||||||
|
def authenticate(self, request):
|
||||||
|
raise exceptions.AuthenticationFailed('authentication failed')
|
||||||
|
|
||||||
|
class ExampleView(APIView):
|
||||||
|
authentication_classes = (FailingAuth,)
|
||||||
|
renderer_classes = (AuthAccessingRenderer,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'foo': 'bar'})
|
||||||
|
|
||||||
|
self.view = ExampleView.as_view()
|
||||||
|
|
||||||
|
def test_failing_auth_accessed_in_renderer(self):
|
||||||
|
"""
|
||||||
|
When authentication fails the renderer should still be able to access
|
||||||
|
`request.user` without raising an exception. Particularly relevant
|
||||||
|
to HTML responses that might reasonably access `request.user`.
|
||||||
|
"""
|
||||||
|
request = factory.get('/')
|
||||||
|
response = self.view(request)
|
||||||
|
content = response.render().content
|
||||||
|
self.assertEqual(content, b'not authenticated')
|
|
@ -36,7 +36,7 @@ urlpatterns = patterns('',
|
||||||
class BreadcrumbTests(TestCase):
|
class BreadcrumbTests(TestCase):
|
||||||
"""Tests the breadcrumb functionality used by the HTML renderer."""
|
"""Tests the breadcrumb functionality used by the HTML renderer."""
|
||||||
|
|
||||||
urls = 'rest_framework.tests.breadcrumbs'
|
urls = 'rest_framework.tests.test_breadcrumbs'
|
||||||
|
|
||||||
def test_root_breadcrumbs(self):
|
def test_root_breadcrumbs(self):
|
||||||
url = '/'
|
url = '/'
|
|
@ -2,15 +2,16 @@
|
||||||
General serializer field tests.
|
General serializer field tests.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from uuid import uuid4
|
||||||
|
from django.core import validators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core import validators
|
from django.utils.datastructures import SortedDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.tests.models import RESTFrameworkModel
|
||||||
|
|
||||||
|
|
||||||
class TimestampedModel(models.Model):
|
class TimestampedModel(models.Model):
|
||||||
|
@ -63,6 +64,20 @@ class BasicFieldTests(TestCase):
|
||||||
serializer = CharPrimaryKeyModelSerializer()
|
serializer = CharPrimaryKeyModelSerializer()
|
||||||
self.assertEqual(serializer.fields['id'].read_only, False)
|
self.assertEqual(serializer.fields['id'].read_only, False)
|
||||||
|
|
||||||
|
def test_dict_field_ordering(self):
|
||||||
|
"""
|
||||||
|
Field should preserve dictionary ordering, if it exists.
|
||||||
|
See: https://github.com/tomchristie/django-rest-framework/issues/832
|
||||||
|
"""
|
||||||
|
ret = SortedDict()
|
||||||
|
ret['c'] = 1
|
||||||
|
ret['b'] = 1
|
||||||
|
ret['a'] = 1
|
||||||
|
ret['z'] = 1
|
||||||
|
field = serializers.Field()
|
||||||
|
keys = list(field.to_native(ret).keys())
|
||||||
|
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
||||||
|
|
||||||
|
|
||||||
class DateFieldTest(TestCase):
|
class DateFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -573,7 +588,7 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure the serializer works correctly
|
Make sure the serializer works correctly
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(max_value=9010,
|
decimal_field = serializers.DecimalField(max_value=9010,
|
||||||
min_value=9000,
|
min_value=9000,
|
||||||
max_digits=6,
|
max_digits=6,
|
||||||
|
@ -591,7 +606,7 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure max_value violations raises ValidationError
|
Make sure max_value violations raises ValidationError
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(max_value=100)
|
decimal_field = serializers.DecimalField(max_value=100)
|
||||||
|
|
||||||
s = DecimalSerializer(data={'decimal_field': '123'})
|
s = DecimalSerializer(data={'decimal_field': '123'})
|
||||||
|
@ -603,7 +618,7 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure min_value violations raises ValidationError
|
Make sure min_value violations raises ValidationError
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(min_value=100)
|
decimal_field = serializers.DecimalField(min_value=100)
|
||||||
|
|
||||||
s = DecimalSerializer(data={'decimal_field': '99'})
|
s = DecimalSerializer(data={'decimal_field': '99'})
|
||||||
|
@ -615,7 +630,7 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure max_digits violations raises ValidationError
|
Make sure max_digits violations raises ValidationError
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(max_digits=5)
|
decimal_field = serializers.DecimalField(max_digits=5)
|
||||||
|
|
||||||
s = DecimalSerializer(data={'decimal_field': '123.456'})
|
s = DecimalSerializer(data={'decimal_field': '123.456'})
|
||||||
|
@ -627,7 +642,7 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure max_decimal_places violations raises ValidationError
|
Make sure max_decimal_places violations raises ValidationError
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(decimal_places=3)
|
decimal_field = serializers.DecimalField(decimal_places=3)
|
||||||
|
|
||||||
s = DecimalSerializer(data={'decimal_field': '123.4567'})
|
s = DecimalSerializer(data={'decimal_field': '123.4567'})
|
||||||
|
@ -639,10 +654,215 @@ class DecimalFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Make sure max_whole_digits violations raises ValidationError
|
Make sure max_whole_digits violations raises ValidationError
|
||||||
"""
|
"""
|
||||||
class DecimalSerializer(Serializer):
|
class DecimalSerializer(serializers.Serializer):
|
||||||
decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
|
decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
|
||||||
|
|
||||||
s = DecimalSerializer(data={'decimal_field': '12345.6'})
|
s = DecimalSerializer(data={'decimal_field': '12345.6'})
|
||||||
|
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the ChoiceField options generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
SAMPLE_CHOICES = [
|
||||||
|
('red', 'Red'),
|
||||||
|
('green', 'Green'),
|
||||||
|
('blue', 'Blue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_choices_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices are rendered if field is required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, self.SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_choices_not_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices (plus blank) are rendered if the field isn't required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for EmailField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class EmailFieldModel(RESTFrameworkModel):
|
||||||
|
email_field = models.EmailField(blank=True)
|
||||||
|
|
||||||
|
class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
email_field = models.EmailField(max_length=150, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
|
||||||
|
|
||||||
|
|
||||||
|
class SlugFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for SlugField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SlugFieldModel(RESTFrameworkModel):
|
||||||
|
slug_field = models.SlugField(blank=True)
|
||||||
|
|
||||||
|
class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
slug_field = models.SlugField(max_length=84, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
slug_field = serializers.SlugField(source='slug_field',
|
||||||
|
max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'],
|
||||||
|
'max_length'), 20)
|
||||||
|
|
||||||
|
def test_invalid_slug(self):
|
||||||
|
"""
|
||||||
|
Make sure an invalid slug raises ValidationError
|
||||||
|
"""
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldModel
|
||||||
|
|
||||||
|
s = SlugFieldSerializer(data={'slug_field': 'a b'})
|
||||||
|
|
||||||
|
self.assertEqual(s.is_valid(), False)
|
||||||
|
self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
|
||||||
|
|
||||||
|
|
||||||
|
class URLFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for URLField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class URLFieldModel(RESTFrameworkModel):
|
||||||
|
url_field = models.URLField(blank=True)
|
||||||
|
|
||||||
|
class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
url_field = models.URLField(max_length=128, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'],
|
||||||
|
'max_length'), 200)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'],
|
||||||
|
'max_length'), 128)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
url_field = serializers.URLField(source='url_field',
|
||||||
|
max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'],
|
||||||
|
'max_length'), 20)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldMetadata(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.required_field = serializers.Field()
|
||||||
|
self.required_field.label = uuid4().hex
|
||||||
|
self.required_field.required = True
|
||||||
|
|
||||||
|
self.optional_field = serializers.Field()
|
||||||
|
self.optional_field.label = uuid4().hex
|
||||||
|
self.optional_field.required = False
|
||||||
|
|
||||||
|
def test_required(self):
|
||||||
|
self.assertEqual(self.required_field.metadata()['required'], True)
|
||||||
|
|
||||||
|
def test_optional(self):
|
||||||
|
self.assertEqual(self.optional_field.metadata()['required'], False)
|
||||||
|
|
||||||
|
def test_label(self):
|
||||||
|
for field in (self.required_field, self.optional_field):
|
||||||
|
self.assertEqual(field.metadata()['label'], field.label)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldCallableDefault(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.simple_callable = lambda: 'foo bar'
|
||||||
|
|
||||||
|
def test_default_can_be_simple_callable(self):
|
||||||
|
"""
|
||||||
|
Ensure that the 'default' argument can also be a simple callable.
|
||||||
|
"""
|
||||||
|
field = serializers.WritableField(default=self.simple_callable)
|
||||||
|
into = {}
|
||||||
|
field.field_from_native({}, {}, 'field', into)
|
||||||
|
self.assertEqual(into, {'field': 'foo bar'})
|
|
@ -1,23 +1,30 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.db import models
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework import generics, serializers, status, filters
|
from rest_framework import generics, serializers, status, filters
|
||||||
from rest_framework.compat import django_filters, patterns, url
|
from rest_framework.compat import django_filters, patterns, url
|
||||||
from rest_framework.tests.models import FilterableItem, BasicModel
|
from rest_framework.tests.models import BasicModel
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
class FilterableItem(models.Model):
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||||
|
date = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
if django_filters:
|
if django_filters:
|
||||||
# Basic filter on a list view.
|
# Basic filter on a list view.
|
||||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
filter_fields = ['decimal', 'date']
|
filter_fields = ['decimal', 'date']
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
# These class are used to test a filter class.
|
# These class are used to test a filter class.
|
||||||
class SeveralFieldsFilter(django_filters.FilterSet):
|
class SeveralFieldsFilter(django_filters.FilterSet):
|
||||||
|
@ -32,7 +39,7 @@ if django_filters:
|
||||||
class FilterClassRootView(generics.ListCreateAPIView):
|
class FilterClassRootView(generics.ListCreateAPIView):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
filter_class = SeveralFieldsFilter
|
filter_class = SeveralFieldsFilter
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
# These classes are used to test a misconfigured filter class.
|
# These classes are used to test a misconfigured filter class.
|
||||||
class MisconfiguredFilter(django_filters.FilterSet):
|
class MisconfiguredFilter(django_filters.FilterSet):
|
||||||
|
@ -45,12 +52,12 @@ if django_filters:
|
||||||
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
|
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
filter_class = MisconfiguredFilter
|
filter_class = MisconfiguredFilter
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
class FilterClassDetailView(generics.RetrieveAPIView):
|
class FilterClassDetailView(generics.RetrieveAPIView):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
filter_class = SeveralFieldsFilter
|
filter_class = SeveralFieldsFilter
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
# Regression test for #814
|
# Regression test for #814
|
||||||
class FilterableItemSerializer(serializers.ModelSerializer):
|
class FilterableItemSerializer(serializers.ModelSerializer):
|
||||||
|
@ -61,11 +68,21 @@ if django_filters:
|
||||||
queryset = FilterableItem.objects.all()
|
queryset = FilterableItem.objects.all()
|
||||||
serializer_class = FilterableItemSerializer
|
serializer_class = FilterableItemSerializer
|
||||||
filter_fields = ['decimal', 'date']
|
filter_fields = ['decimal', 'date']
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
|
class GetQuerysetView(generics.ListCreateAPIView):
|
||||||
|
serializer_class = FilterableItemSerializer
|
||||||
|
filter_class = SeveralFieldsFilter
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return FilterableItem.objects.all()
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
|
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
|
||||||
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
|
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
|
||||||
|
url(r'^get-queryset/$', GetQuerysetView.as_view(),
|
||||||
|
name='get-queryset-view'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -140,6 +157,17 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
||||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||||
self.assertEqual(response.data, expected_data)
|
self.assertEqual(response.data, expected_data)
|
||||||
|
|
||||||
|
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||||
|
def test_filter_with_get_queryset_only(self):
|
||||||
|
"""
|
||||||
|
Regression test for #834.
|
||||||
|
"""
|
||||||
|
view = GetQuerysetView.as_view()
|
||||||
|
request = factory.get('/get-queryset/')
|
||||||
|
view(request).render()
|
||||||
|
# Used to raise "issubclass() arg 2 must be a class or tuple of classes"
|
||||||
|
# here when neither `model' nor `queryset' was specified.
|
||||||
|
|
||||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||||
def test_get_filtered_class_root_view(self):
|
def test_get_filtered_class_root_view(self):
|
||||||
"""
|
"""
|
||||||
|
@ -215,7 +243,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
||||||
"""
|
"""
|
||||||
Integration tests for filtered detail views.
|
Integration tests for filtered detail views.
|
||||||
"""
|
"""
|
||||||
urls = 'rest_framework.tests.filterset'
|
urls = 'rest_framework.tests.test_filters'
|
||||||
|
|
||||||
def _get_url(self, item):
|
def _get_url(self, item):
|
||||||
return reverse('detail-view', kwargs=dict(pk=item.pk))
|
return reverse('detail-view', kwargs=dict(pk=item.pk))
|
||||||
|
@ -256,3 +284,191 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
||||||
response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
|
response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, valid_item_data)
|
self.assertEqual(response.data, valid_item_data)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFilterModel(models.Model):
|
||||||
|
title = models.CharField(max_length=20)
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFilterTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Sequence of title/text is:
|
||||||
|
#
|
||||||
|
# z abc
|
||||||
|
# zz bcd
|
||||||
|
# zzz cde
|
||||||
|
# ...
|
||||||
|
for idx in range(10):
|
||||||
|
title = 'z' * (idx + 1)
|
||||||
|
text = (
|
||||||
|
chr(idx + ord('a')) +
|
||||||
|
chr(idx + ord('b')) +
|
||||||
|
chr(idx + ord('c'))
|
||||||
|
)
|
||||||
|
SearchFilterModel(title=title, text=text).save()
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
class SearchListView(generics.ListAPIView):
|
||||||
|
model = SearchFilterModel
|
||||||
|
filter_backends = (filters.SearchFilter,)
|
||||||
|
search_fields = ('title', 'text')
|
||||||
|
|
||||||
|
view = SearchListView.as_view()
|
||||||
|
request = factory.get('?search=b')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 1, 'title': 'z', 'text': 'abc'},
|
||||||
|
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_exact_search(self):
|
||||||
|
class SearchListView(generics.ListAPIView):
|
||||||
|
model = SearchFilterModel
|
||||||
|
filter_backends = (filters.SearchFilter,)
|
||||||
|
search_fields = ('=title', 'text')
|
||||||
|
|
||||||
|
view = SearchListView.as_view()
|
||||||
|
request = factory.get('?search=zzz')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'title': 'zzz', 'text': 'cde'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_startswith_search(self):
|
||||||
|
class SearchListView(generics.ListAPIView):
|
||||||
|
model = SearchFilterModel
|
||||||
|
filter_backends = (filters.SearchFilter,)
|
||||||
|
search_fields = ('title', '^text')
|
||||||
|
|
||||||
|
view = SearchListView.as_view()
|
||||||
|
request = factory.get('?search=b')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrdringFilterModel(models.Model):
|
||||||
|
title = models.CharField(max_length=20)
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingFilterTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Sequence of title/text is:
|
||||||
|
#
|
||||||
|
# zyx abc
|
||||||
|
# yxw bcd
|
||||||
|
# xwv cde
|
||||||
|
for idx in range(3):
|
||||||
|
title = (
|
||||||
|
chr(ord('z') - idx) +
|
||||||
|
chr(ord('y') - idx) +
|
||||||
|
chr(ord('x') - idx)
|
||||||
|
)
|
||||||
|
text = (
|
||||||
|
chr(idx + ord('a')) +
|
||||||
|
chr(idx + ord('b')) +
|
||||||
|
chr(idx + ord('c'))
|
||||||
|
)
|
||||||
|
OrdringFilterModel(title=title, text=text).save()
|
||||||
|
|
||||||
|
def test_ordering(self):
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('title',)
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('?ordering=text')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reverse_ordering(self):
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('title',)
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('?ordering=-text')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_incorrectfield_ordering(self):
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('title',)
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('?ordering=foobar')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_ordering(self):
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('title',)
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_ordering_using_string(self):
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = 'title'
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
]
|
||||||
|
)
|
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import generics, serializers, status
|
from rest_framework import generics, renderers, serializers, status
|
||||||
from rest_framework.tests.utils import RequestFactory
|
from rest_framework.tests.utils import RequestFactory
|
||||||
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
@ -39,6 +39,7 @@ class SlugBasedInstanceView(InstanceView):
|
||||||
"""
|
"""
|
||||||
model = SlugBasedModel
|
model = SlugBasedModel
|
||||||
serializer_class = SlugSerializer
|
serializer_class = SlugSerializer
|
||||||
|
lookup_field = 'slug'
|
||||||
|
|
||||||
|
|
||||||
class TestRootView(TestCase):
|
class TestRootView(TestCase):
|
||||||
|
@ -120,7 +121,25 @@ class TestRootView(TestCase):
|
||||||
'text/html'
|
'text/html'
|
||||||
],
|
],
|
||||||
'name': 'Root',
|
'name': 'Root',
|
||||||
'description': 'Example description for OPTIONS.'
|
'description': 'Example description for OPTIONS.',
|
||||||
|
'actions': {
|
||||||
|
'POST': {
|
||||||
|
'text': {
|
||||||
|
'max_length': 100,
|
||||||
|
'read_only': False,
|
||||||
|
'required': True,
|
||||||
|
'type': 'string',
|
||||||
|
"label": "Text comes here",
|
||||||
|
"help_text": "Text description."
|
||||||
|
},
|
||||||
|
'id': {
|
||||||
|
'read_only': True,
|
||||||
|
'required': False,
|
||||||
|
'type': 'integer',
|
||||||
|
'label': 'ID',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, expected)
|
self.assertEqual(response.data, expected)
|
||||||
|
@ -223,9 +242,9 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||||
"""
|
"""
|
||||||
request = factory.options('/')
|
request = factory.options('/1')
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(1):
|
||||||
response = self.view(request).render()
|
response = self.view(request, pk=1).render()
|
||||||
expected = {
|
expected = {
|
||||||
'parses': [
|
'parses': [
|
||||||
'application/json',
|
'application/json',
|
||||||
|
@ -237,11 +256,39 @@ class TestInstanceView(TestCase):
|
||||||
'text/html'
|
'text/html'
|
||||||
],
|
],
|
||||||
'name': 'Instance',
|
'name': 'Instance',
|
||||||
'description': 'Example description for OPTIONS.'
|
'description': 'Example description for OPTIONS.',
|
||||||
|
'actions': {
|
||||||
|
'PUT': {
|
||||||
|
'text': {
|
||||||
|
'max_length': 100,
|
||||||
|
'read_only': False,
|
||||||
|
'required': True,
|
||||||
|
'type': 'string',
|
||||||
|
'label': 'Text comes here',
|
||||||
|
'help_text': 'Text description.'
|
||||||
|
},
|
||||||
|
'id': {
|
||||||
|
'read_only': True,
|
||||||
|
'required': False,
|
||||||
|
'type': 'integer',
|
||||||
|
'label': 'ID',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, expected)
|
self.assertEqual(response.data, expected)
|
||||||
|
|
||||||
|
def test_get_instance_view_incorrect_arg(self):
|
||||||
|
"""
|
||||||
|
GET requests with an incorrect pk type, should raise 404, not 500.
|
||||||
|
Regression test for #890.
|
||||||
|
"""
|
||||||
|
request = factory.get('/a')
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
response = self.view(request, pk='a').render()
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_put_cannot_set_id(self):
|
def test_put_cannot_set_id(self):
|
||||||
"""
|
"""
|
||||||
PUT requests to create a new object should not be able to set the id.
|
PUT requests to create a new object should not be able to set the id.
|
||||||
|
@ -434,22 +481,14 @@ class TestFilterBackendAppliedToViews(TestCase):
|
||||||
{'id': obj.id, 'text': obj.text}
|
{'id': obj.id, 'text': obj.text}
|
||||||
for obj in self.objects.all()
|
for obj in self.objects.all()
|
||||||
]
|
]
|
||||||
self.root_view = RootView.as_view()
|
|
||||||
self.instance_view = InstanceView.as_view()
|
|
||||||
self.original_root_backend = getattr(RootView, 'filter_backend')
|
|
||||||
self.original_instance_backend = getattr(InstanceView, 'filter_backend')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
setattr(RootView, 'filter_backend', self.original_root_backend)
|
|
||||||
setattr(InstanceView, 'filter_backend', self.original_instance_backend)
|
|
||||||
|
|
||||||
def test_get_root_view_filters_by_name_with_filter_backend(self):
|
def test_get_root_view_filters_by_name_with_filter_backend(self):
|
||||||
"""
|
"""
|
||||||
GET requests to ListCreateAPIView should return filtered list.
|
GET requests to ListCreateAPIView should return filtered list.
|
||||||
"""
|
"""
|
||||||
setattr(RootView, 'filter_backend', InclusiveFilterBackend)
|
root_view = RootView.as_view(filter_backends=(InclusiveFilterBackend,))
|
||||||
request = factory.get('/')
|
request = factory.get('/')
|
||||||
response = self.root_view(request).render()
|
response = root_view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(len(response.data), 1)
|
||||||
self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}])
|
self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}])
|
||||||
|
@ -458,9 +497,9 @@ class TestFilterBackendAppliedToViews(TestCase):
|
||||||
"""
|
"""
|
||||||
GET requests to ListCreateAPIView should return empty list when all models are filtered out.
|
GET requests to ListCreateAPIView should return empty list when all models are filtered out.
|
||||||
"""
|
"""
|
||||||
setattr(RootView, 'filter_backend', ExclusiveFilterBackend)
|
root_view = RootView.as_view(filter_backends=(ExclusiveFilterBackend,))
|
||||||
request = factory.get('/')
|
request = factory.get('/')
|
||||||
response = self.root_view(request).render()
|
response = root_view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, [])
|
self.assertEqual(response.data, [])
|
||||||
|
|
||||||
|
@ -468,9 +507,9 @@ class TestFilterBackendAppliedToViews(TestCase):
|
||||||
"""
|
"""
|
||||||
GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out.
|
GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out.
|
||||||
"""
|
"""
|
||||||
setattr(InstanceView, 'filter_backend', ExclusiveFilterBackend)
|
instance_view = InstanceView.as_view(filter_backends=(ExclusiveFilterBackend,))
|
||||||
request = factory.get('/1')
|
request = factory.get('/1')
|
||||||
response = self.instance_view(request, pk=1).render()
|
response = instance_view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
self.assertEqual(response.data, {'detail': 'Not found'})
|
self.assertEqual(response.data, {'detail': 'Not found'})
|
||||||
|
|
||||||
|
@ -478,8 +517,40 @@ class TestFilterBackendAppliedToViews(TestCase):
|
||||||
"""
|
"""
|
||||||
GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded
|
GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded
|
||||||
"""
|
"""
|
||||||
setattr(InstanceView, 'filter_backend', InclusiveFilterBackend)
|
instance_view = InstanceView.as_view(filter_backends=(InclusiveFilterBackend,))
|
||||||
request = factory.get('/1')
|
request = factory.get('/1')
|
||||||
response = self.instance_view(request, pk=1).render()
|
response = instance_view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFieldModel(models.Model):
|
||||||
|
field_a = models.CharField(max_length=100)
|
||||||
|
field_b = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicSerializerView(generics.ListCreateAPIView):
|
||||||
|
model = TwoFieldModel
|
||||||
|
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
class DynamicSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = TwoFieldModel
|
||||||
|
fields = ('field_b',)
|
||||||
|
return DynamicSerializer
|
||||||
|
return super(DynamicSerializerView, self).get_serializer_class()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterBackendAppliedToViews(TestCase):
|
||||||
|
|
||||||
|
def test_dynamic_serializer_form_in_browsable_api(self):
|
||||||
|
"""
|
||||||
|
GET requests to ListCreateAPIView should return filtered list.
|
||||||
|
"""
|
||||||
|
view = DynamicSerializerView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request).render()
|
||||||
|
self.assertContains(response, 'field_b')
|
||||||
|
self.assertNotContains(response, 'field_a')
|
|
@ -42,7 +42,7 @@ urlpatterns = patterns('',
|
||||||
|
|
||||||
|
|
||||||
class TemplateHTMLRendererTests(TestCase):
|
class TemplateHTMLRendererTests(TestCase):
|
||||||
urls = 'rest_framework.tests.htmlrenderer'
|
urls = 'rest_framework.tests.test_htmlrenderer'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -66,23 +66,23 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
def test_simple_html_view(self):
|
def test_simple_html_view(self):
|
||||||
response = self.client.get('/')
|
response = self.client.get('/')
|
||||||
self.assertContains(response, "example: foobar")
|
self.assertContains(response, "example: foobar")
|
||||||
self.assertEqual(response['Content-Type'], 'text/html')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
def test_not_found_html_view(self):
|
def test_not_found_html_view(self):
|
||||||
response = self.client.get('/not_found')
|
response = self.client.get('/not_found')
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
self.assertEqual(response.content, six.b("404 Not Found"))
|
self.assertEqual(response.content, six.b("404 Not Found"))
|
||||||
self.assertEqual(response['Content-Type'], 'text/html')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
def test_permission_denied_html_view(self):
|
def test_permission_denied_html_view(self):
|
||||||
response = self.client.get('/permission_denied')
|
response = self.client.get('/permission_denied')
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(response.content, six.b("403 Forbidden"))
|
self.assertEqual(response.content, six.b("403 Forbidden"))
|
||||||
self.assertEqual(response['Content-Type'], 'text/html')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
|
|
||||||
class TemplateHTMLRendererExceptionTests(TestCase):
|
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||||
urls = 'rest_framework.tests.htmlrenderer'
|
urls = 'rest_framework.tests.test_htmlrenderer'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase):
|
||||||
response = self.client.get('/not_found')
|
response = self.client.get('/not_found')
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
self.assertEqual(response.content, six.b("404: Not found"))
|
self.assertEqual(response.content, six.b("404: Not found"))
|
||||||
self.assertEqual(response['Content-Type'], 'text/html')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
def test_permission_denied_html_view_with_template(self):
|
def test_permission_denied_html_view_with_template(self):
|
||||||
response = self.client.get('/permission_denied')
|
response = self.client.get('/permission_denied')
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(response.content, six.b("403: Permission denied"))
|
self.assertEqual(response.content, six.b("403: Permission denied"))
|
||||||
self.assertEqual(response['Content-Type'], 'text/html')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
|
@ -27,6 +27,14 @@ class PhotoSerializer(serializers.Serializer):
|
||||||
return Photo(**attrs)
|
return Photo(**attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Album
|
||||||
|
fields = ('title', 'url')
|
||||||
|
|
||||||
|
|
||||||
class BasicList(generics.ListCreateAPIView):
|
class BasicList(generics.ListCreateAPIView):
|
||||||
model = BasicModel
|
model = BasicModel
|
||||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||||
|
@ -73,6 +81,8 @@ class PhotoListCreate(generics.ListCreateAPIView):
|
||||||
|
|
||||||
class AlbumDetail(generics.RetrieveAPIView):
|
class AlbumDetail(generics.RetrieveAPIView):
|
||||||
model = Album
|
model = Album
|
||||||
|
serializer_class = AlbumSerializer
|
||||||
|
lookup_field = 'title'
|
||||||
|
|
||||||
|
|
||||||
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
@ -96,7 +106,7 @@ urlpatterns = patterns('',
|
||||||
|
|
||||||
|
|
||||||
class TestBasicHyperlinkedView(TestCase):
|
class TestBasicHyperlinkedView(TestCase):
|
||||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -133,7 +143,7 @@ class TestBasicHyperlinkedView(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestManyToManyHyperlinkedView(TestCase):
|
class TestManyToManyHyperlinkedView(TestCase):
|
||||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -180,8 +190,38 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
self.assertEqual(response.data, self.data[0])
|
self.assertEqual(response.data, self.data[0])
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||||
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Create 3 Album instances.
|
||||||
|
"""
|
||||||
|
titles = ['foo', 'bar', 'baz']
|
||||||
|
for title in titles:
|
||||||
|
album = Album(title=title)
|
||||||
|
album.save()
|
||||||
|
self.detail_view = AlbumDetail.as_view()
|
||||||
|
self.data = {
|
||||||
|
'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
|
||||||
|
'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
|
||||||
|
'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_lookup_field(self):
|
||||||
|
"""
|
||||||
|
GET requests to AlbumDetail view should return serialized Albums
|
||||||
|
with a url field keyed by `title`.
|
||||||
|
"""
|
||||||
|
for album in Album.objects.all():
|
||||||
|
request = factory.get('/albums/{0}/'.format(album.title))
|
||||||
|
response = self.detail_view(request, title=album.title)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, self.data[album.title])
|
||||||
|
|
||||||
|
|
||||||
class TestCreateWithForeignKeys(TestCase):
|
class TestCreateWithForeignKeys(TestCase):
|
||||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -206,7 +246,7 @@ class TestCreateWithForeignKeys(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -231,7 +271,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestOptionalRelationHyperlinkedView(TestCase):
|
class TestOptionalRelationHyperlinkedView(TestCase):
|
||||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
|
@ -3,19 +3,24 @@ from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.renderers import BaseRenderer
|
||||||
|
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
class MockJSONRenderer(object):
|
class MockJSONRenderer(BaseRenderer):
|
||||||
media_type = 'application/json'
|
media_type = 'application/json'
|
||||||
|
|
||||||
|
|
||||||
class MockHTMLRenderer(object):
|
class MockHTMLRenderer(BaseRenderer):
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
|
|
||||||
|
|
||||||
|
class NoCharsetSpecifiedRenderer(BaseRenderer):
|
||||||
|
media_type = 'my/media'
|
||||||
|
|
||||||
|
|
||||||
class TestAcceptedMediaType(TestCase):
|
class TestAcceptedMediaType(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
|
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
|
|
@ -1,18 +1,24 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import django
|
from django.db import models
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework import generics, status, pagination, filters, serializers
|
from rest_framework import generics, status, pagination, filters, serializers
|
||||||
from rest_framework.compat import django_filters
|
from rest_framework.compat import django_filters
|
||||||
from rest_framework.tests.models import BasicModel, FilterableItem
|
from rest_framework.tests.models import BasicModel
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
class FilterableItem(models.Model):
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||||
|
date = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
class RootView(generics.ListCreateAPIView):
|
class RootView(generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
Example description for OPTIONS.
|
Example description for OPTIONS.
|
||||||
|
@ -124,7 +130,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
filter_class = DecimalFilter
|
filter_class = DecimalFilter
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
|
||||||
view = FilterFieldsRootView.as_view()
|
view = FilterFieldsRootView.as_view()
|
||||||
|
|
||||||
|
@ -171,7 +177,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
class BasicFilterFieldsRootView(generics.ListCreateAPIView):
|
class BasicFilterFieldsRootView(generics.ListCreateAPIView):
|
||||||
model = FilterableItem
|
model = FilterableItem
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
filter_backend = DecimalFilterBackend
|
filter_backends = (DecimalFilterBackend,)
|
||||||
|
|
||||||
view = BasicFilterFieldsRootView.as_view()
|
view = BasicFilterFieldsRootView.as_view()
|
||||||
|
|
|
@ -108,6 +108,48 @@ class ModelPermissionsIntegrationTests(TestCase):
|
||||||
response = instance_view(request, pk='2')
|
response = instance_view(request, pk='2')
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_options_permitted(self):
|
||||||
|
request = factory.options('/', content_type='application/json',
|
||||||
|
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',
|
||||||
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||||
|
response = instance_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()), ['PUT'])
|
||||||
|
|
||||||
|
def test_options_disallowed(self):
|
||||||
|
request = factory.options('/', content_type='application/json',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||||
|
response = instance_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()), ['PUT'])
|
||||||
|
|
||||||
|
|
||||||
class OwnerModel(models.Model):
|
class OwnerModel(models.Model):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100)
|
100
rest_framework/tests/test_relations.py
Normal file
100
rest_framework/tests/test_relations.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
"""
|
||||||
|
General tests for relational fields.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.tests.models import BlogPost
|
||||||
|
|
||||||
|
|
||||||
|
class NullModel(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FieldTests(TestCase):
|
||||||
|
def test_pk_related_field_with_empty_string(self):
|
||||||
|
"""
|
||||||
|
Regression test for #446
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/issues/446
|
||||||
|
"""
|
||||||
|
field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||||
|
|
||||||
|
def test_hyperlinked_related_field_with_empty_string(self):
|
||||||
|
field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||||
|
|
||||||
|
def test_slug_related_field_with_empty_string(self):
|
||||||
|
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||||
|
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestManyRelatedMixin(TestCase):
|
||||||
|
def test_missing_many_to_many_related_field(self):
|
||||||
|
'''
|
||||||
|
Regression test for #632
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/pull/632
|
||||||
|
'''
|
||||||
|
field = serializers.RelatedField(many=True, read_only=False)
|
||||||
|
|
||||||
|
into = {}
|
||||||
|
field.field_from_native({}, None, 'field_name', into)
|
||||||
|
self.assertEqual(into['field_name'], [])
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
class RelatedFieldSourceTests(TestCase):
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='get_blogposts_manager')
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='a.b.c')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
|
@ -4,6 +4,7 @@ from django.test.client import RequestFactory
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.compat import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.tests.models import (
|
from rest_framework.tests.models import (
|
||||||
|
BlogPost,
|
||||||
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
||||||
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
||||||
)
|
)
|
||||||
|
@ -16,6 +17,7 @@ def dummy_view(request, pk):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
|
||||||
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
||||||
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
||||||
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
|
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
|
||||||
|
@ -69,7 +71,7 @@ class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||||
|
|
||||||
class HyperlinkedManyToManyTests(TestCase):
|
class HyperlinkedManyToManyTests(TestCase):
|
||||||
urls = 'rest_framework.tests.relations_hyperlink'
|
urls = 'rest_framework.tests.test_relations_hyperlink'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for idx in range(1, 4):
|
for idx in range(1, 4):
|
||||||
|
@ -177,7 +179,7 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedForeignKeyTests(TestCase):
|
class HyperlinkedForeignKeyTests(TestCase):
|
||||||
urls = 'rest_framework.tests.relations_hyperlink'
|
urls = 'rest_framework.tests.test_relations_hyperlink'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
target = ForeignKeyTarget(name='target-1')
|
target = ForeignKeyTarget(name='target-1')
|
||||||
|
@ -305,7 +307,7 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedNullableForeignKeyTests(TestCase):
|
class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
urls = 'rest_framework.tests.relations_hyperlink'
|
urls = 'rest_framework.tests.test_relations_hyperlink'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
target = ForeignKeyTarget(name='target-1')
|
target = ForeignKeyTarget(name='target-1')
|
||||||
|
@ -433,7 +435,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedNullableOneToOneTests(TestCase):
|
class HyperlinkedNullableOneToOneTests(TestCase):
|
||||||
urls = 'rest_framework.tests.relations_hyperlink'
|
urls = 'rest_framework.tests.test_relations_hyperlink'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
target = OneToOneTarget(name='target-1')
|
target = OneToOneTarget(name='target-1')
|
||||||
|
@ -451,3 +453,72 @@ class HyperlinkedNullableOneToOneTests(TestCase):
|
||||||
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
class HyperlinkedRelatedFieldSourceTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.test_relations_hyperlink'
|
||||||
|
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='get_blogposts_manager',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='get_blogposts_queryset',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='a.b.c',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
|
@ -1,7 +1,11 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
from rest_framework.tests.models import (
|
||||||
|
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
||||||
|
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
|
||||||
|
)
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,6 +128,7 @@ class PKManyToManyTests(TestCase):
|
||||||
# Ensure source 4 is added, and everything else is as expected
|
# Ensure source 4 is added, and everything else is as expected
|
||||||
queryset = ManyToManySource.objects.all()
|
queryset = ManyToManySource.objects.all()
|
||||||
serializer = ManyToManySourceSerializer(queryset, many=True)
|
serializer = ManyToManySourceSerializer(queryset, many=True)
|
||||||
|
self.assertFalse(serializer.fields['targets'].read_only)
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': 'source-1', 'targets': [1]},
|
{'id': 1, 'name': 'source-1', 'targets': [1]},
|
||||||
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
|
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
|
||||||
|
@ -135,6 +140,7 @@ class PKManyToManyTests(TestCase):
|
||||||
def test_reverse_many_to_many_create(self):
|
def test_reverse_many_to_many_create(self):
|
||||||
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
|
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
|
||||||
serializer = ManyToManyTargetSerializer(data=data)
|
serializer = ManyToManyTargetSerializer(data=data)
|
||||||
|
self.assertFalse(serializer.fields['sources'].read_only)
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
self.assertEqual(serializer.data, data)
|
||||||
|
@ -421,3 +427,116 @@ class PKNullableOneToOneTests(TestCase):
|
||||||
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# The below models and tests ensure that serializer fields corresponding
|
||||||
|
# to a ManyToManyField field with a user-specified ``through`` model are
|
||||||
|
# set to read only
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughTarget(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThrough(models.Model):
|
||||||
|
source = models.ForeignKey('ManyToManyThroughSource')
|
||||||
|
target = models.ForeignKey(ManyToManyThroughTarget)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughSource(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
targets = models.ManyToManyField(ManyToManyThroughTarget,
|
||||||
|
related_name='sources',
|
||||||
|
through='ManyToManyThrough')
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ManyToManyThroughTarget
|
||||||
|
fields = ('id', 'name', 'sources')
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ManyToManyThroughSource
|
||||||
|
fields = ('id', 'name', 'targets')
|
||||||
|
|
||||||
|
|
||||||
|
class PKManyToManyThroughTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = ManyToManyThroughSource.objects.create(
|
||||||
|
name='through-source-1')
|
||||||
|
self.target = ManyToManyThroughTarget.objects.create(
|
||||||
|
name='through-target-1')
|
||||||
|
|
||||||
|
def test_many_to_many_create(self):
|
||||||
|
data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
|
||||||
|
serializer = ManyToManyThroughSourceSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.fields['targets'].read_only)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEqual(obj.name, 'source-2')
|
||||||
|
self.assertEqual(obj.targets.count(), 0)
|
||||||
|
|
||||||
|
def test_many_to_many_reverse_create(self):
|
||||||
|
data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
|
||||||
|
serializer = ManyToManyThroughTargetSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.fields['sources'].read_only)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
serializer.save()
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEqual(obj.name, 'target-2')
|
||||||
|
self.assertEqual(obj.sources.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedFieldSourceTests(TestCase):
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
|
@ -1,14 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import status, permissions
|
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
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
|
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
|
||||||
from rest_framework.parsers import YAMLParser, XMLParser
|
from rest_framework.parsers import YAMLParser, XMLParser
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.compat import StringIO
|
from rest_framework.compat import StringIO
|
||||||
|
@ -26,7 +30,7 @@ RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
|
||||||
|
|
||||||
|
|
||||||
expected_results = [
|
expected_results = [
|
||||||
((elem for elem in [1, 2, 3]), JSONRenderer, '[1, 2, 3]') # Generator
|
((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -129,12 +133,12 @@ class RendererEndToEndTests(TestCase):
|
||||||
End-to-end testing of renderers using an RendererMixin on a generic view.
|
End-to-end testing of renderers using an RendererMixin on a generic view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
urls = 'rest_framework.tests.renderers'
|
urls = 'rest_framework.tests.test_renderers'
|
||||||
|
|
||||||
def test_default_renderer_serializes_content(self):
|
def test_default_renderer_serializes_content(self):
|
||||||
"""If the Accept header is not set the default renderer should serialize the response."""
|
"""If the Accept header is not set the default renderer should serialize the response."""
|
||||||
resp = self.client.get('/')
|
resp = self.client.get('/')
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -142,13 +146,13 @@ class RendererEndToEndTests(TestCase):
|
||||||
"""No response must be included in HEAD requests."""
|
"""No response must be included in HEAD requests."""
|
||||||
resp = self.client.head('/')
|
resp = self.client.head('/')
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, six.b(''))
|
self.assertEqual(resp.content, six.b(''))
|
||||||
|
|
||||||
def test_default_renderer_serializes_content_on_accept_any(self):
|
def test_default_renderer_serializes_content_on_accept_any(self):
|
||||||
"""If the Accept header is set to */* the default renderer should serialize the response."""
|
"""If the Accept header is set to */* the default renderer should serialize the response."""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT='*/*')
|
resp = self.client.get('/', HTTP_ACCEPT='*/*')
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -156,7 +160,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
"""If the Accept header is set the specified renderer should serialize the response.
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
(In this case we check that works for the default renderer)"""
|
(In this case we check that works for the default renderer)"""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
|
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -164,7 +168,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
"""If the Accept header is set the specified renderer should serialize the response.
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
(In this case we check that works for a non-default renderer)"""
|
(In this case we check that works for a non-default renderer)"""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
|
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -175,7 +179,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
RendererB.media_type
|
RendererB.media_type
|
||||||
)
|
)
|
||||||
resp = self.client.get('/' + param)
|
resp = self.client.get('/' + param)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -192,7 +196,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
RendererB.format
|
RendererB.format
|
||||||
)
|
)
|
||||||
resp = self.client.get('/' + param)
|
resp = self.client.get('/' + param)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -200,7 +204,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
"""If a 'format' keyword arg is specified, the renderer with the matching
|
"""If a 'format' keyword arg is specified, the renderer with the matching
|
||||||
format attribute should serialize the response."""
|
format attribute should serialize the response."""
|
||||||
resp = self.client.get('/something.formatb')
|
resp = self.client.get('/something.formatb')
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -213,7 +217,7 @@ class RendererEndToEndTests(TestCase):
|
||||||
)
|
)
|
||||||
resp = self.client.get('/' + param,
|
resp = self.client.get('/' + param,
|
||||||
HTTP_ACCEPT=RendererB.media_type)
|
HTTP_ACCEPT=RendererB.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -235,6 +239,13 @@ class JSONRendererTests(TestCase):
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def test_render_lazy_strings(self):
|
||||||
|
"""
|
||||||
|
JSONRenderer should deal with lazy translated strings.
|
||||||
|
"""
|
||||||
|
ret = JSONRenderer().render(_('test'))
|
||||||
|
self.assertEqual(ret, b'"test"')
|
||||||
|
|
||||||
def test_without_content_type_args(self):
|
def test_without_content_type_args(self):
|
||||||
"""
|
"""
|
||||||
Test basic JSON rendering.
|
Test basic JSON rendering.
|
||||||
|
@ -243,7 +254,7 @@ class JSONRendererTests(TestCase):
|
||||||
renderer = JSONRenderer()
|
renderer = JSONRenderer()
|
||||||
content = renderer.render(obj, 'application/json')
|
content = renderer.render(obj, 'application/json')
|
||||||
# Fix failing test case which depends on version of JSON library.
|
# Fix failing test case which depends on version of JSON library.
|
||||||
self.assertEqual(content, _flat_repr)
|
self.assertEqual(content.decode('utf-8'), _flat_repr)
|
||||||
|
|
||||||
def test_with_content_type_args(self):
|
def test_with_content_type_args(self):
|
||||||
"""
|
"""
|
||||||
|
@ -252,7 +263,24 @@ class JSONRendererTests(TestCase):
|
||||||
obj = {'foo': ['bar', 'baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = JSONRenderer()
|
renderer = JSONRenderer()
|
||||||
content = renderer.render(obj, 'application/json; indent=2')
|
content = renderer.render(obj, 'application/json; indent=2')
|
||||||
self.assertEqual(strip_trailing_whitespace(content), _indented_repr)
|
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
|
||||||
|
|
||||||
|
def test_check_ascii(self):
|
||||||
|
obj = {'countries': ['United Kingdom', 'France', 'España']}
|
||||||
|
renderer = JSONRenderer()
|
||||||
|
content = renderer.render(obj, 'application/json')
|
||||||
|
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeJSONRendererTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests specific for the Unicode JSON Renderer
|
||||||
|
"""
|
||||||
|
def test_proper_encoding(self):
|
||||||
|
obj = {'countries': ['United Kingdom', 'France', 'España']}
|
||||||
|
renderer = UnicodeJSONRenderer()
|
||||||
|
content = renderer.render(obj, 'application/json')
|
||||||
|
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
class JSONPRendererTests(TestCase):
|
class JSONPRendererTests(TestCase):
|
||||||
|
@ -260,7 +288,7 @@ class JSONPRendererTests(TestCase):
|
||||||
Tests specific to the JSONP Renderer
|
Tests specific to the JSONP Renderer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
urls = 'rest_framework.tests.renderers'
|
urls = 'rest_framework.tests.test_renderers'
|
||||||
|
|
||||||
def test_without_callback_with_json_renderer(self):
|
def test_without_callback_with_json_renderer(self):
|
||||||
"""
|
"""
|
||||||
|
@ -269,7 +297,7 @@ class JSONPRendererTests(TestCase):
|
||||||
resp = self.client.get('/jsonp/jsonrenderer',
|
resp = self.client.get('/jsonp/jsonrenderer',
|
||||||
HTTP_ACCEPT='application/javascript')
|
HTTP_ACCEPT='application/javascript')
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/javascript')
|
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
|
||||||
self.assertEqual(resp.content,
|
self.assertEqual(resp.content,
|
||||||
('callback(%s);' % _flat_repr).encode('ascii'))
|
('callback(%s);' % _flat_repr).encode('ascii'))
|
||||||
|
|
||||||
|
@ -280,7 +308,7 @@ class JSONPRendererTests(TestCase):
|
||||||
resp = self.client.get('/jsonp/nojsonrenderer',
|
resp = self.client.get('/jsonp/nojsonrenderer',
|
||||||
HTTP_ACCEPT='application/javascript')
|
HTTP_ACCEPT='application/javascript')
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/javascript')
|
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
|
||||||
self.assertEqual(resp.content,
|
self.assertEqual(resp.content,
|
||||||
('callback(%s);' % _flat_repr).encode('ascii'))
|
('callback(%s);' % _flat_repr).encode('ascii'))
|
||||||
|
|
||||||
|
@ -292,7 +320,7 @@ class JSONPRendererTests(TestCase):
|
||||||
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
|
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
|
||||||
HTTP_ACCEPT='application/javascript')
|
HTTP_ACCEPT='application/javascript')
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/javascript')
|
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
|
||||||
self.assertEqual(resp.content,
|
self.assertEqual(resp.content,
|
||||||
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
|
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
|
||||||
|
|
||||||
|
@ -433,7 +461,7 @@ class CacheRenderTest(TestCase):
|
||||||
Tests specific to caching responses
|
Tests specific to caching responses
|
||||||
"""
|
"""
|
||||||
|
|
||||||
urls = 'rest_framework.tests.renderers'
|
urls = 'rest_framework.tests.test_renderers'
|
||||||
|
|
||||||
cache_key = 'just_a_cache_key'
|
cache_key = 'just_a_cache_key'
|
||||||
|
|
|
@ -254,7 +254,7 @@ urlpatterns = patterns('',
|
||||||
|
|
||||||
|
|
||||||
class TestContentParsingWithAuthentication(TestCase):
|
class TestContentParsingWithAuthentication(TestCase):
|
||||||
urls = 'rest_framework.tests.request'
|
urls = 'rest_framework.tests.test_request'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
|
@ -1,14 +1,18 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from rest_framework.tests.models import BasicModel, BasicModelSerializer
|
||||||
from rest_framework.compat import patterns, url, include
|
from rest_framework.compat import patterns, url, include
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import generics
|
||||||
|
from rest_framework import routers
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.renderers import (
|
from rest_framework.renderers import (
|
||||||
BaseRenderer,
|
BaseRenderer,
|
||||||
JSONRenderer,
|
JSONRenderer,
|
||||||
BrowsableAPIRenderer
|
BrowsableAPIRenderer
|
||||||
)
|
)
|
||||||
|
from rest_framework import viewsets
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
@ -21,6 +25,9 @@ class MockJsonRenderer(BaseRenderer):
|
||||||
media_type = 'application/json'
|
media_type = 'application/json'
|
||||||
|
|
||||||
|
|
||||||
|
class MockTextMediaRenderer(BaseRenderer):
|
||||||
|
media_type = 'text/html'
|
||||||
|
|
||||||
DUMMYSTATUS = status.HTTP_200_OK
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
DUMMYCONTENT = 'dummycontent'
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
|
||||||
|
@ -44,13 +51,26 @@ class RendererB(BaseRenderer):
|
||||||
return RENDERER_B_SERIALIZER(data)
|
return RENDERER_B_SERIALIZER(data)
|
||||||
|
|
||||||
|
|
||||||
|
class RendererC(RendererB):
|
||||||
|
media_type = 'mock/rendererc'
|
||||||
|
format = 'formatc'
|
||||||
|
charset = "rendererc"
|
||||||
|
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
renderer_classes = (RendererA, RendererB)
|
renderer_classes = (RendererA, RendererB, RendererC)
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
|
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSettingContentType(APIView):
|
||||||
|
renderer_classes = (RendererA, RendererB, RendererC)
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
|
||||||
|
|
||||||
|
|
||||||
class HTMLView(APIView):
|
class HTMLView(APIView):
|
||||||
renderer_classes = (BrowsableAPIRenderer, )
|
renderer_classes = (BrowsableAPIRenderer, )
|
||||||
|
|
||||||
|
@ -65,11 +85,29 @@ class HTMLView1(APIView):
|
||||||
return Response('text')
|
return Response('text')
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLNewModelViewSet(viewsets.ModelViewSet):
|
||||||
|
model = BasicModel
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLNewModelView(generics.ListCreateAPIView):
|
||||||
|
renderer_classes = (BrowsableAPIRenderer,)
|
||||||
|
permission_classes = []
|
||||||
|
serializer_class = BasicModelSerializer
|
||||||
|
model = BasicModel
|
||||||
|
|
||||||
|
|
||||||
|
new_model_viewset_router = routers.DefaultRouter()
|
||||||
|
new_model_viewset_router.register(r'', HTMLNewModelViewSet)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||||
|
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||||
url(r'^html$', HTMLView.as_view()),
|
url(r'^html$', HTMLView.as_view()),
|
||||||
url(r'^html1$', HTMLView1.as_view()),
|
url(r'^html1$', HTMLView1.as_view()),
|
||||||
|
url(r'^html_new_model$', HTMLNewModelView.as_view()),
|
||||||
|
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)),
|
||||||
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,12 +118,12 @@ class RendererIntegrationTests(TestCase):
|
||||||
End-to-end testing of renderers using an ResponseMixin on a generic view.
|
End-to-end testing of renderers using an ResponseMixin on a generic view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
urls = 'rest_framework.tests.response'
|
urls = 'rest_framework.tests.test_response'
|
||||||
|
|
||||||
def test_default_renderer_serializes_content(self):
|
def test_default_renderer_serializes_content(self):
|
||||||
"""If the Accept header is not set the default renderer should serialize the response."""
|
"""If the Accept header is not set the default renderer should serialize the response."""
|
||||||
resp = self.client.get('/')
|
resp = self.client.get('/')
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -93,13 +131,13 @@ class RendererIntegrationTests(TestCase):
|
||||||
"""No response must be included in HEAD requests."""
|
"""No response must be included in HEAD requests."""
|
||||||
resp = self.client.head('/')
|
resp = self.client.head('/')
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, six.b(''))
|
self.assertEqual(resp.content, six.b(''))
|
||||||
|
|
||||||
def test_default_renderer_serializes_content_on_accept_any(self):
|
def test_default_renderer_serializes_content_on_accept_any(self):
|
||||||
"""If the Accept header is set to */* the default renderer should serialize the response."""
|
"""If the Accept header is set to */* the default renderer should serialize the response."""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT='*/*')
|
resp = self.client.get('/', HTTP_ACCEPT='*/*')
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -107,7 +145,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
"""If the Accept header is set the specified renderer should serialize the response.
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
(In this case we check that works for the default renderer)"""
|
(In this case we check that works for the default renderer)"""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
|
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererA.media_type)
|
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -115,7 +153,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
"""If the Accept header is set the specified renderer should serialize the response.
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
(In this case we check that works for a non-default renderer)"""
|
(In this case we check that works for a non-default renderer)"""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
|
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -126,7 +164,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
RendererB.media_type
|
RendererB.media_type
|
||||||
)
|
)
|
||||||
resp = self.client.get('/' + param)
|
resp = self.client.get('/' + param)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -134,7 +172,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
"""If a 'format' query is specified, the renderer with the matching
|
"""If a 'format' query is specified, the renderer with the matching
|
||||||
format attribute should serialize the response."""
|
format attribute should serialize the response."""
|
||||||
resp = self.client.get('/?format=%s' % RendererB.format)
|
resp = self.client.get('/?format=%s' % RendererB.format)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -142,7 +180,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
"""If a 'format' keyword arg is specified, the renderer with the matching
|
"""If a 'format' keyword arg is specified, the renderer with the matching
|
||||||
format attribute should serialize the response."""
|
format attribute should serialize the response."""
|
||||||
resp = self.client.get('/something.formatb')
|
resp = self.client.get('/something.formatb')
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -151,7 +189,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
the renderer with the matching format attribute should serialize the response."""
|
the renderer with the matching format attribute should serialize the response."""
|
||||||
resp = self.client.get('/?format=%s' % RendererB.format,
|
resp = self.client.get('/?format=%s' % RendererB.format,
|
||||||
HTTP_ACCEPT=RendererB.media_type)
|
HTTP_ACCEPT=RendererB.media_type)
|
||||||
self.assertEqual(resp['Content-Type'], RendererB.media_type)
|
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@ -160,7 +198,7 @@ class Issue122Tests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests that covers #122.
|
Tests that covers #122.
|
||||||
"""
|
"""
|
||||||
urls = 'rest_framework.tests.response'
|
urls = 'rest_framework.tests.test_response'
|
||||||
|
|
||||||
def test_only_html_renderer(self):
|
def test_only_html_renderer(self):
|
||||||
"""
|
"""
|
||||||
|
@ -173,3 +211,68 @@ class Issue122Tests(TestCase):
|
||||||
Test if no infinite recursion occurs.
|
Test if no infinite recursion occurs.
|
||||||
"""
|
"""
|
||||||
self.client.get('/html1')
|
self.client.get('/html1')
|
||||||
|
|
||||||
|
|
||||||
|
class Issue467Tests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for #467
|
||||||
|
"""
|
||||||
|
|
||||||
|
urls = 'rest_framework.tests.test_response'
|
||||||
|
|
||||||
|
def test_form_has_label_and_help_text(self):
|
||||||
|
resp = self.client.get('/html_new_model')
|
||||||
|
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
self.assertContains(resp, 'Text comes here')
|
||||||
|
self.assertContains(resp, 'Text description.')
|
||||||
|
|
||||||
|
|
||||||
|
class Issue807Tests(TestCase):
|
||||||
|
"""
|
||||||
|
Covers #807
|
||||||
|
"""
|
||||||
|
|
||||||
|
urls = 'rest_framework.tests.test_response'
|
||||||
|
|
||||||
|
def test_does_not_append_charset_by_default(self):
|
||||||
|
"""
|
||||||
|
Renderers don't include a charset unless set explicitly.
|
||||||
|
"""
|
||||||
|
headers = {"HTTP_ACCEPT": RendererA.media_type}
|
||||||
|
resp = self.client.get('/', **headers)
|
||||||
|
expected = "{0}; charset={1}".format(RendererA.media_type, 'utf-8')
|
||||||
|
self.assertEqual(expected, resp['Content-Type'])
|
||||||
|
|
||||||
|
def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self):
|
||||||
|
"""
|
||||||
|
If renderer class has charset attribute declared, it gets appended
|
||||||
|
to Response's Content-Type
|
||||||
|
"""
|
||||||
|
headers = {"HTTP_ACCEPT": RendererC.media_type}
|
||||||
|
resp = self.client.get('/', **headers)
|
||||||
|
expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
|
||||||
|
self.assertEqual(expected, resp['Content-Type'])
|
||||||
|
|
||||||
|
def test_content_type_set_explictly_on_response(self):
|
||||||
|
"""
|
||||||
|
The content type may be set explictly on the response.
|
||||||
|
"""
|
||||||
|
headers = {"HTTP_ACCEPT": RendererC.media_type}
|
||||||
|
resp = self.client.get('/setbyview', **headers)
|
||||||
|
self.assertEqual('setbyview', resp['Content-Type'])
|
||||||
|
|
||||||
|
def test_viewset_label_help_text(self):
|
||||||
|
param = '?%s=%s' % (
|
||||||
|
api_settings.URL_ACCEPT_OVERRIDE,
|
||||||
|
'text/html'
|
||||||
|
)
|
||||||
|
resp = self.client.get('/html_new_model_viewset/' + param)
|
||||||
|
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
self.assertContains(resp, 'Text comes here')
|
||||||
|
self.assertContains(resp, 'Text description.')
|
||||||
|
|
||||||
|
def test_form_has_label_and_help_text(self):
|
||||||
|
resp = self.client.get('/html_new_model')
|
||||||
|
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
self.assertContains(resp, 'Text comes here')
|
||||||
|
self.assertContains(resp, 'Text description.')
|
|
@ -19,7 +19,7 @@ class ReverseTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for fully qualified URLs when using `reverse`.
|
Tests for fully qualified URLs when using `reverse`.
|
||||||
"""
|
"""
|
||||||
urls = 'rest_framework.tests.reverse'
|
urls = 'rest_framework.tests.test_reverse'
|
||||||
|
|
||||||
def test_reversed_urls_are_fully_qualified(self):
|
def test_reversed_urls_are_fully_qualified(self):
|
||||||
request = factory.get('/view')
|
request = factory.get('/view')
|
150
rest_framework/tests/test_routers.py
Normal file
150
rest_framework/tests/test_routers.py
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from rest_framework import serializers, viewsets
|
||||||
|
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
|
||||||
|
|
||||||
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
urlpatterns = patterns('',)
|
||||||
|
|
||||||
|
|
||||||
|
class BasicViewSet(viewsets.ViewSet):
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'list'})
|
||||||
|
|
||||||
|
@action()
|
||||||
|
def action1(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'action1'})
|
||||||
|
|
||||||
|
@action()
|
||||||
|
def action2(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'action2'})
|
||||||
|
|
||||||
|
@action(methods=['post', 'delete'])
|
||||||
|
def action3(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'action2'})
|
||||||
|
|
||||||
|
@link()
|
||||||
|
def link1(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'link1'})
|
||||||
|
|
||||||
|
@link()
|
||||||
|
def link2(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'link2'})
|
||||||
|
|
||||||
|
|
||||||
|
class TestSimpleRouter(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
|
||||||
|
def test_link_and_action_decorator(self):
|
||||||
|
routes = self.router.get_routes(BasicViewSet)
|
||||||
|
decorator_routes = routes[2:]
|
||||||
|
# Make sure all these endpoints exist and none have been clobbered
|
||||||
|
for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
|
||||||
|
route = decorator_routes[i]
|
||||||
|
# check url listing
|
||||||
|
self.assertEqual(route.url,
|
||||||
|
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
|
||||||
|
# check method to function mapping
|
||||||
|
if endpoint == 'action3':
|
||||||
|
methods_map = ['post', 'delete']
|
||||||
|
elif endpoint.startswith('action'):
|
||||||
|
methods_map = ['post']
|
||||||
|
else:
|
||||||
|
methods_map = ['get']
|
||||||
|
for method in methods_map:
|
||||||
|
self.assertEqual(route.mapping[method], endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterTestModel(models.Model):
|
||||||
|
uuid = models.CharField(max_length=20)
|
||||||
|
text = models.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomLookupFields(TestCase):
|
||||||
|
"""
|
||||||
|
Ensure that custom lookup fields are correctly routed.
|
||||||
|
"""
|
||||||
|
urls = 'rest_framework.tests.test_routers'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class NoteSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RouterTestModel
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
fields = ('url', 'uuid', 'text')
|
||||||
|
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = RouterTestModel.objects.all()
|
||||||
|
serializer_class = NoteSerializer
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
RouterTestModel.objects.create(uuid='123', text='foo bar')
|
||||||
|
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
|
||||||
|
from rest_framework.tests import test_routers
|
||||||
|
urls = getattr(test_routers, 'urlpatterns')
|
||||||
|
urls += patterns('',
|
||||||
|
url(r'^', include(self.router.urls)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_lookup_field_route(self):
|
||||||
|
detail_route = self.router.urls[-1]
|
||||||
|
detail_url_pattern = detail_route.regex.pattern
|
||||||
|
self.assertIn('<uuid>', detail_url_pattern)
|
||||||
|
|
||||||
|
def test_retrieve_lookup_field_list_view(self):
|
||||||
|
response = self.client.get('/notes/')
|
||||||
|
self.assertEqual(response.data,
|
||||||
|
[{
|
||||||
|
"url": "http://testserver/notes/123/",
|
||||||
|
"uuid": "123", "text": "foo bar"
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_retrieve_lookup_field_detail_view(self):
|
||||||
|
response = self.client.get('/notes/123/')
|
||||||
|
self.assertEqual(response.data,
|
||||||
|
{
|
||||||
|
"url": "http://testserver/notes/123/",
|
||||||
|
"uuid": "123", "text": "foo bar"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrailingSlash(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
self.urls = self.router.urls
|
||||||
|
|
||||||
|
def test_urls_have_trailing_slash_by_default(self):
|
||||||
|
expected = ['^notes/$', '^notes/(?P<pk>[^/]+)/$']
|
||||||
|
for idx in range(len(expected)):
|
||||||
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrailingSlash(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
|
||||||
|
self.router = SimpleRouter(trailing_slash=False)
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
self.urls = self.router.urls
|
||||||
|
|
||||||
|
def test_urls_can_have_trailing_slash_removed(self):
|
||||||
|
expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
|
||||||
|
for idx in range(len(expected)):
|
||||||
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
|
@ -1,10 +1,14 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.db import models
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers, fields, relations
|
||||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||||
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
|
||||||
|
from rest_framework.tests.models import BasicModelSerializer
|
||||||
import datetime
|
import datetime
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
|
@ -43,6 +47,17 @@ class CommentSerializer(serializers.Serializer):
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class NamesSerializer(serializers.Serializer):
|
||||||
|
first = serializers.CharField()
|
||||||
|
last = serializers.CharField(required=False, default='')
|
||||||
|
initials = serializers.CharField(required=False, default='')
|
||||||
|
|
||||||
|
|
||||||
|
class PersonIdentifierSerializer(serializers.Serializer):
|
||||||
|
ssn = serializers.CharField()
|
||||||
|
names = NamesSerializer(source='names', required=False)
|
||||||
|
|
||||||
|
|
||||||
class BookSerializer(serializers.ModelSerializer):
|
class BookSerializer(serializers.ModelSerializer):
|
||||||
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
||||||
|
|
||||||
|
@ -78,6 +93,29 @@ class PersonSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('age',)
|
read_only_fields = ('age',)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSerializer(serializers.Serializer):
|
||||||
|
info = serializers.Field()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
|
||||||
|
nested = NestedSerializer(source='*')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
|
||||||
|
|
||||||
|
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Testing for #652.
|
||||||
|
"""
|
||||||
|
info = serializers.Field(source='info')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
fields = ('name', 'age', 'info')
|
||||||
|
read_only_fields = ('age', 'info')
|
||||||
|
|
||||||
|
|
||||||
class AlbumsSerializer(serializers.ModelSerializer):
|
class AlbumsSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -91,11 +129,6 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
||||||
fields = ['some_integer']
|
fields = ['some_integer']
|
||||||
|
|
||||||
|
|
||||||
class BrokenModelSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
fields = ['some_field']
|
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(TestCase):
|
class BasicTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.comment = Comment(
|
self.comment = Comment(
|
||||||
|
@ -141,6 +174,42 @@ class BasicTests(TestCase):
|
||||||
self.assertFalse(serializer.object is expected)
|
self.assertFalse(serializer.object is expected)
|
||||||
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||||
|
|
||||||
|
def test_create_nested(self):
|
||||||
|
"""Test a serializer with nested data."""
|
||||||
|
names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
self.assertEqual(serializer.data['names'], names)
|
||||||
|
|
||||||
|
def test_create_partial_nested(self):
|
||||||
|
"""Test a serializer with nested data which has missing fields."""
|
||||||
|
names = {'first': 'John'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
expected_names = {'first': 'John', 'last': '', 'initials': ''}
|
||||||
|
data['names'] = expected_names
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is expected_names)
|
||||||
|
self.assertEqual(serializer.data['names'], expected_names)
|
||||||
|
|
||||||
|
def test_null_nested(self):
|
||||||
|
"""Test a serializer with a nonexistent nested field"""
|
||||||
|
data = {'ssn': '1234567890'}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
expected = {'ssn': '1234567890', 'names': None}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
serializer = CommentSerializer(self.comment, data=self.data)
|
serializer = CommentSerializer(self.comment, data=self.data)
|
||||||
expected = self.comment
|
expected = self.comment
|
||||||
|
@ -189,6 +258,12 @@ class BasicTests(TestCase):
|
||||||
# Assert age is unchanged (35)
|
# Assert age is unchanged (35)
|
||||||
self.assertEqual(instance.age, self.person_data['age'])
|
self.assertEqual(instance.age, self.person_data['age'])
|
||||||
|
|
||||||
|
def test_invalid_read_only_fields(self):
|
||||||
|
"""
|
||||||
|
Regression test for #652.
|
||||||
|
"""
|
||||||
|
self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
|
||||||
|
|
||||||
|
|
||||||
class DictStyleSerializer(serializers.Serializer):
|
class DictStyleSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
|
@ -344,19 +419,34 @@ class ValidationTests(TestCase):
|
||||||
Assert that a meaningful exception message is outputted when the model
|
Assert that a meaningful exception message is outputted when the model
|
||||||
field is missing (e.g. when mistyping ``model``).
|
field is missing (e.g. when mistyping ``model``).
|
||||||
"""
|
"""
|
||||||
|
class BrokenModelSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
fields = ['some_field']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serializer = BrokenModelSerializer()
|
BrokenModelSerializer()
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
|
self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
|
||||||
except:
|
except:
|
||||||
self.fail('Wrong exception type thrown.')
|
self.fail('Wrong exception type thrown.')
|
||||||
|
|
||||||
|
def test_writable_star_source_on_nested_serializer(self):
|
||||||
|
"""
|
||||||
|
Assert that a nested serializer instantiated with source='*' correctly
|
||||||
|
expands the data into the outer serializer.
|
||||||
|
"""
|
||||||
|
serializer = ModelSerializerWithNestedSerializer(data={
|
||||||
|
'name': 'marko',
|
||||||
|
'nested': {'info': 'hi'}},
|
||||||
|
)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
|
||||||
class CustomValidationTests(TestCase):
|
class CustomValidationTests(TestCase):
|
||||||
class CommentSerializerWithFieldValidator(CommentSerializer):
|
class CommentSerializerWithFieldValidator(CommentSerializer):
|
||||||
|
|
||||||
def validate_email(self, attrs, source):
|
def validate_email(self, attrs, source):
|
||||||
value = attrs[source]
|
attrs[source]
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def validate_content(self, attrs, source):
|
def validate_content(self, attrs, source):
|
||||||
|
@ -853,23 +943,6 @@ class RelatedTraversalTest(TestCase):
|
||||||
|
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_queryset_nested_traversal(self):
|
|
||||||
"""
|
|
||||||
Relational fields should be able to use methods as their source.
|
|
||||||
"""
|
|
||||||
BlogPost.objects.create(title='blah')
|
|
||||||
|
|
||||||
class QuerysetMethodSerializer(serializers.Serializer):
|
|
||||||
blogposts = serializers.RelatedField(many=True, source='get_all_blogposts')
|
|
||||||
|
|
||||||
class ClassWithQuerysetMethod(object):
|
|
||||||
def get_all_blogposts(self):
|
|
||||||
return BlogPost.objects
|
|
||||||
|
|
||||||
obj = ClassWithQuerysetMethod()
|
|
||||||
serializer = QuerysetMethodSerializer(obj)
|
|
||||||
self.assertEqual(serializer.data, {'blogposts': ['BlogPost object']})
|
|
||||||
|
|
||||||
|
|
||||||
class SerializerMethodFieldTests(TestCase):
|
class SerializerMethodFieldTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1000,6 +1073,130 @@ class SerializerPickleTests(TestCase):
|
||||||
repr(pickle.loads(pickle.dumps(data, 0)))
|
repr(pickle.loads(pickle.dumps(data, 0)))
|
||||||
|
|
||||||
|
|
||||||
|
# test for issue #725
|
||||||
|
class SeveralChoicesModel(models.Model):
|
||||||
|
color = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
|
||||||
|
blank=False
|
||||||
|
)
|
||||||
|
drink = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
|
||||||
|
blank=False,
|
||||||
|
default='beer'
|
||||||
|
)
|
||||||
|
os = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
music_genre = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
|
||||||
|
blank=True,
|
||||||
|
default='metal'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerChoiceFields(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SerializerChoiceFields, self).setUp()
|
||||||
|
|
||||||
|
class SeveralChoicesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SeveralChoicesModel
|
||||||
|
fields = ('color', 'drink', 'os', 'music_genre')
|
||||||
|
|
||||||
|
self.several_choices_serializer = SeveralChoicesSerializer
|
||||||
|
|
||||||
|
def test_choices_blank_false_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['color'].choices,
|
||||||
|
[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_false_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['drink'].choices,
|
||||||
|
[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['os'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['music_genre'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #675
|
||||||
|
class Ticket(models.Model):
|
||||||
|
assigned = models.ForeignKey(
|
||||||
|
Person, related_name='assigned_tickets')
|
||||||
|
reviewer = models.ForeignKey(
|
||||||
|
Person, blank=True, null=True, related_name='reviewed_tickets')
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerRelatedChoicesTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SerializerRelatedChoicesTest, self).setUp()
|
||||||
|
|
||||||
|
class RelatedChoicesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Ticket
|
||||||
|
fields = ('assigned', 'reviewer')
|
||||||
|
|
||||||
|
self.related_fields_serializer = RelatedChoicesSerializer
|
||||||
|
|
||||||
|
def test_empty_queryset_required(self):
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['assigned'].widget.choices],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_empty_queryset_not_required(self):
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['reviewer'].widget.choices],
|
||||||
|
[('', '---------')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_some_persons_required(self):
|
||||||
|
Person.objects.create(name="Lionel Messi")
|
||||||
|
Person.objects.create(name="Xavi Hernandez")
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['assigned'].widget.choices],
|
||||||
|
[(1, 'Person object - 1'), (2, 'Person object - 2')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_some_persons_not_required(self):
|
||||||
|
Person.objects.create(name="Lionel Messi")
|
||||||
|
Person.objects.create(name="Xavi Hernandez")
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['reviewer'].widget.choices],
|
||||||
|
[('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DepthTest(TestCase):
|
class DepthTest(TestCase):
|
||||||
def test_implicit_nesting(self):
|
def test_implicit_nesting(self):
|
||||||
|
|
||||||
|
@ -1125,3 +1322,312 @@ class DeserializeListTestCase(TestCase):
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
expected = [{}, {'email': ['This field is required.']}, {}]
|
expected = [{}, {'email': ['This field is required.']}, {}]
|
||||||
self.assertEqual(serializer.errors, expected)
|
self.assertEqual(serializer.errors, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# Test for issue 747
|
||||||
|
|
||||||
|
class LazyStringModel(object):
|
||||||
|
def __init__(self, lazystring):
|
||||||
|
self.lazystring = lazystring
|
||||||
|
|
||||||
|
|
||||||
|
class LazyStringSerializer(serializers.Serializer):
|
||||||
|
lazystring = serializers.Field()
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
if instance is not None:
|
||||||
|
instance.lazystring = attrs.get('lazystring', instance.lazystring)
|
||||||
|
return instance
|
||||||
|
return LazyStringModel(**attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class LazyStringsTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.model = LazyStringModel(lazystring=_('lazystring'))
|
||||||
|
|
||||||
|
def test_lazy_strings_are_translated(self):
|
||||||
|
serializer = LazyStringSerializer(self.model)
|
||||||
|
self.assertEqual(type(serializer.data['lazystring']),
|
||||||
|
type('lazystring'))
|
||||||
|
|
||||||
|
|
||||||
|
# Test for issue #467
|
||||||
|
|
||||||
|
class FieldLabelTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.serializer_class = BasicModelSerializer
|
||||||
|
|
||||||
|
def test_label_from_model(self):
|
||||||
|
"""
|
||||||
|
Validates that label and help_text are correctly copied from the model class.
|
||||||
|
"""
|
||||||
|
serializer = self.serializer_class()
|
||||||
|
text_field = serializer.fields['text']
|
||||||
|
|
||||||
|
self.assertEqual('Text comes here', text_field.label)
|
||||||
|
self.assertEqual('Text description.', text_field.help_text)
|
||||||
|
|
||||||
|
def test_field_ctor(self):
|
||||||
|
"""
|
||||||
|
This is check that ctor supports both label and help_text.
|
||||||
|
"""
|
||||||
|
self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label)
|
||||||
|
self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text)
|
||||||
|
self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class AMOAFModel(RESTFrameworkModel):
|
||||||
|
char_field = models.CharField(max_length=1024, blank=True)
|
||||||
|
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||||
|
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||||
|
email_field = models.EmailField(max_length=1024, blank=True)
|
||||||
|
file_field = models.FileField(max_length=1024, blank=True)
|
||||||
|
image_field = models.ImageField(max_length=1024, blank=True)
|
||||||
|
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||||
|
url_field = models.URLField(max_length=1024, blank=True)
|
||||||
|
|
||||||
|
class AMOAFSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = AMOAFModel
|
||||||
|
|
||||||
|
self.serializer_class = AMOAFSerializer
|
||||||
|
self.fields_attributes = {
|
||||||
|
'char_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'comma_separated_integer_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'decimal_field': [
|
||||||
|
('max_digits', 64),
|
||||||
|
('decimal_places', 32),
|
||||||
|
],
|
||||||
|
'email_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'file_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'image_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'slug_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'url_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def field_test(self, field):
|
||||||
|
serializer = self.serializer_class(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
for attribute in self.fields_attributes[field]:
|
||||||
|
self.assertEqual(
|
||||||
|
getattr(serializer.fields[field], attribute[0]),
|
||||||
|
attribute[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_char_field(self):
|
||||||
|
self.field_test('char_field')
|
||||||
|
|
||||||
|
def test_comma_separated_integer_field(self):
|
||||||
|
self.field_test('comma_separated_integer_field')
|
||||||
|
|
||||||
|
def test_decimal_field(self):
|
||||||
|
self.field_test('decimal_field')
|
||||||
|
|
||||||
|
def test_email_field(self):
|
||||||
|
self.field_test('email_field')
|
||||||
|
|
||||||
|
def test_file_field(self):
|
||||||
|
self.field_test('file_field')
|
||||||
|
|
||||||
|
def test_image_field(self):
|
||||||
|
self.field_test('image_field')
|
||||||
|
|
||||||
|
def test_slug_field(self):
|
||||||
|
self.field_test('slug_field')
|
||||||
|
|
||||||
|
def test_url_field(self):
|
||||||
|
self.field_test('url_field')
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class DVOAFModel(RESTFrameworkModel):
|
||||||
|
positive_integer_field = models.PositiveIntegerField(blank=True)
|
||||||
|
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
|
||||||
|
email_field = models.EmailField(blank=True)
|
||||||
|
file_field = models.FileField(blank=True)
|
||||||
|
image_field = models.ImageField(blank=True)
|
||||||
|
slug_field = models.SlugField(blank=True)
|
||||||
|
url_field = models.URLField(blank=True)
|
||||||
|
|
||||||
|
class DVOAFSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = DVOAFModel
|
||||||
|
|
||||||
|
self.serializer_class = DVOAFSerializer
|
||||||
|
self.fields_attributes = {
|
||||||
|
'positive_integer_field': [
|
||||||
|
('min_value', 0),
|
||||||
|
],
|
||||||
|
'positive_small_integer_field': [
|
||||||
|
('min_value', 0),
|
||||||
|
],
|
||||||
|
'email_field': [
|
||||||
|
('max_length', 75),
|
||||||
|
],
|
||||||
|
'file_field': [
|
||||||
|
('max_length', 100),
|
||||||
|
],
|
||||||
|
'image_field': [
|
||||||
|
('max_length', 100),
|
||||||
|
],
|
||||||
|
'slug_field': [
|
||||||
|
('max_length', 50),
|
||||||
|
],
|
||||||
|
'url_field': [
|
||||||
|
('max_length', 200),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def field_test(self, field):
|
||||||
|
serializer = self.serializer_class(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
for attribute in self.fields_attributes[field]:
|
||||||
|
self.assertEqual(
|
||||||
|
getattr(serializer.fields[field], attribute[0]),
|
||||||
|
attribute[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_positive_integer_field(self):
|
||||||
|
self.field_test('positive_integer_field')
|
||||||
|
|
||||||
|
def test_positive_small_integer_field(self):
|
||||||
|
self.field_test('positive_small_integer_field')
|
||||||
|
|
||||||
|
def test_email_field(self):
|
||||||
|
self.field_test('email_field')
|
||||||
|
|
||||||
|
def test_file_field(self):
|
||||||
|
self.field_test('file_field')
|
||||||
|
|
||||||
|
def test_image_field(self):
|
||||||
|
self.field_test('image_field')
|
||||||
|
|
||||||
|
def test_slug_field(self):
|
||||||
|
self.field_test('slug_field')
|
||||||
|
|
||||||
|
def test_url_field(self):
|
||||||
|
self.field_test('url_field')
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataSerializer(serializers.Serializer):
|
||||||
|
field1 = serializers.CharField(3, required=True)
|
||||||
|
field2 = serializers.CharField(10, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataSerializerTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.serializer = MetadataSerializer()
|
||||||
|
|
||||||
|
def test_serializer_metadata(self):
|
||||||
|
metadata = self.serializer.metadata()
|
||||||
|
expected = {
|
||||||
|
'field1': {
|
||||||
|
'required': True,
|
||||||
|
'max_length': 3,
|
||||||
|
'type': 'string',
|
||||||
|
'read_only': False
|
||||||
|
},
|
||||||
|
'field2': {
|
||||||
|
'required': False,
|
||||||
|
'max_length': 10,
|
||||||
|
'type': 'string',
|
||||||
|
'read_only': False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(expected, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
### Regression test for #840
|
||||||
|
|
||||||
|
class SimpleModel(models.Model):
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleModelSerializer(serializers.ModelSerializer):
|
||||||
|
text = serializers.CharField()
|
||||||
|
other = serializers.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SimpleModel
|
||||||
|
|
||||||
|
def validate_other(self, attrs, source):
|
||||||
|
del attrs['other']
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class FieldValidationRemovingAttr(TestCase):
|
||||||
|
def test_removing_non_model_field_in_validation(self):
|
||||||
|
"""
|
||||||
|
Removing an attr during field valiation should ensure that it is not
|
||||||
|
passed through when restoring the object.
|
||||||
|
|
||||||
|
This allows additional non-model fields to be supported.
|
||||||
|
|
||||||
|
Regression test for #840.
|
||||||
|
"""
|
||||||
|
serializer = SimpleModelSerializer(data={'text': 'foo', 'other': 'bar'})
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
serializer.save()
|
||||||
|
self.assertEqual(serializer.object.text, 'foo')
|
||||||
|
|
||||||
|
|
||||||
|
### Regression test for #878
|
||||||
|
|
||||||
|
class SimpleTargetModel(models.Model):
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePKSourceModelSerializer(serializers.Serializer):
|
||||||
|
targets = serializers.PrimaryKeyRelatedField(queryset=SimpleTargetModel.objects.all(), many=True)
|
||||||
|
text = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleSlugSourceModelSerializer(serializers.Serializer):
|
||||||
|
targets = serializers.SlugRelatedField(queryset=SimpleTargetModel.objects.all(), many=True, slug_field='pk')
|
||||||
|
text = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerSupportsManyRelationships(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
SimpleTargetModel.objects.create(text='foo')
|
||||||
|
SimpleTargetModel.objects.create(text='bar')
|
||||||
|
|
||||||
|
def test_serializer_supports_pk_many_relationships(self):
|
||||||
|
"""
|
||||||
|
Regression test for #878.
|
||||||
|
|
||||||
|
Note that pk behavior has a different code path to usual cases,
|
||||||
|
for performance reasons.
|
||||||
|
"""
|
||||||
|
serializer = SimplePKSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
|
||||||
|
|
||||||
|
def test_serializer_supports_slug_many_relationships(self):
|
||||||
|
"""
|
||||||
|
Regression test for #878.
|
||||||
|
"""
|
||||||
|
serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
|
|
@ -36,7 +36,7 @@ class MockView_MinuteThrottling(APIView):
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingTests(TestCase):
|
class ThrottlingTests(TestCase):
|
||||||
urls = 'rest_framework.tests.throttling'
|
urls = 'rest_framework.tests.test_throttling'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
|
@ -63,3 +63,25 @@ class TestPreSaveValidationExclusions(TestCase):
|
||||||
# does not have `blank=True`, so this serializer should not validate.
|
# does not have `blank=True`, so this serializer should not validate.
|
||||||
serializer = ShouldValidateModelSerializer(data={'renamed': ''})
|
serializer = ShouldValidateModelSerializer(data={'renamed': ''})
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
self.assertEqual(serializer.is_valid(), False)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationSerializer(serializers.Serializer):
|
||||||
|
foo = serializers.CharField()
|
||||||
|
|
||||||
|
def validate_foo(self, attrs, source):
|
||||||
|
raise serializers.ValidationError("foo invalid")
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
raise serializers.ValidationError("serializer invalid")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAvoidValidation(TestCase):
|
||||||
|
"""
|
||||||
|
If serializer was initialized with invalid data (None or non dict-like), it
|
||||||
|
should avoid validation layer (validate_<field> and validate methods)
|
||||||
|
"""
|
||||||
|
def test_serializer_errors_has_only_invalid_data_error(self):
|
||||||
|
serializer = ValidationSerializer(data='invalid data')
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
self.assertDictEqual(serializer.errors,
|
||||||
|
{'non_field_errors': ['Invalid data']})
|
|
@ -1,12 +1,15 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
import copy
|
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
# http://djangosnippets.org/snippets/1011/
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management import call_command
|
|
||||||
from django.db.models import loading
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
NO_SETTING = ('!', None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsManager(object):
|
|
||||||
"""
|
|
||||||
A class which can modify some Django settings temporarily for a
|
|
||||||
test and then revert them to their original values later.
|
|
||||||
|
|
||||||
Automatically handles resyncing the DB if INSTALLED_APPS is
|
|
||||||
modified.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self._original_settings = {}
|
|
||||||
|
|
||||||
def set(self, **kwargs):
|
|
||||||
for k, v in kwargs.iteritems():
|
|
||||||
self._original_settings.setdefault(k, getattr(settings, k,
|
|
||||||
NO_SETTING))
|
|
||||||
setattr(settings, k, v)
|
|
||||||
if 'INSTALLED_APPS' in kwargs:
|
|
||||||
self.syncdb()
|
|
||||||
|
|
||||||
def syncdb(self):
|
|
||||||
loading.cache.loaded = False
|
|
||||||
call_command('syncdb', verbosity=0)
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
for k, v in self._original_settings.iteritems():
|
|
||||||
if v == NO_SETTING:
|
|
||||||
delattr(settings, k)
|
|
||||||
else:
|
|
||||||
setattr(settings, k, v)
|
|
||||||
if 'INSTALLED_APPS' in self._original_settings:
|
|
||||||
self.syncdb()
|
|
||||||
self._original_settings = {}
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsTestCase(TestCase):
|
|
||||||
"""
|
|
||||||
A subclass of the Django TestCase with a settings_manager
|
|
||||||
attribute which is an instance of TestSettingsManager.
|
|
||||||
|
|
||||||
Comes with a tearDown() method that calls
|
|
||||||
self.settings_manager.revert().
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(SettingsTestCase, self).__init__(*args, **kwargs)
|
|
||||||
self.settings_manager = TestSettingsManager()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.settings_manager.revert()
|
|
||||||
|
|
||||||
|
|
||||||
class TestModelsTestCase(SettingsTestCase):
|
|
||||||
def setUp(self, *args, **kwargs):
|
|
||||||
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
|
|
||||||
self.settings_manager.set(INSTALLED_APPS=installed_apps)
|
|
|
@ -4,11 +4,13 @@ runner to pick up the tests. Yowzers.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
modules = [filename.rsplit('.', 1)[0]
|
modules = [filename.rsplit('.', 1)[0]
|
||||||
for filename in os.listdir(os.path.dirname(__file__))
|
for filename in os.listdir(os.path.dirname(__file__))
|
||||||
if filename.endswith('.py') and not filename.startswith('_')]
|
if filename.endswith('.py') and not filename.startswith('_')]
|
||||||
__test__ = dict()
|
__test__ = dict()
|
||||||
|
|
||||||
for module in modules:
|
if django.VERSION < (1, 6):
|
||||||
exec("from rest_framework.tests.%s import *" % module)
|
for module in modules:
|
||||||
|
exec("from rest_framework.tests.%s import *" % module)
|
||||||
|
|
|
@ -3,7 +3,8 @@ Helper classes for parsers.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from rest_framework.compat import timezone
|
from django.utils.functional import Promise
|
||||||
|
from rest_framework.compat import timezone, force_text
|
||||||
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
|
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
@ -19,7 +20,9 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
# For Date Time string spec, see ECMA 262
|
# For Date Time string spec, see ECMA 262
|
||||||
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||||
if isinstance(o, datetime.datetime):
|
if isinstance(o, Promise):
|
||||||
|
return force_text(o)
|
||||||
|
elif isinstance(o, datetime.datetime):
|
||||||
r = o.isoformat()
|
r = o.isoformat()
|
||||||
if o.microsecond:
|
if o.microsecond:
|
||||||
r = r[:23] + r[26:]
|
r = r[:23] + r[26:]
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
Provides an APIView class that is the base of all views in REST framework.
|
Provides an APIView class that is the base of all views in REST framework.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from rest_framework import status, exceptions
|
from rest_framework import status, exceptions
|
||||||
from rest_framework.compat import View
|
from rest_framework.compat import View
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||||
|
|
||||||
|
@ -51,21 +53,6 @@ class APIView(View):
|
||||||
'Vary': 'Accept'
|
'Vary': 'Accept'
|
||||||
}
|
}
|
||||||
|
|
||||||
def metadata(self, request):
|
|
||||||
return {
|
|
||||||
'name': get_view_name(self.__class__),
|
|
||||||
'description': get_view_description(self.__class__),
|
|
||||||
'renders': [renderer.media_type for renderer in self.renderer_classes],
|
|
||||||
'parses': [parser.media_type for parser in self.parser_classes],
|
|
||||||
}
|
|
||||||
# TODO: Add 'fields', from serializer info, if it exists.
|
|
||||||
# serializer = self.get_serializer()
|
|
||||||
# if serializer is not None:
|
|
||||||
# field_name_types = {}
|
|
||||||
# for name, field in form.fields.iteritems():
|
|
||||||
# field_name_types[name] = field.__class__.__name__
|
|
||||||
# content['fields'] = field_name_types
|
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
If `request.method` does not correspond to a handler method,
|
If `request.method` does not correspond to a handler method,
|
||||||
|
@ -348,3 +335,15 @@ class APIView(View):
|
||||||
a less useful default implementation.
|
a less useful default implementation.
|
||||||
"""
|
"""
|
||||||
return Response(self.metadata(request), status=status.HTTP_200_OK)
|
return Response(self.metadata(request), status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def metadata(self, request):
|
||||||
|
"""
|
||||||
|
Return a dictionary of metadata about the view.
|
||||||
|
Used to return responses for OPTIONS requests.
|
||||||
|
"""
|
||||||
|
ret = SortedDict()
|
||||||
|
ret['name'] = get_view_name(self.__class__)
|
||||||
|
ret['description'] = get_view_description(self.__class__)
|
||||||
|
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||||
|
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||||
|
return ret
|
||||||
|
|
|
@ -108,10 +108,18 @@ class ViewSet(ViewSetMixin, views.APIView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
|
||||||
|
"""
|
||||||
|
The GenericViewSet class does not provide any actions by default,
|
||||||
|
but does include the base set of generic view behavior, such as
|
||||||
|
the `get_object` and `get_queryset` methods.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
|
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
ViewSetMixin,
|
GenericViewSet):
|
||||||
generics.GenericAPIView):
|
|
||||||
"""
|
"""
|
||||||
A viewset that provides default `list()` and `retrieve()` actions.
|
A viewset that provides default `list()` and `retrieve()` actions.
|
||||||
"""
|
"""
|
||||||
|
@ -123,8 +131,7 @@ class ModelViewSet(mixins.CreateModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
ViewSetMixin,
|
GenericViewSet):
|
||||||
generics.GenericAPIView):
|
|
||||||
"""
|
"""
|
||||||
A viewset that provides default `create()`, `retrieve()`, `update()`,
|
A viewset that provides default `create()`, `retrieve()`, `update()`,
|
||||||
`partial_update()`, `destroy()` and `list()` actions.
|
`partial_update()`, `destroy()` and `list()` actions.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user