mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-23 15:54:16 +03:00
Merge with master
This commit is contained in:
commit
8953a60196
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ html/
|
|||
coverage/
|
||||
build/
|
||||
dist/
|
||||
rest_framework.egg-info/
|
||||
*.egg-info/
|
||||
MANIFEST
|
||||
|
||||
!.gitignore
|
||||
|
|
|
@ -11,6 +11,8 @@ env:
|
|||
|
||||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install -r requirements.txt --use-mirrors
|
||||
- pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
|
79
README.md
79
README.md
|
@ -6,11 +6,23 @@
|
|||
|
||||
[![build-status-image]][travis]
|
||||
|
||||
---
|
||||
|
||||
**Full documentation for REST framework is available on [http://django-rest-framework.org][docs].**
|
||||
|
||||
Note that this is the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub.
|
||||
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
This branch is the redesign of Django REST framework. It is a work in progress.
|
||||
Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views.
|
||||
|
||||
For more information, check out [the documentation][docs], in particular, the tutorial is recommended as the best place to get an overview of the redesign.
|
||||
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
||||
|
||||
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
|
||||
|
||||
There is also a sandbox API you can use for testing purposes, [available here][sandbox].
|
||||
|
||||
# Requirements
|
||||
|
||||
|
@ -24,21 +36,15 @@ For more information, check out [the documentation][docs], in particular, the tu
|
|||
|
||||
# Installation
|
||||
|
||||
**Leaving these instructions in for the moment, they'll be valid once this becomes the master version**
|
||||
|
||||
Install using `pip`...
|
||||
|
||||
pip install rest_framework
|
||||
pip install djangorestframework
|
||||
|
||||
...or clone the project from github.
|
||||
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Quickstart
|
||||
|
||||
**TODO**
|
||||
|
||||
# Development
|
||||
|
||||
To build the docs.
|
||||
|
@ -51,8 +57,54 @@ To run the tests.
|
|||
|
||||
# Changelog
|
||||
|
||||
## 2.1.2
|
||||
|
||||
**Date**: 9th Nov 2012
|
||||
|
||||
* **Filtering support.**
|
||||
* Bugfix: Support creation of objects with reverse M2M relations.
|
||||
|
||||
## 2.1.1
|
||||
|
||||
**Date**: 7th Nov 2012
|
||||
|
||||
* Support use of HTML exception templates. Eg. `403.html`
|
||||
* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
|
||||
* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs.
|
||||
* Bugfix: Make textareas same width as other fields in browsable API.
|
||||
* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
**Date**: 5th Nov 2012
|
||||
|
||||
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
|
||||
|
||||
* **Serializer `instance` and `data` keyword args have their position swapped.**
|
||||
* `queryset` argument is now optional on writable model fields.
|
||||
* Hyperlinked related fields optionally take `slug_field` and `slug_field_kwarg` arguments.
|
||||
* Support Django's cache framework.
|
||||
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
|
||||
* Bugfixes (Support choice field in Browseable API)
|
||||
|
||||
## 2.0.2
|
||||
|
||||
**Date**: 2nd Nov 2012
|
||||
|
||||
* Fix issues with pk related fields in the browsable API.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
**Date**: 1st Nov 2012
|
||||
|
||||
* Add support for relational fields in the browsable API.
|
||||
* Added SlugRelatedField and ManySlugRelatedField.
|
||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
**Date**: 30th Oct 2012
|
||||
|
||||
* Redesign of core components.
|
||||
* Fix **all of the things**.
|
||||
|
||||
|
@ -82,9 +134,14 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
[docs]: http://tomchristie.github.com/django-rest-framework/
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
||||
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||
|
||||
[docs]: http://django-rest-framework.org/
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
||||
|
|
|
@ -35,8 +35,8 @@ The value of `request.user` and `request.auth` for unauthenticated requests can
|
|||
The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION': (
|
||||
'rest_framework.authentication.UserBasicAuthentication',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ The default authentication schemes may be set globally, using the `DEFAULT_AUTHE
|
|||
You can also set the authentication scheme on a per-view basis, using the `APIView` class based views.
|
||||
|
||||
class ExampleView(APIView):
|
||||
authentication_classes = (SessionAuthentication, UserBasicAuthentication)
|
||||
authentication_classes = (SessionAuthentication, BasicAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
@ -56,8 +56,8 @@ You can also set the authentication scheme on a per-view basis, using the `APIVi
|
|||
|
||||
Or, if you're using the `@api_view` decorator with function based views.
|
||||
|
||||
@api_view(('GET',)),
|
||||
@authentication_classes((SessionAuthentication, UserBasicAuthentication))
|
||||
@api_view(['GET'])
|
||||
@authentication_classes((SessionAuthentication, BasicAuthentication))
|
||||
@permissions_classes((IsAuthenticated,))
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
|
@ -89,7 +89,7 @@ This authentication scheme uses [HTTP Basic Authentication][basicauth], signed a
|
|||
|
||||
If successfully authenticated, `BasicAuthentication` provides the following credentials.
|
||||
|
||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
||||
* `request.user` will be a Django `User` instance.
|
||||
* `request.auth` will be `None`.
|
||||
|
||||
Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
|
||||
|
@ -111,13 +111,13 @@ You'll also need to create tokens for your users.
|
|||
token = Token.objects.create(user=...)
|
||||
print token.key
|
||||
|
||||
For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace seperating the two strings. For example:
|
||||
For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example:
|
||||
|
||||
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
|
||||
|
||||
If successfully authenticated, `TokenAuthentication` provides the following credentials.
|
||||
|
||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
||||
* `request.user` will be a Django `User` instance.
|
||||
* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
|
||||
|
||||
Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
|
||||
|
@ -132,7 +132,7 @@ This authentication scheme uses the [OAuth 2.0][oauth] protocol to authenticate
|
|||
|
||||
If successfully authenticated, `OAuth2Authentication` provides the following credentials.
|
||||
|
||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
||||
* `request.user` will be a Django `User` instance.
|
||||
* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
|
||||
|
||||
**TODO**: Note type of response (401 vs 403)
|
||||
|
@ -145,7 +145,7 @@ This authentication scheme uses Django's default session backend for authenticat
|
|||
|
||||
If successfully authenticated, `SessionAuthentication` provides the following credentials.
|
||||
|
||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
||||
* `request.user` will be a Django `User` instance.
|
||||
* `request.auth` will be `None`.
|
||||
|
||||
Unauthenticated responses that are denied permission will result in an `HTTP 403 Forbidden` response.
|
||||
|
|
|
@ -7,3 +7,60 @@
|
|||
> — [RFC 2616][cite], Fielding et al.
|
||||
|
||||
[cite]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html
|
||||
|
||||
Content negotiation is the process of selecting one of multiple possible representations to return to a client, based on client or server preferences.
|
||||
|
||||
## Determining the accepted renderer
|
||||
|
||||
REST framework uses a simple style of content negotiation to determine which media type should be returned to a client, based on the available renderers, the priorities of each of those renderers, and the client's `Accept:` header. The style used is partly client-driven, and partly server-driven.
|
||||
|
||||
1. More specific media types are given preference to less specific media types.
|
||||
2. If multiple media types have the same specificity, then preference is given to based on the ordering of the renderers configured for the given view.
|
||||
|
||||
For example, given the following `Accept` header:
|
||||
|
||||
application/json; indent=4, application/json, application/yaml, text/html, */*
|
||||
|
||||
The priorities for each of the given media types would be:
|
||||
|
||||
* `application/json; indent=4`
|
||||
* `application/json`, `application/yaml` and `text/html`
|
||||
* `*/*`
|
||||
|
||||
If the requested view was only configured with renderers for `YAML` and `HTML`, then REST framework would select whichever renderer was listed first in the `renderer_classes` list or `DEFAULT_RENDERER_CLASSES` setting.
|
||||
|
||||
For more information on the `HTTP Accept` header, see [RFC 2616][accept-header]
|
||||
|
||||
---
|
||||
|
||||
**Note**: "q" values are not taken into account by REST framework when determining preference. The use of "q" values negatively impacts caching, and in the author's opinion they are an unnecessary and overcomplicated approach to content negotiation.
|
||||
|
||||
This is a valid approach as the HTTP spec deliberately underspecifies how a server should weight server-based preferences against client-based preferences.
|
||||
|
||||
---
|
||||
|
||||
# Custom content negotiation
|
||||
|
||||
It's unlikely that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`.
|
||||
|
||||
REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods.
|
||||
|
||||
## Example
|
||||
|
||||
The following is a custom content negotiation class which ignores the client
|
||||
request when selecting the appropriate parser or renderer.
|
||||
|
||||
class IgnoreClientContentNegotiation(BaseContentNegotiation):
|
||||
def select_parser(self, request, parsers):
|
||||
"""
|
||||
Select the first parser in the `.parser_classes` list.
|
||||
"""
|
||||
return parsers[0]
|
||||
|
||||
def select_renderer(self, request, renderers, format_suffix):
|
||||
"""
|
||||
Select the first renderer in the `.renderer_classes` list.
|
||||
"""
|
||||
return renderers[0]
|
||||
|
||||
[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
|
|
|
@ -25,7 +25,7 @@ For example, the following request:
|
|||
DELETE http://api.example.com/foo/bar HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
Might recieve an error response indicating that the `DELETE` method is not allowed on that resource:
|
||||
Might receive an error response indicating that the `DELETE` method is not allowed on that resource:
|
||||
|
||||
HTTP/1.1 405 Method Not Allowed
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
@ -33,6 +33,10 @@ Might recieve an error response indicating that the `DELETE` method is not allow
|
|||
|
||||
{"detail": "Method 'DELETE' not allowed."}
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## APIException
|
||||
|
||||
**Signature:** `APIException(detail=None)`
|
||||
|
@ -98,4 +102,4 @@ Raised when an incoming request fails the throttling checks.
|
|||
By default this exception results in a response with the HTTP status code "429 Too Many Requests".
|
||||
|
||||
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
|
||||
[authentication]: authentication.md
|
||||
[authentication]: authentication.md
|
||||
|
|
|
@ -12,6 +12,51 @@ Serializer fields handle converting between primative values and internal dataty
|
|||
|
||||
**Note:** The serializer fields are declared in fields.py, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
### `source`
|
||||
|
||||
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `Field(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `Field(source='user.email')`.
|
||||
|
||||
The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations. (See the implementation of the `PaginationSerializer` class for an example.)
|
||||
|
||||
Defaults to the name of the field.
|
||||
|
||||
### `read_only`
|
||||
|
||||
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance dureing deserialization.
|
||||
|
||||
Defaults to `False`
|
||||
|
||||
### `required`
|
||||
|
||||
Normally an error will be raised if a field is not supplied during deserialization.
|
||||
Set to false if this field is not required to be present during deserialization.
|
||||
|
||||
Defaults to `True`.
|
||||
|
||||
### `default`
|
||||
|
||||
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behaviour is to not populate the attribute at all.
|
||||
|
||||
### `validators`
|
||||
|
||||
A list of Django validators that should be used to validate deserialized values.
|
||||
|
||||
### `error_messages`
|
||||
|
||||
A dictionary of error codes to error messages.
|
||||
|
||||
### `widget`
|
||||
|
||||
Used only if rendering the field to HTML.
|
||||
This argument sets the widget that should be used to render the field.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Generic Fields
|
||||
|
@ -42,7 +87,7 @@ A serializer definition that looked like this:
|
|||
class Meta:
|
||||
fields = ('url', 'owner', 'name', 'expired')
|
||||
|
||||
Would produced output similar to:
|
||||
Would produce output similar to:
|
||||
|
||||
{
|
||||
'url': 'http://example.com/api/accounts/3/',
|
||||
|
@ -51,7 +96,7 @@ Would produced output similar to:
|
|||
'expired': True
|
||||
}
|
||||
|
||||
Be default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when neccesary.
|
||||
By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary.
|
||||
|
||||
You can customize this behaviour by overriding the `.to_native(self, value)` method.
|
||||
|
||||
|
@ -73,34 +118,52 @@ These fields represent basic datatypes, and support both reading and writing val
|
|||
|
||||
## BooleanField
|
||||
|
||||
A Boolean representation, corresponds to `django.db.models.fields.BooleanField`.
|
||||
A Boolean representation.
|
||||
|
||||
Corresponds to `django.db.models.fields.BooleanField`.
|
||||
|
||||
## CharField
|
||||
|
||||
A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`, corresponds to `django.db.models.fields.CharField`
|
||||
A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`.
|
||||
|
||||
Corresponds to `django.db.models.fields.CharField`
|
||||
or `django.db.models.fields.TextField`.
|
||||
|
||||
**Signature:** `CharField([max_length=<Integer>[, min_length=<Integer>]])`
|
||||
**Signature:** `CharField(max_length=None, min_length=None)`
|
||||
|
||||
## ChoiceField
|
||||
|
||||
A field that can accept a value out of a limited set of choices.
|
||||
|
||||
## EmailField
|
||||
|
||||
A text representation, validates the text to be a valid e-mail adress. Corresponds to `django.db.models.fields.EmailField`
|
||||
A text representation, validates the text to be a valid e-mail address.
|
||||
|
||||
Corresponds to `django.db.models.fields.EmailField`
|
||||
|
||||
## DateField
|
||||
|
||||
A date representation. Corresponds to `django.db.models.fields.DateField`
|
||||
A date representation.
|
||||
|
||||
Corresponds to `django.db.models.fields.DateField`
|
||||
|
||||
## DateTimeField
|
||||
|
||||
A date and time representation. Corresponds to `django.db.models.fields.DateTimeField`
|
||||
A date and time representation.
|
||||
|
||||
Corresponds to `django.db.models.fields.DateTimeField`
|
||||
|
||||
## IntegerField
|
||||
|
||||
An integer representation. Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`
|
||||
An integer representation.
|
||||
|
||||
Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`
|
||||
|
||||
## FloatField
|
||||
|
||||
A floating point representation. Corresponds to `django.db.models.fields.FloatField`.
|
||||
A floating point representation.
|
||||
|
||||
Corresponds to `django.db.models.fields.FloatField`.
|
||||
|
||||
---
|
||||
|
||||
|
@ -165,33 +228,61 @@ And a model serializer defined like this:
|
|||
model = Bookmark
|
||||
exclude = ('id',)
|
||||
|
||||
The an example output format for a Bookmark instance would be:
|
||||
Then an example output format for a Bookmark instance would be:
|
||||
|
||||
{
|
||||
'tags': [u'django', u'python'],
|
||||
'url': u'https://www.djangoproject.com/'
|
||||
}
|
||||
|
||||
## PrimaryKeyRelatedField
|
||||
## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
|
||||
|
||||
As with `RelatedField` field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
|
||||
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
|
||||
|
||||
`PrimaryKeyRelatedField` will represent the target of the field using it's primary key.
|
||||
By default these fields are read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
Be default, `PrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `readonly` flag.
|
||||
**Arguments**:
|
||||
|
||||
## ManyPrimaryKeyRelatedField
|
||||
* `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`.
|
||||
|
||||
As with `RelatedField` field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
|
||||
## SlugRelatedField / ManySlugRelatedField
|
||||
|
||||
`PrimaryKeyRelatedField` will represent the target of the field using their primary key.
|
||||
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
|
||||
|
||||
Be default, `ManyPrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `readonly` flag.
|
||||
By default these fields read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
## HyperlinkedRelatedField
|
||||
**Arguments**:
|
||||
|
||||
## ManyHyperlinkedRelatedField
|
||||
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
|
||||
* `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`.
|
||||
|
||||
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
|
||||
|
||||
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
|
||||
|
||||
By default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `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`.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `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`.
|
||||
|
||||
## HyperLinkedIdentityField
|
||||
|
||||
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
|
||||
|
||||
This field is always read-only.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `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`.
|
||||
|
||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||
|
|
179
docs/api-guide/filtering.md
Normal file
179
docs/api-guide/filtering.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
<a class="github" href="filters.py"></a>
|
||||
|
||||
# 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.
|
||||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
The default behavior of REST framework's generic list views is to return the entire queryset for a model manager. Often you will want your API to restrict the items that are returned by the queryset.
|
||||
|
||||
The simplest way to filter the queryset of any view that subclasses `MultipleObjectAPIView` is to override the `.get_queryset()` method.
|
||||
|
||||
Overriding this method allows you to customize the queryset returned by the view in a number of different ways.
|
||||
|
||||
## Filtering against the current user
|
||||
|
||||
You might want to filter the queryset to ensure that only results relevant to the currently authenticated user making the request are returned.
|
||||
|
||||
You can do so by filtering based on the value of `request.user`.
|
||||
|
||||
For example:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list of all the purchases
|
||||
for the currently authenticated user.
|
||||
"""
|
||||
user = self.request.user
|
||||
return Purchase.objects.filter(purchaser=user)
|
||||
|
||||
|
||||
## Filtering against the URL
|
||||
|
||||
Another style of filtering might involve restricting the queryset based on some part of the URL.
|
||||
|
||||
For example if your URL config contained an entry like this:
|
||||
|
||||
url('^purchases/(?P<username>.+)/$', PurchaseList.as_view()),
|
||||
|
||||
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list of all the purchases for
|
||||
the user as determined by the username portion of the URL.
|
||||
"""
|
||||
username = self.kwargs['username']
|
||||
return Purchase.objects.filter(purchaser__username=username)
|
||||
|
||||
## Filtering against query parameters
|
||||
|
||||
A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url.
|
||||
|
||||
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Optionally restricts the returned purchases to a given user,
|
||||
by filtering against a `username` query parameter in the URL.
|
||||
"""
|
||||
queryset = Purchase.objects.all()
|
||||
username = self.request.QUERY_PARAMS.get('username', None):
|
||||
if username is not None:
|
||||
queryset = queryset.filter(purchaser__username=username)
|
||||
return queryset
|
||||
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
|
||||
|
||||
To use REST framework's default filtering backend, first install `django-filter`.
|
||||
|
||||
pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
|
||||
|
||||
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
|
||||
}
|
||||
|
||||
**Note**: The currently supported version of `django-filter` is the `master` branch. A PyPI release is expected to be coming soon.
|
||||
|
||||
## Specifying filter fields
|
||||
|
||||
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, listing the set of fields you wish to filter against.
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
filter_fields = ('category', 'in_stock')
|
||||
|
||||
This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as:
|
||||
|
||||
http://example.com/api/products?category=clothing&in_stock=True
|
||||
|
||||
## Specifying a FilterSet
|
||||
|
||||
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
min_price = django_filters.NumberFilter(lookup_type='gte')
|
||||
max_price = django_filters.NumberFilter(lookup_type='lte')
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
filter_class = ProductFilter
|
||||
|
||||
Which will allow you to make requests such as:
|
||||
|
||||
http://example.com/api/products?category=clothing&max_price=10.00
|
||||
|
||||
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
|
||||
|
||||
---
|
||||
|
||||
**Hints & Tips**
|
||||
|
||||
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'FILTER_BACKEND'` 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].)
|
||||
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
||||
|
||||
---
|
||||
|
||||
## 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()
|
||||
---
|
||||
|
||||
# Custom generic filtering
|
||||
|
||||
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
|
||||
|
||||
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 `'FILTER_BACKEND'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class.
|
||||
|
||||
For example:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'FILTER_BACKEND': 'custom_filters.CustomFilterBackend'
|
||||
}
|
||||
|
||||
[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-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
||||
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
|
@ -7,5 +7,55 @@ used all the time.
|
|||
>
|
||||
> — Roy Fielding, [REST discuss mailing list][cite]
|
||||
|
||||
[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857
|
||||
A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation.
|
||||
|
||||
Adding format-suffix patterns to each individual entry in the URLconf for your API is error-prone and non-DRY, so REST framework provides a shortcut to adding these patterns to your URLConf.
|
||||
|
||||
## format_suffix_patterns
|
||||
|
||||
**Signature**: format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None)
|
||||
|
||||
Returns a URL pattern list which includes format suffix patterns appended to each of the URL patterns provided.
|
||||
|
||||
Arguments:
|
||||
|
||||
* **urlpatterns**: Required. A URL pattern list.
|
||||
* **suffix_required**: Optional. A boolean indicating if suffixes in the URLs should be optional or mandatory. Defaults to `False`, meaning that suffixes are optional by default.
|
||||
* **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used.
|
||||
|
||||
Example:
|
||||
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
urlpatterns = patterns('blog.views',
|
||||
url(r'^/$', 'api_root'),
|
||||
url(r'^comment/$', 'comment_root'),
|
||||
url(r'^comment/(?P<pk>[0-9]+)/$', 'comment_instance')
|
||||
)
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
|
||||
|
||||
When using `format_suffix_patterns`, you must make sure to add the `'format'` keyword argument to the corresponding views. For example.
|
||||
|
||||
@api_view(('GET',))
|
||||
def api_root(request, format=None):
|
||||
# do stuff...
|
||||
|
||||
The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` setting.
|
||||
|
||||
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
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]
|
||||
|
||||
The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern.
|
||||
|
||||
[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857
|
||||
[cite2]: http://tech.groups.yahoo.com/group/rest-discuss/message/14844
|
|
@ -30,7 +30,7 @@ For more complex cases you might also want to override various methods on the vi
|
|||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
def get_paginate_by(self):
|
||||
def get_paginate_by(self, queryset):
|
||||
"""
|
||||
Use smaller pagination for HTML representations.
|
||||
"""
|
||||
|
@ -49,21 +49,21 @@ For very simple cases you might want to pass through any class attributes using
|
|||
|
||||
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.
|
||||
|
||||
## CreateAPIView
|
||||
|
||||
Used for **create-only** endpoints.
|
||||
|
||||
Provides `post` method handlers.
|
||||
|
||||
Extends: [GenericAPIView], [CreateModelMixin]
|
||||
|
||||
## ListAPIView
|
||||
|
||||
Used for **read-only** endpoints to represent a **collection of model instances**.
|
||||
|
||||
Provides a `get` method handler.
|
||||
|
||||
Extends: [MultipleObjectBaseAPIView], [ListModelMixin]
|
||||
|
||||
## ListCreateAPIView
|
||||
|
||||
Used for **read-write** endpoints to represent a **collection of model instances**.
|
||||
|
||||
Provides `get` and `post` method handlers.
|
||||
|
||||
Extends: [MultipleObjectBaseAPIView], [ListModelMixin], [CreateModelMixin]
|
||||
Extends: [MultipleObjectAPIView], [ListModelMixin]
|
||||
|
||||
## RetrieveAPIView
|
||||
|
||||
|
@ -71,7 +71,31 @@ Used for **read-only** endpoints to represent a **single model instance**.
|
|||
|
||||
Provides a `get` method handler.
|
||||
|
||||
Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin]
|
||||
Extends: [SingleObjectAPIView], [RetrieveModelMixin]
|
||||
|
||||
## DestroyAPIView
|
||||
|
||||
Used for **delete-only** endpoints for a **single model instance**.
|
||||
|
||||
Provides a `delete` method handler.
|
||||
|
||||
Extends: [SingleObjectAPIView], [DestroyModelMixin]
|
||||
|
||||
## UpdateAPIView
|
||||
|
||||
Used for **update-only** endpoints for a **single model instance**.
|
||||
|
||||
Provides a `put` method handler.
|
||||
|
||||
Extends: [SingleObjectAPIView], [UpdateModelMixin]
|
||||
|
||||
## ListCreateAPIView
|
||||
|
||||
Used for **read-write** endpoints to represent a **collection of model instances**.
|
||||
|
||||
Provides `get` and `post` method handlers.
|
||||
|
||||
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin]
|
||||
|
||||
## RetrieveDestroyAPIView
|
||||
|
||||
|
@ -79,15 +103,15 @@ Used for **read or delete** endpoints to represent a **single model instance**.
|
|||
|
||||
Provides `get` and `delete` method handlers.
|
||||
|
||||
Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [DestroyModelMixin]
|
||||
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]
|
||||
|
||||
## RetrieveUpdateDestroyAPIView
|
||||
|
||||
Used for **read-write** endpoints to represent a **single model instance**.
|
||||
Used for **read-write-delete** endpoints to represent a **single model instance**.
|
||||
|
||||
Provides `get`, `put` and `delete` method handlers.
|
||||
|
||||
Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
|
||||
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
|
||||
|
||||
---
|
||||
|
||||
|
@ -95,17 +119,17 @@ Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [UpdateModelMixin], [D
|
|||
|
||||
Each of the generic views provided is built by combining one of the base views below, with one or more mixin classes.
|
||||
|
||||
## BaseAPIView
|
||||
## GenericAPIView
|
||||
|
||||
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
|
||||
|
||||
## MultipleObjectBaseAPIView
|
||||
## MultipleObjectAPIView
|
||||
|
||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
|
||||
|
||||
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
|
||||
|
||||
## SingleObjectBaseAPIView
|
||||
## SingleObjectAPIView
|
||||
|
||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
|
||||
|
||||
|
@ -121,31 +145,31 @@ The mixin classes provide the actions that are used to provide the basic view be
|
|||
|
||||
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
||||
|
||||
Should be mixed in with [MultipleObjectBaseAPIView].
|
||||
Should be mixed in with [MultipleObjectAPIView].
|
||||
|
||||
## CreateModelMixin
|
||||
|
||||
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
||||
|
||||
Should be mixed in with any [BaseAPIView].
|
||||
Should be mixed in with any [GenericAPIView].
|
||||
|
||||
## RetrieveModelMixin
|
||||
|
||||
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
||||
|
||||
Should be mixed in with [SingleObjectBaseAPIView].
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
## UpdateModelMixin
|
||||
|
||||
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
||||
|
||||
Should be mixed in with [SingleObjectBaseAPIView].
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
## DestroyModelMixin
|
||||
|
||||
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
||||
|
||||
Should be mixed in with [SingleObjectBaseAPIView].
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||
[MultipleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/
|
||||
|
@ -153,9 +177,9 @@ Should be mixed in with [SingleObjectBaseAPIView].
|
|||
[multiple-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.list/MultipleObjectMixin/
|
||||
[single-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.detail/SingleObjectMixin/
|
||||
|
||||
[BaseAPIView]: #baseapiview
|
||||
[SingleObjectBaseAPIView]: #singleobjectbaseapiview
|
||||
[MultipleObjectBaseAPIView]: #multipleobjectbaseapiview
|
||||
[GenericAPIView]: #genericapiview
|
||||
[SingleObjectAPIView]: #singleobjectapiview
|
||||
[MultipleObjectAPIView]: #multipleobjectapiview
|
||||
[ListModelMixin]: #listmodelmixin
|
||||
[CreateModelMixin]: #createmodelmixin
|
||||
[RetrieveModelMixin]: #retrievemodelmixin
|
||||
|
|
|
@ -100,12 +100,16 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
|
|||
|
||||
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
||||
|
||||
## Custom pagination serializers
|
||||
---
|
||||
|
||||
# Custom pagination serializers
|
||||
|
||||
To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return.
|
||||
|
||||
You can also override the name used for the object list field, by setting the `results_field` attribute, which defaults to `'results'`.
|
||||
|
||||
## Example
|
||||
|
||||
For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
|
||||
|
||||
class LinksSerializer(serializers.Serializer):
|
||||
|
|
|
@ -8,7 +8,7 @@ sending more complex data than simple forms
|
|||
>
|
||||
> — Malcom Tredinnick, [Django developers group][cite]
|
||||
|
||||
REST framework includes a number of built in Parser classes, that allow you to accept requests with various media types. There is also support for defining your own custom parsers, which gives you the flexiblity to design the media types that your API accepts.
|
||||
REST framework includes a number of built in Parser classes, that allow you to accept requests with various media types. There is also support for defining your own custom parsers, which gives you the flexibility to design the media types that your API accepts.
|
||||
|
||||
## How the parser is determined
|
||||
|
||||
|
@ -16,10 +16,10 @@ The set of valid parsers for a view is always defined as a list of classes. Whe
|
|||
|
||||
## Setting the parsers
|
||||
|
||||
The default set of parsers may be set globally, using the `DEFAULT_PARSERS` setting. For example, the following settings would allow requests with `YAML` content.
|
||||
The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PARSERS': (
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.YAMLParser',
|
||||
)
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ You can also set the renderers used for an individual view, using the `APIView`
|
|||
|
||||
Or, if you're using the `@api_view` decorator with function based views.
|
||||
|
||||
@api_view(('POST',)),
|
||||
@api_view(['POST'])
|
||||
@parser_classes((YAMLParser,))
|
||||
def example_view(request, format=None):
|
||||
"""
|
||||
|
@ -65,7 +65,7 @@ Parses `YAML` request content.
|
|||
|
||||
Parses REST framework's default style of `XML` request content.
|
||||
|
||||
Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
|
||||
Note that the `XML` markup language is typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
|
||||
|
||||
If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
|
||||
|
||||
|
@ -91,19 +91,27 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
|
|||
|
||||
# Custom parsers
|
||||
|
||||
To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse_stream(self, stream, parser_context)` method.
|
||||
To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method.
|
||||
|
||||
The method should return the data that will be used to populate the `request.DATA` property.
|
||||
|
||||
The arguments passed to `.parse_stream()` are:
|
||||
The arguments passed to `.parse()` are:
|
||||
|
||||
### stream
|
||||
|
||||
A stream-like object representing the body of the request.
|
||||
|
||||
### media_type
|
||||
|
||||
Optional. If provided, this is the media type of the incoming request content.
|
||||
|
||||
Depending on the request's `Content-Type:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"text/plain; charset=utf-8"`.
|
||||
|
||||
### parser_context
|
||||
|
||||
If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. By default it includes the keys `'upload_handlers'` and `'meta'`, which contain the values of the `request.upload_handlers` and `request.meta` properties.
|
||||
Optional. If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content.
|
||||
|
||||
By default this will include the following keys: `view`, `request`, `args`, `kwargs`.
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -116,7 +124,7 @@ The following is an example plaintext parser that will populate the `request.DAT
|
|||
|
||||
media_type = 'text/plain'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Simply return a string representing the body of the request.
|
||||
"""
|
||||
|
@ -124,7 +132,7 @@ The following is an example plaintext parser that will populate the `request.DAT
|
|||
|
||||
## Uploading file content
|
||||
|
||||
If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse_stream()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property.
|
||||
If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -132,8 +140,9 @@ For example:
|
|||
"""
|
||||
A naive raw file upload parser.
|
||||
"""
|
||||
media_type = '*/*' # Accept anything
|
||||
|
||||
def parse_stream(self, stream, parser_context):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
content = stream.read()
|
||||
name = 'example.dat'
|
||||
content_type = 'application/octet-stream'
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
> — [Apple Developer Documentation][cite]
|
||||
|
||||
Together with [authentication] and [throttling], permissions determine wheter a request should be granted or denied access.
|
||||
Together with [authentication] and [throttling], permissions determine whether a request should be granted or denied access.
|
||||
|
||||
Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
|
||||
|
||||
|
@ -25,14 +25,20 @@ Object level permissions are run by REST framework's generic views when `.get_ob
|
|||
|
||||
## Setting the permission policy
|
||||
|
||||
The default permission policy may be set globally, using the `DEFAULT_PERMISSIONS` setting. For example.
|
||||
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSIONS': (
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
)
|
||||
}
|
||||
|
||||
If not specified, this setting defaults to allowing unrestricted access:
|
||||
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.AllowAny',
|
||||
)
|
||||
|
||||
You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
|
||||
|
||||
class ExampleView(APIView):
|
||||
|
@ -54,8 +60,16 @@ Or, if you're using the `@api_view` decorator with function based views.
|
|||
}
|
||||
return Response(content)
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## AllowAny
|
||||
|
||||
The `AllowAny` permission class will allow unrestricted access, **regardless of if the request was authenticated or unauthenticated**.
|
||||
|
||||
This permission is not strictly required, since you can achieve the same result by using an empty list or tuple for the permissions setting, but you may find it useful to specify this class because it makes the intention explicit.
|
||||
|
||||
## IsAuthenticated
|
||||
|
||||
The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise.
|
||||
|
@ -64,7 +78,7 @@ This permission is suitable if you want your API to only be accessible to regist
|
|||
|
||||
## IsAdminUser
|
||||
|
||||
The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff`is `True` in which case permission will be allowed.
|
||||
The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff` is `True` in which case permission will be allowed.
|
||||
|
||||
This permission is suitable is you want your API to only be accessible to a subset of trusted administrators.
|
||||
|
||||
|
@ -88,12 +102,15 @@ To use custom model permissions, override `DjangoModelPermissions` and set the `
|
|||
|
||||
The `DjangoModelPermissions` class also supports object-level permissions. Third-party authorization backends such as [django-guardian][guardian] that provide object-level permissions should work just fine with `DjangoModelPermissions` without any custom configuration required.
|
||||
|
||||
---
|
||||
|
||||
# Custom permissions
|
||||
|
||||
To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, view, obj=None)` method.
|
||||
|
||||
The method should return `True` if the request should be granted access, and `False` otherwise.
|
||||
|
||||
|
||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||
[authentication]: authentication.md
|
||||
[throttling]: throttling.md
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
REST framework includes a number of built in Renderer classes, that allow you to return responses with various media types. There is also support for defining your own custom renderers, which gives you the flexiblity to design your own media types.
|
||||
REST framework includes a number of built in Renderer classes, that allow you to return responses with various media types. There is also support for defining your own custom renderers, which gives you the flexibility to design your own media types.
|
||||
|
||||
## How the renderer is determined
|
||||
|
||||
|
@ -18,10 +18,10 @@ For more information see the documentation on [content negotation][conneg].
|
|||
|
||||
## Setting the renderers
|
||||
|
||||
The default set of renderers may be set globally, using the `DEFAULT_RENDERERS` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API.
|
||||
The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERERS': (
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.YAMLRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
)
|
||||
|
@ -42,7 +42,7 @@ You can also set the renderers used for an individual view, using the `APIView`
|
|||
|
||||
Or, if you're using the `@api_view` decorator with function based views.
|
||||
|
||||
@api_view(('GET',)),
|
||||
@api_view(['GET'])
|
||||
@renderer_classes((JSONRenderer, JSONPRenderer))
|
||||
def user_count_view(request, format=None):
|
||||
"""
|
||||
|
@ -106,12 +106,12 @@ If you are considering using `XML` for your API, you may want to consider implem
|
|||
|
||||
**.format**: `'.xml'`
|
||||
|
||||
## HTMLRenderer
|
||||
## TemplateHTMLRenderer
|
||||
|
||||
Renders data to HTML, using Django's standard template rendering.
|
||||
Unlike other renderers, the data passed to the `Response` does not need to be serialized. Also, unlike other renderers, you may want to include a `template_name` argument when creating the `Response`.
|
||||
|
||||
The HTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context.
|
||||
The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context.
|
||||
|
||||
The template name is determined by (in order of preference):
|
||||
|
||||
|
@ -119,27 +119,49 @@ The template name is determined by (in order of preference):
|
|||
2. An explicit `.template_name` attribute set on this class.
|
||||
3. The return result of calling `view.get_template_names()`.
|
||||
|
||||
An example of a view that uses `HTMLRenderer`:
|
||||
An example of a view that uses `TemplateHTMLRenderer`:
|
||||
|
||||
class UserInstance(generics.RetrieveUserAPIView):
|
||||
"""
|
||||
A view that returns a templated HTML representations of a given user.
|
||||
"""
|
||||
model = Users
|
||||
renderer_classes = (HTMLRenderer,)
|
||||
renderer_classes = (TemplateHTMLRenderer,)
|
||||
|
||||
def get(self, request, *args, **kwargs)
|
||||
self.object = self.get_object()
|
||||
return Response(self.object, template_name='user_detail.html')
|
||||
return Response({'user': self.object}, template_name='user_detail.html')
|
||||
|
||||
You can use `HTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
|
||||
You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
|
||||
|
||||
If you're building websites that use `HTMLRenderer` along with other renderer classes, you should consider listing `HTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers.
|
||||
If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers.
|
||||
|
||||
**.media_type**: `text/html`
|
||||
|
||||
**.format**: `'.html'`
|
||||
|
||||
See also: `StaticHTMLRenderer`
|
||||
|
||||
## StaticHTMLRenderer
|
||||
|
||||
A simple renderer that simply returns pre-rendered HTML. Unlike other renderers, the data passed to the response object should be a string representing the content to be returned.
|
||||
|
||||
An example of a view that uses `TemplateHTMLRenderer`:
|
||||
|
||||
@api_view(('GET',))
|
||||
@renderer_classes((StaticHTMLRenderer,))
|
||||
def simple_html_view(request):
|
||||
data = '<html><body><h1>Hello, world</h1></body></html>'
|
||||
return Response(data)
|
||||
|
||||
You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
|
||||
|
||||
**.media_type**: `text/html`
|
||||
|
||||
**.format**: `'.html'`
|
||||
|
||||
See also: `TemplateHTMLRenderer`
|
||||
|
||||
## BrowsableAPIRenderer
|
||||
|
||||
Renders data into HTML for the Browseable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
|
||||
|
@ -162,11 +184,14 @@ The request data, as set by the `Response()` instantiation.
|
|||
|
||||
### `media_type=None`
|
||||
|
||||
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"`.
|
||||
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"`.
|
||||
|
||||
### `renderer_context=None`
|
||||
|
||||
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`.
|
||||
|
||||
## Example
|
||||
|
@ -204,7 +229,7 @@ In some cases you might want your view to use different serialization styles dep
|
|||
For example:
|
||||
|
||||
@api_view(('GET',))
|
||||
@renderer_classes((HTMLRenderer, JSONRenderer))
|
||||
@renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
def list_users(request):
|
||||
"""
|
||||
A view that can return JSON or HTML representations
|
||||
|
@ -212,9 +237,9 @@ For example:
|
|||
"""
|
||||
queryset = Users.objects.filter(active=True)
|
||||
|
||||
if request.accepted_media_type == 'text/html':
|
||||
if request.accepted_renderer.format == 'html':
|
||||
# TemplateHTMLRenderer takes a context dict,
|
||||
# and additionally requiresa 'template_name'.
|
||||
# and additionally requires a 'template_name'.
|
||||
# It does not require serialization.
|
||||
data = {'users': queryset}
|
||||
return Response(data, template_name='list_users.html')
|
||||
|
@ -226,12 +251,27 @@ For example:
|
|||
|
||||
## 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 neeed 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.
|
||||
|
||||
In [the words of Roy Fielding][quote], "A REST API should spend almost all of its descriptive effort in defining the media type(s) used for representing resources and driving application state, or in defining extended relation names and/or hypertext-enabled mark-up for existing standard media types.".
|
||||
|
||||
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
|
||||
|
||||
## HTML error views
|
||||
|
||||
Typically a renderer will behave the same regardless of if it's dealing with a regular response, or with a response caused by an exception being raised, such as an `Http404` or `PermissionDenied` exception, or a subclass of `APIException`.
|
||||
|
||||
If you're using either the `TemplateHTMLRenderer` or the `StaticHTMLRenderer` and an exception is raised, the behavior is slightly different, and mirrors [Django's default handling of error views][django-error-views].
|
||||
|
||||
Exceptions raised and handled by an HTML renderer will attempt to render using one of the following methods, by order of precedence.
|
||||
|
||||
* Load and render a template named `{status_code}.html`.
|
||||
* Load and render a template named `api_exception.html`.
|
||||
* Render the HTTP status code and text, for example "404 Not Found".
|
||||
|
||||
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
||||
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||
[conneg]: content-negotiation.md
|
||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||
|
@ -240,3 +280,4 @@ For good examples of custom media types, see GitHub's use of a custom [applicati
|
|||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
||||
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
|
|
@ -25,19 +25,19 @@ For more details see the [parsers documentation].
|
|||
|
||||
## .FILES
|
||||
|
||||
`request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing that is used for `request.DATA`.
|
||||
`request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing is used for `request.DATA`.
|
||||
|
||||
For more details see the [parsers documentation].
|
||||
|
||||
## .QUERY_PARAMS
|
||||
|
||||
`request.QUERY_PARAMS` is a more correcly named synonym for `request.GET`.
|
||||
`request.QUERY_PARAMS` is a more correctly named synonym for `request.GET`.
|
||||
|
||||
For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead of the usual `request.GET`, as *any* HTTP method type may include query parameters.
|
||||
|
||||
## .parsers
|
||||
|
||||
The `APIView` class or `@api_view` decorator will ensure that this property is automatically to a list of `Parser` instances, based on the `parser_classes` set on the view or based on the `DEFAULT_PARSERS` setting.
|
||||
The `APIView` class or `@api_view` decorator will ensure that this property is automatically set to a list of `Parser` instances, based on the `parser_classes` set on the view or based on the `DEFAULT_PARSER_CLASSES` setting.
|
||||
|
||||
You won't typically need to access this property.
|
||||
|
||||
|
@ -51,7 +51,7 @@ If a client sends a request with a content-type that cannot be parsed then a `Un
|
|||
|
||||
# Authentication
|
||||
|
||||
REST framework provides flexbile, per-request authentication, that gives you the abilty to:
|
||||
REST framework provides flexible, per-request authentication, that gives you the ability to:
|
||||
|
||||
* Use different authentication policies for different parts of your API.
|
||||
* Support the use of multiple authentication policies.
|
||||
|
@ -75,7 +75,7 @@ For more details see the [authentication documentation].
|
|||
|
||||
## .authenticators
|
||||
|
||||
The `APIView` class or `@api_view` decorator will ensure that this property is automatically to a list of `Authentication` instances, based on the `authentication_classes` set on the view or based on the `DEFAULT_AUTHENTICATORS` setting.
|
||||
The `APIView` class or `@api_view` decorator will ensure that this property is automatically set to a list of `Authentication` instances, based on the `authentication_classes` set on the view or based on the `DEFAULT_AUTHENTICATORS` setting.
|
||||
|
||||
You won't typically need to access this property.
|
||||
|
||||
|
@ -83,7 +83,7 @@ You won't typically need to access this property.
|
|||
|
||||
# Browser enhancements
|
||||
|
||||
REST framework supports a few browser enhancments such as browser-based `PUT` and `DELETE` forms.
|
||||
REST framework supports a few browser enhancements such as browser-based `PUT` and `DELETE` forms.
|
||||
|
||||
## .method
|
||||
|
||||
|
@ -125,4 +125,4 @@ Note that due to implementation reasons the `Request` class does not inherit fro
|
|||
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
|
||||
[parsers documentation]: parsers.md
|
||||
[authentication documentation]: authentication.md
|
||||
[browser enhancements documentation]: ../topics/browser-enhancements.md
|
||||
[browser enhancements documentation]: ../topics/browser-enhancements.md
|
||||
|
|
|
@ -86,7 +86,7 @@ The `Response` class extends `SimpleTemplateResponse`, and all the usual attribu
|
|||
|
||||
**Signature:** `.render()`
|
||||
|
||||
As with any other `TemplateResponse`, this methd is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance.
|
||||
As with any other `TemplateResponse`, this method is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance.
|
||||
|
||||
You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
> — Roy Fielding, [Architectural Styles and the Design of Network-based Software Architectures][cite]
|
||||
|
||||
As a rule, it's probably better practice to return absolute URIs from you Web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`.
|
||||
As a rule, it's probably better practice to return absolute URIs from your Web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`.
|
||||
|
||||
The advantages of doing so are:
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ The first part of serializer class defines the fields that get serialized/deseri
|
|||
|
||||
We can now use `CommentSerializer` to serialize a comment, or list of comments. Again, using the `Serializer` class looks a lot like using a `Form` class.
|
||||
|
||||
serializer = CommentSerializer(instance=comment)
|
||||
serializer = CommentSerializer(comment)
|
||||
serializer.data
|
||||
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
|
||||
|
||||
|
@ -65,18 +65,54 @@ Deserialization is similar. First we parse a stream into python native datatype
|
|||
|
||||
...then we restore those native datatypes into a fully populated object instance.
|
||||
|
||||
serializer = CommentSerializer(data)
|
||||
serializer = CommentSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
# True
|
||||
serializer.object
|
||||
# <Comment object at 0x10633b2d0>
|
||||
>>> serializer.deserialize('json', stream)
|
||||
|
||||
When deserializing data, we can either create a new instance, or update an existing instance.
|
||||
|
||||
serializer = CommentSerializer(data=data) # Create new instance
|
||||
serializer = CommentSerializer(comment, data=data) # Update `instance`
|
||||
|
||||
## Validation
|
||||
|
||||
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages.
|
||||
|
||||
**TODO: Describe validation in more depth**
|
||||
### 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.
|
||||
|
||||
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:
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
class BlogPostSerializer(serializers.Serializer):
|
||||
title = serializers.CharField(max_length=100)
|
||||
content = serializers.CharField()
|
||||
|
||||
def validate_title(self, attrs, source):
|
||||
"""
|
||||
Check that the blog post is about Django.
|
||||
"""
|
||||
value = attrs[source]
|
||||
if "django" not in value.lower():
|
||||
raise serializers.ValidationError("Blog post is not about Django")
|
||||
return attrs
|
||||
|
||||
### 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`.
|
||||
|
||||
## Saving object state
|
||||
|
||||
Serializers also include a `.save()` method that you can override if you want to provide a method of persisting the state of a deserialized object. The default behavior of the method is to simply call `.save()` on the deserialized object instance.
|
||||
|
||||
The generic views provided by REST framework call the `.save()` method when updating or creating entities.
|
||||
|
||||
## Dealing with nested objects
|
||||
|
||||
|
@ -86,21 +122,21 @@ where some of the attributes of an object might not be simple datatypes such as
|
|||
The `Serializer` class is itself a type of `Field`, and can be used to represent relationships where one object type is nested inside another.
|
||||
|
||||
class UserSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
username = serializers.CharField()
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
return User(**attrs)
|
||||
|
||||
email = serializers.Field()
|
||||
username = serializers.Field()
|
||||
|
||||
class CommentSerializer(serializers.Serializer):
|
||||
user = UserSerializer()
|
||||
title = serializers.CharField()
|
||||
content = serializers.CharField(max_length=200)
|
||||
created = serializers.DateTimeField()
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
return Comment(**attrs)
|
||||
title = serializers.Field()
|
||||
content = serializers.Field()
|
||||
created = serializers.Field()
|
||||
|
||||
---
|
||||
|
||||
**Note**: Nested serializers are only suitable for read-only representations, as there are cases where they would have ambiguous or non-obvious behavior if used when updating instances. For read-write representations you should always use a flat representation, by using one of the `RelatedField` subclasses.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Creating custom fields
|
||||
|
||||
|
@ -114,7 +150,6 @@ Let's look at an example of serializing a class that represents an RGB color val
|
|||
"""
|
||||
A color represented in the RGB colorspace.
|
||||
"""
|
||||
|
||||
def __init__(self, red, green, blue):
|
||||
assert(red >= 0 and green >= 0 and blue >= 0)
|
||||
assert(red < 256 and green < 256 and blue < 256)
|
||||
|
@ -124,7 +159,6 @@ Let's look at an example of serializing a class that represents an RGB color val
|
|||
"""
|
||||
Color objects are serialized into "rgb(#, #, #)" notation.
|
||||
"""
|
||||
|
||||
def to_native(self, obj):
|
||||
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
|
||||
|
||||
|
@ -156,7 +190,7 @@ As an example, let's create a field that can be used represent the class name of
|
|||
# ModelSerializers
|
||||
|
||||
Often you'll want serializer classes that map closely to model definitions.
|
||||
The `ModelSerializer` class lets you automatically create a Serializer class with fields that corrospond to the Model fields.
|
||||
The `ModelSerializer` class lets you automatically create a Serializer class with fields that correspond to the Model fields.
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -169,13 +203,13 @@ The `ModelSerializer` class lets you automatically create a Serializer class wit
|
|||
You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
url = CharField(source='get_absolute_url', readonly=True)
|
||||
url = CharField(source='get_absolute_url', read_only=True)
|
||||
group = NaturalKeyField()
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
|
||||
Extra fields can corrospond to any property or callable on the model.
|
||||
Extra fields can correspond to any property or callable on the model.
|
||||
|
||||
## Relational fields
|
||||
|
||||
|
@ -187,7 +221,7 @@ The `PrimaryKeyRelatedField` and `HyperlinkedRelatedField` fields provide altern
|
|||
|
||||
The `ModelSerializer` class can itself be used as a field, in order to serialize relationships using nested representations.
|
||||
|
||||
The `RelatedField` class may be subclassed to create a custom represenation of a relationship. The subclass should override `.to_native()`, and optionally `.from_native()` if deserialization is supported.
|
||||
The `RelatedField` class may be subclassed to create a custom representation of a relationship. The subclass should override `.to_native()`, and optionally `.from_native()` if deserialization is supported.
|
||||
|
||||
All the relational fields may be used for any relationship or reverse relationship on a model.
|
||||
|
||||
|
@ -204,40 +238,54 @@ For example:
|
|||
|
||||
## Specifiying nested serialization
|
||||
|
||||
The default `ModelSerializer` uses primary keys for relationships, but you can also easily generate nested representations using the `nested` option:
|
||||
The default `ModelSerializer` uses primary keys for relationships, but you can also easily generate nested representations using the `depth` option:
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
exclude = ('id',)
|
||||
nested = True
|
||||
depth = 1
|
||||
|
||||
The `nested` option may be set to either `True`, `False`, or an integer value. If given an integer value it indicates the depth of relationships that should be traversed before reverting to a flat representation.
|
||||
The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
|
||||
|
||||
When serializing objects using a nested representation any occurances of recursion will be recognised, and will fall back to using a flat representation.
|
||||
## Customising the default fields
|
||||
|
||||
## Customising the default fields used by a ModelSerializer
|
||||
You can create customized subclasses of `ModelSerializer` that use a different set of default fields for the representation, by overriding various `get_<field_type>_field` methods.
|
||||
|
||||
Each of these methods may either return a field or serializer instance, or `None`.
|
||||
|
||||
### get_pk_field
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
**Signature**: `.get_pk_field(self, model_field)`
|
||||
|
||||
Returns the field instance that should be used to represent the pk field.
|
||||
|
||||
### get_nested_field
|
||||
|
||||
**Signature**: `.get_nested_field(self, model_field)`
|
||||
|
||||
Returns the field instance that should be used to represent a related field when `depth` is specified as being non-zero.
|
||||
|
||||
### get_related_field
|
||||
|
||||
**Signature**: `.get_related_field(self, model_field, to_many=False)`
|
||||
|
||||
Returns the field instance that should be used to represent a related field when `depth` is not specified, or when nested representations are being used and the depth reaches zero.
|
||||
|
||||
### get_field
|
||||
|
||||
**Signature**: `.get_field(self, model_field)`
|
||||
|
||||
Returns the field instance that should be used for non-relational, non-pk fields.
|
||||
|
||||
### Example:
|
||||
|
||||
The following custom model serializer could be used as a base class for model serializers that should always exclude the pk by default.
|
||||
|
||||
class NoPKModelSerializer(serializers.ModelSerializer):
|
||||
def get_pk_field(self, model_field):
|
||||
return serializers.Field(readonly=True)
|
||||
return None
|
||||
|
||||
def get_nested_field(self, model_field):
|
||||
return serializers.ModelSerializer()
|
||||
|
||||
def get_related_field(self, model_field, to_many=False):
|
||||
queryset = model_field.rel.to._default_manager
|
||||
if to_many:
|
||||
return return serializers.ManyRelatedField(queryset=queryset)
|
||||
return serializers.RelatedField(queryset=queryset)
|
||||
|
||||
def get_field(self, model_field):
|
||||
return serializers.ModelField(model_field=model_field)
|
||||
|
||||
|
||||
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion
|
||||
|
|
|
@ -11,10 +11,10 @@ Configuration for REST framework is all namespaced inside a single Django settin
|
|||
For example your project's `settings.py` file might include something like this:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERERS': (
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.YAMLRenderer',
|
||||
)
|
||||
'DEFAULT_PARSERS': (
|
||||
),
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.YAMLParser',
|
||||
)
|
||||
}
|
||||
|
@ -26,11 +26,15 @@ you should use the `api_settings` object. For example.
|
|||
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
print api_settings.DEFAULT_AUTHENTICATION
|
||||
print api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
|
||||
The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
|
||||
|
||||
## DEFAULT_RENDERERS
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## DEFAULT_RENDERER_CLASSES
|
||||
|
||||
A list or tuple of renderer classes, that determines the default set of renderers that may be used when returning a `Response` object.
|
||||
|
||||
|
@ -38,11 +42,11 @@ Default:
|
|||
|
||||
(
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer'
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
'rest_framework.renderers.TemplateHTMLRenderer'
|
||||
)
|
||||
|
||||
## DEFAULT_PARSERS
|
||||
## DEFAULT_PARSER_CLASSES
|
||||
|
||||
A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property.
|
||||
|
||||
|
@ -53,7 +57,7 @@ Default:
|
|||
'rest_framework.parsers.FormParser'
|
||||
)
|
||||
|
||||
## DEFAULT_AUTHENTICATION
|
||||
## DEFAULT_AUTHENTICATION_CLASSES
|
||||
|
||||
A list or tuple of authentication classes, that determines the default set of authenticators used when accessing the `request.user` or `request.auth` properties.
|
||||
|
||||
|
@ -64,25 +68,29 @@ Default:
|
|||
'rest_framework.authentication.UserBasicAuthentication'
|
||||
)
|
||||
|
||||
## DEFAULT_PERMISSIONS
|
||||
## DEFAULT_PERMISSION_CLASSES
|
||||
|
||||
A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view.
|
||||
|
||||
Default: `()`
|
||||
Default:
|
||||
|
||||
## DEFAULT_THROTTLES
|
||||
(
|
||||
'rest_framework.permissions.AllowAny',
|
||||
)
|
||||
|
||||
## DEFAULT_THROTTLE_CLASSES
|
||||
|
||||
A list or tuple of throttle classes, that determines the default set of throttles checked at the start of a view.
|
||||
|
||||
Default: `()`
|
||||
|
||||
## DEFAULT_MODEL_SERIALIZER
|
||||
## DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
|
||||
**TODO**
|
||||
|
||||
Default: `rest_framework.serializers.ModelSerializer`
|
||||
|
||||
## DEFAULT_PAGINATION_SERIALIZER
|
||||
## DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
|
||||
**TODO**
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ Response status codes beginning with the digit "5" indicate cases in which the s
|
|||
HTTP_503_SERVICE_UNAVAILABLE
|
||||
HTTP_504_GATEWAY_TIMEOUT
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
|
||||
HTTP_511_NETWORD_AUTHENTICATION_REQUIRED
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
|
||||
|
||||
|
||||
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
|
||||
|
|
|
@ -27,13 +27,13 @@ If any throttle check fails an `exceptions.Throttled` exception will be raised,
|
|||
|
||||
## Setting the throttling policy
|
||||
|
||||
The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` and `DEFAULT_THROTTLE_RATES` settings. For example.
|
||||
The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_CLASSES` and `DEFAULT_THROTTLE_RATES` settings. For example.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLES': (
|
||||
'rest_framework.throttles.AnonThrottle',
|
||||
'rest_framework.throttles.UserThrottle',
|
||||
)
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/day',
|
||||
'user': '1000/day'
|
||||
|
@ -63,6 +63,8 @@ Or, if you're using the `@api_view` decorator with function based views.
|
|||
}
|
||||
return Response(content)
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## AnonRateThrottle
|
||||
|
@ -78,7 +80,7 @@ The allowed request rate is determined from one of the following (in order of pr
|
|||
|
||||
## UserRateThrottle
|
||||
|
||||
The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticted requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
|
||||
The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
|
||||
|
||||
The allowed request rate is determined from one of the following (in order of preference).
|
||||
|
||||
|
@ -98,10 +100,10 @@ For example, multiple user throttle rates could be implemented by using the foll
|
|||
...and the following settings.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLES': (
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'example.throttles.BurstRateThrottle',
|
||||
'example.throttles.SustainedRateThrottle',
|
||||
)
|
||||
'example.throttles.SustainedRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'burst': '60/min',
|
||||
'sustained': '1000/day'
|
||||
|
@ -112,7 +114,7 @@ For example, multiple user throttle rates could be implemented by using the foll
|
|||
|
||||
## ScopedRateThrottle
|
||||
|
||||
The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unqiue user id or IP address.
|
||||
The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address.
|
||||
|
||||
The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope".
|
||||
|
||||
|
@ -133,9 +135,9 @@ For example, given the following views...
|
|||
...and the following settings.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLES': (
|
||||
'rest_framework.throttles.ScopedRateThrottle',
|
||||
)
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttling.ScopedRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'contacts': '1000/day',
|
||||
'uploads': '20/day'
|
||||
|
@ -144,10 +146,12 @@ For example, given the following views...
|
|||
|
||||
User requests to either `ContactListView` or `ContactDetailView` would be restricted to a total of 1000 requests per-day. User requests to `UploadView` would be restricted to 20 requests per day.
|
||||
|
||||
---
|
||||
|
||||
# Custom throttles
|
||||
|
||||
To create a custom throttle, override `BaseThrottle` and implement `.allow_request(request, view)`. The method should return `True` if the request should be allowed, and `False` otherwise.
|
||||
|
||||
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recomended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
|
||||
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
|
||||
|
||||
[permissions]: permissions.md
|
||||
|
|
|
@ -27,14 +27,14 @@ For example:
|
|||
* Only admin users are able to access this view.
|
||||
"""
|
||||
authentication_classes = (authentication.TokenAuthentication,)
|
||||
permission_classes = (permissions.IsAdmin,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
Return a list of all users.
|
||||
"""
|
||||
users = [user.username for user in User.objects.all()]
|
||||
return Response(users)
|
||||
usernames = [user.username for user in User.objects.all()]
|
||||
return Response(usernames)
|
||||
|
||||
## API policy attributes
|
||||
|
||||
|
@ -118,9 +118,51 @@ You won't typically need to override this method.
|
|||
>
|
||||
> — [Nick Coghlan][cite2]
|
||||
|
||||
REST framework also gives you to work with regular function based views...
|
||||
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.
|
||||
|
||||
**[TODO]**
|
||||
## @api_view()
|
||||
|
||||
**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:
|
||||
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
@api_view(['GET'])
|
||||
def hello_world(request):
|
||||
return Response({"message": "Hello, world!"})
|
||||
|
||||
|
||||
This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings).
|
||||
|
||||
## API policy decorators
|
||||
|
||||
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
|
||||
|
||||
from rest_framework.decorators import api_view, throttle_classes
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
class OncePerDayUserThrottle(UserRateThrottle):
|
||||
rate = '1/day'
|
||||
|
||||
@api_view(['GET'])
|
||||
@throttle_classes([OncePerDayUserThrottle])
|
||||
def view(request):
|
||||
return Response({"message": "Hello for today! See you tomorrow!"})
|
||||
|
||||
These decorators correspond to the attributes set on `APIView` subclasses, described above.
|
||||
|
||||
The available decorators are:
|
||||
|
||||
* `@renderer_classes(...)`
|
||||
* `@parser_classes(...)`
|
||||
* `@authentication_classes(...)`
|
||||
* `@throttle_classes(...)`
|
||||
* `@permission_classes(...)`
|
||||
|
||||
Each of these decorators takes a single argument which must be a list or tuple of classes.
|
||||
|
||||
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
|
||||
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
|
||||
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
|
||||
[settings]: api-guide/settings.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
|
|
|
@ -88,6 +88,10 @@ pre {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-list a {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Set the table of contents to static so it flows back into the content when
|
||||
viewed on tablets and smaller. */
|
||||
@media (max-width: 767px) {
|
||||
|
|
|
@ -5,12 +5,24 @@
|
|||
|
||||
**A toolkit for building well-connected, self-describing Web APIs.**
|
||||
|
||||
**WARNING: This documentation is for the 2.0 redesign of REST framework. It is a work in progress.**
|
||||
---
|
||||
|
||||
**Note**: This documentation is for the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub.
|
||||
|
||||
---
|
||||
|
||||
Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views.
|
||||
|
||||
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
||||
|
||||
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
|
||||
|
||||
There is also a sandbox API you can use for testing purposes, [available here][sandbox].
|
||||
|
||||
**Below**: *Screenshot from the browseable API*
|
||||
|
||||
![Screenshot][image]
|
||||
|
||||
## Requirements
|
||||
|
||||
REST framework requires the following:
|
||||
|
@ -22,11 +34,10 @@ The following packages are optional:
|
|||
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the self describing API.
|
||||
* [PyYAML][yaml] (3.10+) - YAML content-type support.
|
||||
* [django-filter][django-filter] (master) - Filtering support.
|
||||
|
||||
## Installation
|
||||
|
||||
**WARNING: These instructions will only become valid once this becomes the master version**
|
||||
|
||||
Install using `pip`, including any optional packages you want...
|
||||
|
||||
pip install djangorestframework
|
||||
|
@ -47,7 +58,7 @@ Add `rest_framework` to your `INSTALLED_APPS`.
|
|||
'rest_framework',
|
||||
)
|
||||
|
||||
If you're intending to use the browserable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
||||
If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
||||
|
||||
urlpatterns = patterns('',
|
||||
...
|
||||
|
@ -67,9 +78,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
|
|||
* [1 - Serialization][tut-1]
|
||||
* [2 - Requests & Responses][tut-2]
|
||||
* [3 - Class based views][tut-3]
|
||||
* [4 - Authentication, permissions & throttling][tut-4]
|
||||
* [4 - Authentication & permissions][tut-4]
|
||||
* [5 - Relationships & hyperlinked APIs][tut-5]
|
||||
<!-- * [6 - Resource orientated projects][tut-6]-->
|
||||
|
||||
## API Guide
|
||||
|
||||
|
@ -86,6 +96,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Authentication][authentication]
|
||||
* [Permissions][permissions]
|
||||
* [Throttling][throttling]
|
||||
* [Filtering][filtering]
|
||||
* [Pagination][pagination]
|
||||
* [Content negotiation][contentnegotiation]
|
||||
* [Format suffixes][formatsuffixes]
|
||||
|
@ -98,12 +109,10 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
|
||||
General guides to using REST framework.
|
||||
|
||||
* [CSRF][csrf]
|
||||
* [Browser enhancements][browser-enhancements]
|
||||
* [The Browsable API][browsableapi]
|
||||
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
||||
* [Contributing to REST framework][contributing]
|
||||
* [2.0 Migration Guide][migration]
|
||||
* [2.0 Announcement][rest-framework-2-announcement]
|
||||
* [Release Notes][release-notes]
|
||||
* [Credits][credits]
|
||||
|
||||
|
@ -119,7 +128,6 @@ Run the tests:
|
|||
|
||||
./rest_framework/runtests/runtests.py
|
||||
|
||||
For more information see the [Contributing to REST framework][contributing] section.
|
||||
## Support
|
||||
|
||||
For support please see the [REST framework discussion group][group], or try the `#restframework` channel on `irc.freenode.net`.
|
||||
|
@ -151,19 +159,22 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[yaml]: http://pypi.python.org/pypi/PyYAML
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[image]: img/quickstart.png
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
|
||||
[quickstart]: tutorial/quickstart.md
|
||||
[tut-1]: tutorial/1-serialization.md
|
||||
[tut-2]: tutorial/2-requests-and-responses.md
|
||||
[tut-3]: tutorial/3-class-based-views.md
|
||||
[tut-4]: tutorial/4-authentication-permissions-and-throttling.md
|
||||
[tut-4]: tutorial/4-authentication-and-permissions.md
|
||||
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
||||
[tut-6]: tutorial/6-resource-orientated-projects.md
|
||||
|
||||
[request]: api-guide/requests.md
|
||||
[response]: api-guide/responses.md
|
||||
|
@ -176,6 +187,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[authentication]: api-guide/authentication.md
|
||||
[permissions]: api-guide/permissions.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
[filtering]: api-guide/filtering.md
|
||||
[pagination]: api-guide/pagination.md
|
||||
[contentnegotiation]: api-guide/content-negotiation.md
|
||||
[formatsuffixes]: api-guide/format-suffixes.md
|
||||
|
@ -189,10 +201,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[browsableapi]: topics/browsable-api.md
|
||||
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
|
||||
[contributing]: topics/contributing.md
|
||||
[migration]: topics/migration.md
|
||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
||||
[release-notes]: topics/release-notes.md
|
||||
[credits]: topics/credits.md
|
||||
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[DabApps]: http://dabapps.com
|
||||
[email]: mailto:tom@tomchristie.com
|
||||
[email]: mailto:tom@tomchristie.com
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<html lang="en">
|
||||
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<title>Django REST framework</title>
|
||||
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||
|
@ -17,6 +18,21 @@
|
|||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-18852272-2']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body onload="prettyPrint()" class="{{ page_id }}-page">
|
||||
|
||||
<div class="wrapper">
|
||||
|
@ -24,7 +40,7 @@
|
|||
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/restframework2">GitHub</a>
|
||||
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
|
||||
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
|
@ -41,9 +57,8 @@
|
|||
<li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li>
|
||||
<li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li>
|
||||
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
|
||||
<li><a href="{{ base_url }}/tutorial/4-authentication-permissions-and-throttling{{ suffix }}">4 - Authentication, permissions and throttling</a></li>
|
||||
<li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
|
||||
<li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
|
||||
<!-- <li><a href="{{ base_url }}/tutorial/6-resource-orientated-projects{{ suffix }}">6 - Resource orientated projects</a></li> -->
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
|
@ -60,6 +75,7 @@
|
|||
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/filtering{{ suffix }}">Filtering</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/pagination{{ suffix }}">Pagination</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li>
|
||||
|
@ -72,12 +88,10 @@
|
|||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ base_url }}/topics/csrf{{ suffix }}">Working with AJAX and CSRF</a></li>
|
||||
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
|
||||
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
|
||||
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
|
||||
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
|
||||
<li><a href="{{ base_url }}/topics/migration{{ suffix }}">2.0 Migration Guide</a></li>
|
||||
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
||||
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
|
||||
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -2,42 +2,63 @@
|
|||
|
||||
> "There are two noncontroversial uses for overloaded POST. The first is to *simulate* HTTP's uniform interface for clients like web browsers that don't support PUT or DELETE"
|
||||
>
|
||||
> — [RESTful Web Services](1), Leonard Richardson & Sam Ruby.
|
||||
> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
|
||||
|
||||
## Browser based PUT, DELETE, etc...
|
||||
|
||||
**TODO: Preamble.** Note that this is the same strategy as is used in [Ruby on Rails](2).
|
||||
REST framework supports browser-based `PUT`, `DELETE` and other methods, by
|
||||
overloading `POST` requests using a hidden form field.
|
||||
|
||||
Note that this is the same strategy as is used in [Ruby on Rails][rails].
|
||||
|
||||
For example, given the following form:
|
||||
|
||||
<form action="/news-items/5" method="POST">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
</form>
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
</form>
|
||||
|
||||
`request.method` would return `"DELETE"`.
|
||||
|
||||
## Browser based submission of non-form content
|
||||
|
||||
Browser-based submission of content types other than form are supported by using form fields named `_content` and `_content_type`:
|
||||
Browser-based submission of content types other than form are supported by
|
||||
using form fields named `_content` and `_content_type`:
|
||||
|
||||
For example, given the following form:
|
||||
|
||||
<form action="/news-items/5" method="PUT">
|
||||
<input type="hidden" name="_content_type" value="application/json">
|
||||
<input name="_content" value="{'count': 1}">
|
||||
</form>
|
||||
<input type="hidden" name="_content_type" value="application/json">
|
||||
<input name="_content" value="{'count': 1}">
|
||||
</form>
|
||||
|
||||
`request.content_type` would return `"application/json"`, and `request.stream` would return `"{'count': 1}"`
|
||||
`request.content_type` would return `"application/json"`, and
|
||||
`request.stream` would return `"{'count': 1}"`
|
||||
|
||||
## URL based accept headers
|
||||
|
||||
REST framework can take `?accept=application/json` style URL parameters,
|
||||
which allow the `Accept` header to be overridden.
|
||||
|
||||
This can be useful for testing the API from a web browser, where you don't
|
||||
have any control over what is sent in the `Accept` header.
|
||||
|
||||
## URL based format suffixes
|
||||
|
||||
REST framework can take `?format=json` style URL parameters, which can be a
|
||||
useful shortcut for determing which content type should be returned from
|
||||
the view.
|
||||
|
||||
This is a more concise than using the `accept` override, but it also gives
|
||||
you less control. (For example you can't specify any media type parameters)
|
||||
|
||||
## Doesn't HTML5 support PUT and DELETE forms?
|
||||
|
||||
Nope. It was at one point intended to support `PUT` and `DELETE` forms, but was later [dropped from the spec](3). There remains [ongoing discussion](4) about adding support for `PUT` and `DELETE`, as well as how to support content types other than form-encoded data.
|
||||
Nope. It was at one point intended to support `PUT` and `DELETE` forms, but
|
||||
was later [dropped from the spec][html5]. There remains
|
||||
[ongoing discussion][put_delete] about adding support for `PUT` and `DELETE`,
|
||||
as well as how to support content types other than form-encoded data.
|
||||
|
||||
[1]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
|
||||
[2]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
|
||||
[3]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
||||
[4]: http://amundsen.com/examples/put-delete-forms/
|
||||
[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
|
||||
[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
|
||||
[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
||||
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
||||
|
|
|
@ -47,6 +47,17 @@ The following people have helped make REST framework great.
|
|||
* Mattbo - [mattbo]
|
||||
* Max Hurl - [maximilianhurl]
|
||||
* Tomi Pajunen - [eofs]
|
||||
* Rob Dobson - [rdobson]
|
||||
* Daniel Vaca Araujo - [diviei]
|
||||
* Madis Väin - [madisvain]
|
||||
* Stephan Groß - [minddust]
|
||||
* Pavel Savchenko - [asfaltboy]
|
||||
* Otto Yiu - [ottoyiu]
|
||||
* Jacob Magnusson - [jmagnusson]
|
||||
* Osiloke Harold Emoekpere - [osiloke]
|
||||
* Michael Shepanski - [mjs7231]
|
||||
* Toni Michel - [tonimichel]
|
||||
* Ben Konrath - [benkonrath]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -58,6 +69,8 @@ Project hosting is with [GitHub].
|
|||
|
||||
Continuous integration testing is managed with [Travis CI][travis-ci].
|
||||
|
||||
The [live sandbox][sandbox] is hosted on [Heroku].
|
||||
|
||||
Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects.
|
||||
|
||||
Development of REST framework 2.0 was sponsored by [DabApps].
|
||||
|
@ -73,12 +86,14 @@ To contact the author directly:
|
|||
[twitter]: http://twitter.com/_tomchristie
|
||||
[bootstrap]: http://twitter.github.com/bootstrap/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/
|
||||
[github]: github.com/tomchristie/django-rest-framework
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework
|
||||
[piston]: https://bitbucket.org/jespern/django-piston
|
||||
[tastypie]: https://github.com/toastdriven/django-tastypie
|
||||
[dagny]: https://github.com/zacharyvoase/dagny
|
||||
[dabapps]: http://lab.dabapps.com
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[heroku]: http://www.heroku.com/
|
||||
|
||||
[tomchristie]: https://github.com/tomchristie
|
||||
[markotibold]: https://github.com/markotibold
|
||||
|
@ -124,4 +139,15 @@ To contact the author directly:
|
|||
[j4mie]: https://github.com/j4mie
|
||||
[mattbo]: https://github.com/mattbo
|
||||
[maximilianhurl]: https://github.com/maximilianhurl
|
||||
[eofs]: https://github.com/eofs
|
||||
[eofs]: https://github.com/eofs
|
||||
[rdobson]: https://github.com/rdobson
|
||||
[diviei]: https://github.com/diviei
|
||||
[madisvain]: https://github.com/madisvain
|
||||
[minddust]: https://github.com/minddust
|
||||
[asfaltboy]: https://github.com/asfaltboy
|
||||
[ottoyiu]: https://github.com/OttoYiu
|
||||
[jmagnusson]: https://github.com/jmagnusson
|
||||
[osiloke]: https://github.com/osiloke
|
||||
[mjs7231]: https://github.com/mjs7231
|
||||
[tonimichel]: https://github.com/tonimichel
|
||||
[benkonrath]: https://github.com/benkonrath
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
> — [Jeff Atwood][cite]
|
||||
|
||||
* Explain need to add CSRF token to AJAX requests.
|
||||
* Explain defered CSRF style used by REST framework
|
||||
* Explain deferred CSRF style used by REST framework
|
||||
* Why you should use Django's standard login/logout views, and not REST framework view
|
||||
|
||||
|
||||
[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
|
||||
[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
|
||||
|
|
|
@ -4,10 +4,57 @@
|
|||
>
|
||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||
|
||||
## 2.1.2
|
||||
|
||||
**Date**: 9th Nov 2012
|
||||
|
||||
* **Filtering support.**
|
||||
* Bugfix: Support creation of objects with reverse M2M relations.
|
||||
|
||||
## 2.1.1
|
||||
|
||||
**Date**: 7th Nov 2012
|
||||
|
||||
* Support use of HTML exception templates. Eg. `403.html`
|
||||
* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
|
||||
* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs.
|
||||
* Bugfix: Make textareas same width as other fields in browsable API.
|
||||
* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
**Date**: 5th Nov 2012
|
||||
|
||||
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
|
||||
|
||||
* **Serializer `instance` and `data` keyword args have their position swapped.**
|
||||
* `queryset` argument is now optional on writable model fields.
|
||||
* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
|
||||
* Support Django's cache framework.
|
||||
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
|
||||
* Bugfix: Support choice field in Browseable API.
|
||||
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
|
||||
|
||||
## 2.0.2
|
||||
|
||||
**Date**: 2nd Nov 2012
|
||||
|
||||
* Fix issues with pk related fields in the browsable API.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
**Date**: 1st Nov 2012
|
||||
|
||||
* Add support for relational fields in the browsable API.
|
||||
* Added SlugRelatedField and ManySlugRelatedField.
|
||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
**Date**: 30th Oct 2012
|
||||
|
||||
* **Fix all of the things.** (Well, almost.)
|
||||
* For more information please see the [2.0 migration guide][migration].
|
||||
* For more information please see the [2.0 announcement][announcement].
|
||||
|
||||
---
|
||||
|
||||
|
@ -113,4 +160,5 @@
|
|||
* Initial release.
|
||||
|
||||
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
|
||||
[migration]: migration.md
|
||||
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||
[announcement]: rest-framework-2-announcement.md
|
||||
|
|
100
docs/topics/rest-framework-2-announcement.md
Normal file
100
docs/topics/rest-framework-2-announcement.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
# Django REST framework 2
|
||||
|
||||
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.
|
||||
>
|
||||
> — [Roy Fielding][cite]
|
||||
|
||||
---
|
||||
|
||||
**Announcement:** REST framework 2 released - Tue 30th Oct 2012
|
||||
|
||||
---
|
||||
|
||||
REST framework 2 is an almost complete reworking of the original framework, which comprehensively addresses some of the original design issues.
|
||||
|
||||
Because the latest version should be considered a re-release, rather than an incremental improvement, we've skipped a version, and called this release Django REST framework 2.0.
|
||||
|
||||
This article is intended to give you a flavor of what REST framework 2 is, and why you might want to give it a try.
|
||||
|
||||
## User feedback
|
||||
|
||||
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]
|
||||
|
||||
"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]
|
||||
|
||||
Sounds good, right? Let's get into some details...
|
||||
|
||||
## Serialization
|
||||
|
||||
REST framework 2 includes a totally re-worked serialization engine, that was initially intended as a replacement for Django's existing inflexible fixture serialization, and which meets the following design goals:
|
||||
|
||||
* A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API.
|
||||
* Structural concerns are decoupled from encoding concerns.
|
||||
* Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms.
|
||||
* Validation that can be mapped to obvious and comprehensive error responses.
|
||||
* Serializers that support both nested, flat, and partially-nested representations.
|
||||
* Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations.
|
||||
|
||||
Mapping between the internal state of the system and external representations of that state is the core concern of building Web APIs. Designing serializers that allow the developer to do so in a flexible and obvious way is a deceptively difficult design task, and with the new serialization API we think we've pretty much nailed it.
|
||||
|
||||
## Generic views
|
||||
|
||||
When REST framework was initially released at the start of 2011, the current Django release was version 1.2. REST framework included a backport of Django 1.3's upcoming `View` class, but it didn't take full advantage of the generic view implementations.
|
||||
|
||||
With the new release the generic views in REST framework now tie in with Django's generic views. The end result is that framework is clean, lightweight and easy to use.
|
||||
|
||||
## Requests, Responses & Views
|
||||
|
||||
REST framework 2 includes `Request` and `Response` classes, than are used in place of Django's existing `HttpRequest` and `HttpResponse` classes. Doing so allows logic such as parsing the incoming request or rendering the outgoing response to be supported transparently by the framework.
|
||||
|
||||
The `Request`/`Response` approach leads to a much cleaner API, less logic in the view itself, and a simple, obvious request-response cycle.
|
||||
|
||||
REST framework 2 also allows you to work with both function-based and class-based views. For simple API views all you need is a single `@api_view` decorator, and you're good to go.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
## The Browseable API
|
||||
|
||||
Django REST framework's most unique feature is the way it is able to serve up both machine-readable representations, and a fully browsable HTML representation to the same endpoints.
|
||||
|
||||
Browseable Web APIs are easier to work with, visualize and debug, and generally makes it easier and more frictionless to inspect and work with.
|
||||
|
||||
With REST framework 2, the browseable 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.
|
||||
|
||||
![Browseable API][image]
|
||||
|
||||
**Image above**: An example of the browseable API in REST framework 2
|
||||
|
||||
## Documentation
|
||||
|
||||
As you can see the documentation for REST framework has been radically improved. It gets a completely new style, using markdown for the documentation source, and a bootstrap-based theme for the styling.
|
||||
|
||||
We're really pleased with how the docs style looks - it's simple and clean, is easy to navigate around, and we think it reads great.
|
||||
|
||||
## Summary
|
||||
|
||||
In short, we've engineered the hell outta this thing, and we're incredibly proud of the result.
|
||||
|
||||
If you're interested please take a browse around the documentation. [The tutorial][tut] is a great place to get started.
|
||||
|
||||
There's also a [live sandbox version of the tutorial API][sandbox] available for testing.
|
||||
|
||||
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-724
|
||||
[quote1]: https://twitter.com/kobutsu/status/261689665952833536
|
||||
[quote2]: https://groups.google.com/d/msg/django-rest-framework/heRGHzG6BWQ/ooVURgpwVC0J
|
||||
[quote3]: https://groups.google.com/d/msg/django-rest-framework/flsXbvYqRoY/9lSyntOf5cUJ
|
||||
[image]: ../img/quickstart.png
|
||||
[readthedocs]: https://readthedocs.org/
|
||||
[tut]: ../tutorial/1-serialization.md
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
> — Mike Amundsen, [REST fest 2012 keynote][cite].
|
||||
|
||||
First off, the disclaimer. The name "Django REST framework" was choosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
|
||||
First off, the disclaimer. The name "Django REST framework" was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
|
||||
|
||||
If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices.
|
||||
|
||||
|
@ -22,7 +22,7 @@ For a more thorough background, check out Klabnik's [Hypermedia API reading list
|
|||
|
||||
## Building Hypermedia APIs with REST framework
|
||||
|
||||
REST framework is an agnositic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style.
|
||||
REST framework is an agnostic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style.
|
||||
|
||||
## What REST framework provides.
|
||||
|
||||
|
@ -50,4 +50,4 @@ What REST framework doesn't do is give you is machine readable hypermedia format
|
|||
[parser]: ../api-guide/parsers.md
|
||||
[renderer]: ../api-guide/renderers.md
|
||||
[fields]: ../api-guide/fields.md
|
||||
[conneg]: ../api-guide/content-negotiation.md
|
||||
[conneg]: ../api-guide/content-negotiation.md
|
||||
|
|
|
@ -2,7 +2,15 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
This tutorial will walk you through the building blocks that make up REST framework. It'll take a little while to get through, but it'll 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. -->
|
||||
|
||||
---
|
||||
|
||||
**Note**: The final code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. There is also a sandbox version for testing, [available here][sandbox].
|
||||
|
||||
---
|
||||
|
||||
## Setting up a new environment
|
||||
|
||||
|
@ -10,13 +18,14 @@ Before we do anything else we'll create a new virtual environment, using [virtua
|
|||
|
||||
:::bash
|
||||
mkdir ~/env
|
||||
virtualenv --no-site-packages ~/env/tutorial
|
||||
virtualenv ~/env/tutorial
|
||||
source ~/env/tutorial/bin/activate
|
||||
|
||||
Now that we're inside a virtualenv environment, we can install our package requirements.
|
||||
|
||||
pip install django
|
||||
pip install djangorestframework
|
||||
pip install pygments # We'll be using this for the code highlighting
|
||||
|
||||
**Note:** To exit the virtualenv environment at any time, just type `deactivate`. For more information see the [virtualenv documentation][virtualenv].
|
||||
|
||||
|
@ -30,8 +39,9 @@ To get started, let's create a new project to work with.
|
|||
cd tutorial
|
||||
|
||||
Once that's done we can create an app that we'll use to create a simple Web API.
|
||||
We're going to create a project that
|
||||
|
||||
python manage.py startapp blog
|
||||
python manage.py startapp snippets
|
||||
|
||||
The simplest way to get up and running will probably be to use an `sqlite3` database for the tutorial. Edit the `tutorial/settings.py` file, and set the default database `"ENGINE"` to `"sqlite3"`, and `"NAME"` to `"tmp.db"`.
|
||||
|
||||
|
@ -46,32 +56,48 @@ The simplest way to get up and running will probably be to use an `sqlite3` data
|
|||
}
|
||||
}
|
||||
|
||||
We'll also need to add our new `blog` app and the `rest_framework` app to `INSTALLED_APPS`.
|
||||
We'll also need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`.
|
||||
|
||||
INSTALLED_APPS = (
|
||||
...
|
||||
'rest_framework',
|
||||
'blog'
|
||||
'snippets'
|
||||
)
|
||||
|
||||
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our blog views.
|
||||
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views.
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^', include('blog.urls')),
|
||||
url(r'^', include('snippets.urls')),
|
||||
)
|
||||
|
||||
Okay, we're ready to roll.
|
||||
|
||||
## Creating a model to work with
|
||||
|
||||
For the purposes of this tutorial we're going to start by creating a simple `Comment` model that is used to store comments against a blog post. Go ahead and edit the `blog` app's `models.py` file.
|
||||
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.
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Comment(models.Model):
|
||||
email = models.EmailField()
|
||||
content = models.CharField(max_length=200)
|
||||
from pygments.lexers import get_all_lexers
|
||||
from pygments.styles import get_all_styles
|
||||
|
||||
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in get_all_lexers()])
|
||||
STYLE_CHOICES = sorted((item, item) for item in list(get_all_styles()))
|
||||
|
||||
|
||||
class Snippet(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
title = models.CharField(max_length=100, default='')
|
||||
code = models.TextField()
|
||||
linenos = models.BooleanField(default=False)
|
||||
language = models.CharField(choices=LANGUAGE_CHOICES,
|
||||
default='python',
|
||||
max_length=100)
|
||||
style = models.CharField(choices=STYLE_CHOICES,
|
||||
default='friendly',
|
||||
max_length=100)
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
Don't forget to sync the database for the first time.
|
||||
|
||||
|
@ -79,28 +105,40 @@ Don't forget to sync the database for the first time.
|
|||
|
||||
## Creating a Serializer class
|
||||
|
||||
We're going to create a simple Web API that we can use to edit these comment objects with. The first thing we need is a way of serializing and deserializing the objects into representations such as `json`. We do this by declaring serializers that work very similarly to Django's forms. Create a file in the `blog` directory named `serializers.py` and add the following.
|
||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||
|
||||
from blog import models
|
||||
from django.forms import widgets
|
||||
from rest_framework import serializers
|
||||
from snippets import models
|
||||
|
||||
|
||||
class CommentSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField(readonly=True)
|
||||
email = serializers.EmailField()
|
||||
content = serializers.CharField(max_length=200)
|
||||
created = serializers.DateTimeField(readonly=True)
|
||||
|
||||
class SnippetSerializer(serializers.Serializer):
|
||||
pk = serializers.Field() # Note: `Field` is an untyped read-only field.
|
||||
title = serializers.CharField(required=False,
|
||||
max_length=100)
|
||||
code = serializers.CharField(widget=widgets.Textarea,
|
||||
max_length=100000)
|
||||
linenos = serializers.BooleanField(required=False)
|
||||
language = serializers.ChoiceField(choices=models.LANGUAGE_CHOICES,
|
||||
default='python')
|
||||
style = serializers.ChoiceField(choices=models.STYLE_CHOICES,
|
||||
default='friendly')
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
Create or update a new comment instance.
|
||||
Create or update a new snippet instance.
|
||||
"""
|
||||
if instance:
|
||||
instance.email = attrs['email']
|
||||
instance.content = attrs['content']
|
||||
instance.created = attrs['created']
|
||||
# Update existing instance
|
||||
instance.title = attrs['title']
|
||||
instance.code = attrs['code']
|
||||
instance.linenos = attrs['linenos']
|
||||
instance.language = attrs['language']
|
||||
instance.style = attrs['style']
|
||||
return instance
|
||||
return models.Comment(**attrs)
|
||||
|
||||
# Create new instance
|
||||
return models.Snippet(**attrs)
|
||||
|
||||
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.
|
||||
|
||||
|
@ -112,133 +150,146 @@ Before we go any further we'll familiarise ourselves with using our new Serializ
|
|||
|
||||
python manage.py shell
|
||||
|
||||
Okay, once we've got a few imports out of the way, we'd better create a few comments to work with.
|
||||
Okay, once we've got a few imports out of the way, let's create a code snippet to work with.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.parsers import JSONParser
|
||||
|
||||
c1 = Comment(email='leila@example.com', content='nothing to say')
|
||||
c2 = Comment(email='tom@example.com', content='foo bar')
|
||||
c3 = Comment(email='anna@example.com', content='LOLZ!')
|
||||
c1.save()
|
||||
c2.save()
|
||||
c3.save()
|
||||
snippet = Snippet(code='print "hello, world"\n')
|
||||
snippet.save()
|
||||
|
||||
We've now got a few comment instances to play with. Let's take a look at serializing one of those instances.
|
||||
We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances.
|
||||
|
||||
serializer = CommentSerializer(instance=c1)
|
||||
serializer = SnippetSerializer(snippet)
|
||||
serializer.data
|
||||
# {'id': 1, 'email': u'leila@example.com', 'content': u'nothing to say', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774, tzinfo=<UTC>)}
|
||||
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
||||
|
||||
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
|
||||
|
||||
stream = JSONRenderer().render(serializer.data)
|
||||
stream
|
||||
# '{"id": 1, "email": "leila@example.com", "content": "nothing to say", "created": "2012-08-22T16:20:09.822"}'
|
||||
content = JSONRenderer().render(serializer.data)
|
||||
content
|
||||
# '{"pk": 1, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
|
||||
|
||||
Deserialization is similar. First we parse a stream into python native datatypes...
|
||||
|
||||
import StringIO
|
||||
|
||||
stream = StringIO.StringIO(content)
|
||||
data = JSONParser().parse(stream)
|
||||
|
||||
...then we restore those native datatypes into to a fully populated object instance.
|
||||
|
||||
serializer = CommentSerializer(data)
|
||||
serializer = SnippetSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
# True
|
||||
serializer.object
|
||||
# <Comment: Comment object>
|
||||
# <Snippet: Snippet object>
|
||||
|
||||
Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.
|
||||
|
||||
## Writing regular Django views using our Serializers
|
||||
## Using ModelSerializers
|
||||
|
||||
Our `SnippetSerializer` class is replicating a lot of information that's also contained in the `Snippet` model. It would be nice if we could keep out code a bit more concise.
|
||||
|
||||
In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes.
|
||||
|
||||
Let's look at refactoring our serializer using the `ModelSerializer` class.
|
||||
Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class.
|
||||
|
||||
class SnippetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Snippet
|
||||
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
|
||||
|
||||
|
||||
|
||||
## Writing regular Django views using our Serializer
|
||||
|
||||
Let's see how we can write some API views using our new Serializer class.
|
||||
For the moment we won't use any of REST framework's other features, we'll just write the views as regular Django views.
|
||||
|
||||
We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
|
||||
|
||||
Edit the `blog/views.py` file, and add the following.
|
||||
Edit the `snippet/views.py` file, and add the following.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.parsers import JSONParser
|
||||
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
|
||||
class JSONResponse(HttpResponse):
|
||||
"""
|
||||
An HttpResponse that renders it's content into JSON.
|
||||
"""
|
||||
|
||||
def __init__(self, data, **kwargs):
|
||||
content = JSONRenderer().render(data)
|
||||
kwargs['content_type'] = 'application/json'
|
||||
super(JSONResponse, self).__init__(content, **kwargs)
|
||||
|
||||
|
||||
The root of our API is going to be a view that supports listing all the existing comments, or creating a new comment.
|
||||
The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet.
|
||||
|
||||
@csrf_exempt
|
||||
def comment_root(request):
|
||||
def snippet_list(request):
|
||||
"""
|
||||
List all comments, or create a new comment.
|
||||
List all code snippets, or create a new snippet.
|
||||
"""
|
||||
if request.method == 'GET':
|
||||
comments = Comment.objects.all()
|
||||
serializer = CommentSerializer(instance=comments)
|
||||
snippets = Snippet.objects.all()
|
||||
serializer = SnippetSerializer(snippets)
|
||||
return JSONResponse(serializer.data)
|
||||
|
||||
elif request.method == 'POST':
|
||||
data = JSONParser().parse(request)
|
||||
serializer = CommentSerializer(data)
|
||||
serializer = SnippetSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return JSONResponse(serializer.data, status=201)
|
||||
else:
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
|
||||
Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
|
||||
|
||||
We'll also need a view which corresponds to an individual comment, and can be used to retrieve, update or delete the comment.
|
||||
We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet.
|
||||
|
||||
@csrf_exempt
|
||||
def comment_instance(request, pk):
|
||||
def snippet_detail(request, pk):
|
||||
"""
|
||||
Retrieve, update or delete a comment instance.
|
||||
Retrieve, update or delete a code snippet.
|
||||
"""
|
||||
try:
|
||||
comment = Comment.objects.get(pk=pk)
|
||||
except Comment.DoesNotExist:
|
||||
snippet = Snippet.objects.get(pk=pk)
|
||||
except Snippet.DoesNotExist:
|
||||
return HttpResponse(status=404)
|
||||
|
||||
if request.method == 'GET':
|
||||
serializer = CommentSerializer(instance=comment)
|
||||
serializer = SnippetSerializer(snippet)
|
||||
return JSONResponse(serializer.data)
|
||||
|
||||
elif request.method == 'PUT':
|
||||
data = JSONParser().parse(request)
|
||||
serializer = CommentSerializer(data, instance=comment)
|
||||
serializer = SnippetSerializer(snippet, data=data)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return JSONResponse(serializer.data)
|
||||
else:
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
comment.delete()
|
||||
snippet.delete()
|
||||
return HttpResponse(status=204)
|
||||
|
||||
Finally we need to wire these views up. Create the `blog/urls.py` file:
|
||||
Finally we need to wire these views up. Create the `snippets/urls.py` file:
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns('blog.views',
|
||||
url(r'^$', 'comment_root'),
|
||||
url(r'^(?P<pk>[0-9]+)$', 'comment_instance')
|
||||
urlpatterns = patterns('snippets.views',
|
||||
url(r'^snippets/$', 'snippet_list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
|
||||
)
|
||||
|
||||
It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
|
||||
|
@ -257,5 +308,8 @@ Our API views don't do anything particularly special at the moment, beyond serve
|
|||
|
||||
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
||||
|
||||
[quickstart]: quickstart.md
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[virtualenv]: http://www.virtualenv.org/en/latest/index.html
|
||||
[tut-2]: 2-requests-and-responses.md
|
||||
|
|
|
@ -5,7 +5,7 @@ Let's introduce a couple of essential building blocks.
|
|||
|
||||
## Request objects
|
||||
|
||||
REST framework intoduces 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.DATA # Handles arbitrary data. Works any HTTP request with content.
|
||||
|
@ -38,27 +38,27 @@ Okay, let's go ahead and start using these new components to write a few views.
|
|||
|
||||
We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def comment_root(request):
|
||||
def snippet_list(request):
|
||||
"""
|
||||
List all comments, or create a new comment.
|
||||
List all snippets, or create a new snippet.
|
||||
"""
|
||||
if request.method == 'GET':
|
||||
comments = Comment.objects.all()
|
||||
serializer = CommentSerializer(instance=comments)
|
||||
snippets = Snippet.objects.all()
|
||||
serializer = SnippetSerializer(snippets)
|
||||
return Response(serializer.data)
|
||||
|
||||
elif request.method == 'POST':
|
||||
serializer = CommentSerializer(request.DATA)
|
||||
serializer = SnippetSerializer(data=request.DATA)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
@ -67,30 +67,29 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
|||
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
|
||||
|
||||
@api_view(['GET', 'PUT', 'DELETE'])
|
||||
def comment_instance(request, pk):
|
||||
def snippet_detail(request, pk):
|
||||
"""
|
||||
Retrieve, update or delete a comment instance.
|
||||
Retrieve, update or delete a snippet instance.
|
||||
"""
|
||||
try:
|
||||
comment = Comment.objects.get(pk=pk)
|
||||
except Comment.DoesNotExist:
|
||||
snippet = Snippet.objects.get(pk=pk)
|
||||
except Snippet.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if request.method == 'GET':
|
||||
serializer = CommentSerializer(instance=comment)
|
||||
serializer = SnippetSerializer(snippet)
|
||||
return Response(serializer.data)
|
||||
|
||||
elif request.method == 'PUT':
|
||||
serializer = CommentSerializer(request.DATA, instance=comment)
|
||||
serializer = SnippetSerializer(snippet, data=request.DATA)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
comment.delete()
|
||||
snippet.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
This should all feel very familiar - there's not a lot different to working with regular Django views.
|
||||
|
@ -103,20 +102,20 @@ To take advantage of the fact that our responses are no longer hardwired to a si
|
|||
|
||||
Start by adding a `format` keyword argument to both of the views, like so.
|
||||
|
||||
def comment_root(request, format=None):
|
||||
def snippet_list(request, format=None):
|
||||
|
||||
and
|
||||
|
||||
def comment_instance(request, pk, format=None):
|
||||
def snippet_detail(request, pk, format=None):
|
||||
|
||||
Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs.
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
urlpatterns = patterns('blog.views',
|
||||
url(r'^$', 'comment_root'),
|
||||
url(r'^(?P<pk>[0-9]+)$', 'comment_instance')
|
||||
urlpatterns = patterns('snippet.views',
|
||||
url(r'^snippets/$', 'snippet_list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
|
||||
)
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
@ -129,9 +128,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
|
|||
|
||||
**TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
|
||||
|
||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/][devserver]."
|
||||
|
||||
**Note: Right now the Browseable API only works with the CBV's. Need to fix that.**
|
||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]."
|
||||
|
||||
### Browsability
|
||||
|
||||
|
@ -145,7 +142,7 @@ See the [browsable api][browseable-api] topic for more information about the bro
|
|||
In [tutorial part 3][tut-3], we'll start using class based views, and see how generic views reduce the amount of code we need to write.
|
||||
|
||||
[json-url]: http://example.com/api/items/4.json
|
||||
[devserver]: http://127.0.0.1:8000/
|
||||
[devserver]: http://127.0.0.1:8000/snippets/
|
||||
[browseable-api]: ../topics/browsable-api.md
|
||||
[tut-1]: 1-serialization.md
|
||||
[tut-3]: 3-class-based-views.md
|
||||
|
|
|
@ -6,61 +6,58 @@ We can also write our API views using class based views, rather than function ba
|
|||
|
||||
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from django.http import Http404
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class CommentRoot(APIView):
|
||||
class SnippetList(APIView):
|
||||
"""
|
||||
List all comments, or create a new comment.
|
||||
List all snippets, or create a new snippet.
|
||||
"""
|
||||
def get(self, request, format=None):
|
||||
comments = Comment.objects.all()
|
||||
serializer = CommentSerializer(instance=comments)
|
||||
snippets = Snippet.objects.all()
|
||||
serializer = SnippetSerializer(snippets)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, format=None):
|
||||
serializer = CommentSerializer(request.DATA)
|
||||
serializer = SnippetSerializer(data=request.DATA)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view.
|
||||
|
||||
class CommentInstance(APIView):
|
||||
class SnippetDetail(APIView):
|
||||
"""
|
||||
Retrieve, update or delete a comment instance.
|
||||
Retrieve, update or delete a snippet instance.
|
||||
"""
|
||||
|
||||
def get_object(self, pk):
|
||||
try:
|
||||
return Comment.objects.get(pk=pk)
|
||||
except Comment.DoesNotExist:
|
||||
return Snippet.objects.get(pk=pk)
|
||||
except Snippet.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
def get(self, request, pk, format=None):
|
||||
comment = self.get_object(pk)
|
||||
serializer = CommentSerializer(instance=comment)
|
||||
snippet = self.get_object(pk)
|
||||
serializer = SnippetSerializer(snippet)
|
||||
return Response(serializer.data)
|
||||
|
||||
def put(self, request, pk, format=None):
|
||||
comment = self.get_object(pk)
|
||||
serializer = CommentSerializer(request.DATA, instance=comment)
|
||||
snippet = self.get_object(pk)
|
||||
serializer = SnippetSerializer(snippet, data=request.DATA)
|
||||
if serializer.is_valid():
|
||||
comment = serializer.object
|
||||
comment.save()
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, pk, format=None):
|
||||
comment = self.get_object(pk)
|
||||
comment.delete()
|
||||
snippet = self.get_object(pk)
|
||||
snippet.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
That's looking good. Again, it's still pretty similar to the function based view right now.
|
||||
|
@ -69,11 +66,11 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
|
|||
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
from blogpost import views
|
||||
from snippetpost import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views.CommentRoot.as_view()),
|
||||
url(r'^(?P<pk>[0-9]+)$', views.CommentInstance.as_view())
|
||||
url(r'^snippets/$', views.SnippetList.as_view()),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view())
|
||||
)
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
@ -88,16 +85,16 @@ The create/retrieve/update/delete operations that we've been using so far are go
|
|||
|
||||
Let's take a look at how we can compose our views by using the mixin classes.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from rest_framework import mixins
|
||||
from rest_framework import generics
|
||||
|
||||
class CommentRoot(mixins.ListModelMixin,
|
||||
class SnippetList(mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
generics.MultipleObjectBaseView):
|
||||
model = Comment
|
||||
serializer_class = CommentSerializer
|
||||
generics.MultipleObjectAPIView):
|
||||
model = Snippet
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.list(request, *args, **kwargs)
|
||||
|
@ -105,16 +102,16 @@ Let's take a look at how we can compose our views by using the mixin classes.
|
|||
def post(self, 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 `MultipleObjectBaseView`, and adding in `ListModelMixin` and `CreateModelMixin`.
|
||||
We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, 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 explictly 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.
|
||||
|
||||
class CommentInstance(mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
generics.SingleObjectBaseView):
|
||||
model = Comment
|
||||
serializer_class = CommentSerializer
|
||||
class SnippetDetail(mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
generics.SingleObjectBaseView):
|
||||
model = Snippet
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
@ -131,23 +128,23 @@ Pretty similar. This time we're using the `SingleObjectBaseView` class to provi
|
|||
|
||||
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
|
||||
|
||||
from blog.models import Comment
|
||||
from blog.serializers import CommentSerializer
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
|
||||
class CommentRoot(generics.ListCreateAPIView):
|
||||
model = Comment
|
||||
serializer_class = CommentSerializer
|
||||
class SnippetList(generics.ListCreateAPIView):
|
||||
model = Snippet
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
|
||||
class CommentInstance(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = Comment
|
||||
serializer_class = CommentSerializer
|
||||
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = Snippet
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django.
|
||||
|
||||
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can customize the behavior of our views to support a range of authentication, permissions, throttling and other aspects.
|
||||
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API.
|
||||
|
||||
[dry]: http://en.wikipedia.org/wiki/Don't_repeat_yourself
|
||||
[tut-4]: 4-authentication-permissions-and-throttling.md
|
||||
[tut-4]: 4-authentication-and-permissions.md
|
||||
|
|
193
docs/tutorial/4-authentication-and-permissions.md
Normal file
193
docs/tutorial/4-authentication-and-permissions.md
Normal file
|
@ -0,0 +1,193 @@
|
|||
# Tutorial 4: Authentication & Permissions
|
||||
|
||||
Currently our API doesn't have any restrictions on who can edit or delete code snippets. We'd like to have some more advanced behavior in order to make sure that:
|
||||
|
||||
* Code snippets are always associated with a creator.
|
||||
* Only authenticated users may create snippets.
|
||||
* Only the creator of a snippet may update or delete it.
|
||||
* Unauthenticated requests should have full read-only access.
|
||||
|
||||
## Adding information to our model
|
||||
|
||||
We're going to make a couple of changes to our `Snippet` model class.
|
||||
First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code.
|
||||
|
||||
Add the following two fields to the model.
|
||||
|
||||
owner = models.ForeignKey('auth.User', related_name='snippets')
|
||||
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'll need some extra imports:
|
||||
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from pygments import highlight
|
||||
|
||||
And now we can add a `.save()` method to our model class:
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Use the `pygments` library to create an highlighted HTML
|
||||
representation of the code snippet.
|
||||
"""
|
||||
lexer = get_lexer_by_name(self.language)
|
||||
linenos = self.linenos and 'table' or False
|
||||
options = self.title and {'title': self.title} or {}
|
||||
formatter = HtmlFormatter(style=self.style, linenos=linenos,
|
||||
full=True, **options)
|
||||
self.highlighted = highlight(self.code, lexer, formatter)
|
||||
super(Snippet, self).save(*args, **kwargs)
|
||||
|
||||
When that's all done we'll need to update our database tables.
|
||||
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
|
||||
|
||||
rm tmp.db
|
||||
python ./manage.py syncdb
|
||||
|
||||
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
|
||||
|
||||
python ./manage.py createsuperuser
|
||||
|
||||
## Adding endpoints for our User models
|
||||
|
||||
Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy:
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
snippets = serializers.ManyPrimaryKeyRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'snippets')
|
||||
|
||||
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it.
|
||||
|
||||
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
|
||||
|
||||
class UserList(generics.ListAPIView):
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
class UserInstance(generics.RetrieveAPIView):
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
|
||||
Finally we need to add those views into the API, by referencing them from the URL conf.
|
||||
|
||||
url(r'^users/$', views.UserList.as_view()),
|
||||
url(r'^users/(?P<pk>[0-9]+)/$', views.UserInstance.as_view())
|
||||
|
||||
## Associating Snippets with Users
|
||||
|
||||
Right now, if we created a code snippet, there'd be no way of associating the user that created the snippet, with the snippet instance. The user isn't sent as part of the serialized representation, but is instead a property of the incoming request.
|
||||
|
||||
The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL.
|
||||
|
||||
On **both** the `SnippetList` and `SnippetDetail` view classes, add the following method:
|
||||
|
||||
def pre_save(self, obj):
|
||||
obj.owner = self.request.user
|
||||
|
||||
## 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:
|
||||
|
||||
owner = serializers.Field(source='owner.username')
|
||||
|
||||
**Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class.
|
||||
|
||||
This field is doing something quite interesting. The `source` argument controls which attribute is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as it is used with Django's template language.
|
||||
|
||||
The field we've added is the untyped `Field` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `Field` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized.
|
||||
|
||||
**TODO: Explain the SessionAuthentication and BasicAuthentication classes, and demonstrate using HTTP basic authentication with curl requests**
|
||||
|
||||
## Adding required permissions to views
|
||||
|
||||
Now that code snippets are associated with users we want to make sure that only authenticated users are able to create, update and delete code snippets.
|
||||
|
||||
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.
|
||||
|
||||
First add the following import in the views module
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
Then, add the following property to **both** the `SnippetList` and `SnippetDetail` view classes.
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
**TODO: Now that the permissions are restricted, demonstrate using HTTP basic authentication with curl requests**
|
||||
|
||||
## Adding login to the Browseable API
|
||||
|
||||
If you open a browser and navigate to the browseable 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 browseable API, by editing our URLconf once more.
|
||||
|
||||
Add the following import at the top of the file:
|
||||
|
||||
from django.conf.urls import include
|
||||
|
||||
And, at the end of the file, add a pattern to include the login and logout views for the browseable API.
|
||||
|
||||
urlpatterns += patterns('',
|
||||
url(r'^api-auth/', include('rest_framework.urls',
|
||||
namespace='rest_framework'))
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Object level permissions
|
||||
|
||||
Really we'd like all code snippets to be visible to anyone, but also make sure that only the user that created a code snippet is able update or delete it.
|
||||
|
||||
To do that we're going to need to create a custom permission.
|
||||
|
||||
In the snippets app, create a new file, `permissions.py`
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class IsOwnerOrReadOnly(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission to only allow owners of an object to edit it.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view, obj=None):
|
||||
# Skip the check unless this is an object-level test
|
||||
if obj is None:
|
||||
return True
|
||||
|
||||
# Read permissions are allowed to any request
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions are only allowed to the owner of the snippet
|
||||
return obj.owner == request.user
|
||||
|
||||
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
|
||||
IsOwnerOrReadOnly,)
|
||||
|
||||
Make sure to also import the `IsOwnerOrReadOnly` class.
|
||||
|
||||
from snippets.permissions import IsOwnerOrReadOnly
|
||||
|
||||
Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions only appear on a snippet instance endpoint if you're logged in as the same user that created the code snippet.
|
||||
|
||||
## Summary
|
||||
|
||||
We've now got a fairly fine-grained set of permissions on our Web API, and end points for users of the system and for the code snippets that they have created.
|
||||
|
||||
In [part 5][tut-5] of the tutorial we'll look at how we can tie everything together by creating an HTML endpoint for our hightlighted snippets, and improve the cohesion of our API by using hyperlinking for the relationships within the system.
|
||||
|
||||
[tut-5]: 5-relationships-and-hyperlinked-apis.md
|
|
@ -1,5 +0,0 @@
|
|||
# Tutorial 4: Authentication & Permissions
|
||||
|
||||
Nothing to see here. Onwards to [part 5][tut-5].
|
||||
|
||||
[tut-5]: 5-relationships-and-hyperlinked-apis.md
|
|
@ -1,11 +1,176 @@
|
|||
# Tutorial 5 - Relationships & Hyperlinked APIs
|
||||
|
||||
**TODO**
|
||||
At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
|
||||
|
||||
* Create BlogPost model
|
||||
* Demonstrate nested relationships
|
||||
* Demonstrate and describe hyperlinked relationships
|
||||
## Creating an endpoint for the root of our API
|
||||
|
||||
<!-- Onwards to [part 6][tut-6].
|
||||
Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier.
|
||||
|
||||
[tut-6]: 6-resource-orientated-projects.md -->
|
||||
from rest_framework import renderers
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
@api_view(('GET',))
|
||||
def api_root(request, format=None):
|
||||
return Response({
|
||||
'users': reverse('user-list', request=request),
|
||||
'snippets': reverse('snippet-list', request=request)
|
||||
})
|
||||
|
||||
Notice that we're using REST framework's `reverse` function in order to return fully-qualified URLs.
|
||||
|
||||
## Creating an endpoint for the highlighted snippets
|
||||
|
||||
The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.
|
||||
|
||||
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
|
||||
|
||||
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:
|
||||
|
||||
from rest_framework import renderers
|
||||
from rest_framework.response import Response
|
||||
|
||||
class SnippetHighlight(generics.SingleObjectAPIView):
|
||||
model = Snippet
|
||||
renderer_classes = (renderers.StaticHTMLRenderer,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
snippet = self.get_object()
|
||||
return Response(snippet.highlighted)
|
||||
|
||||
As usual we need to add the new views that we've created in to our URLconf.
|
||||
We'll add a url pattern for our new API root:
|
||||
|
||||
url(r'^$', 'api_root'),
|
||||
|
||||
And then add a url pattern for the snippet highlights:
|
||||
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
|
||||
|
||||
## Hyperlinking our API
|
||||
|
||||
Dealing with relationships between entities is one of the more challenging aspects of Web API design. There are a number of different ways that we might choose to represent a relationship:
|
||||
|
||||
* Using primary keys.
|
||||
* Using hyperlinking between entities.
|
||||
* Using a unique identifying slug field on the related entity.
|
||||
* Using the default string representation of the related entity.
|
||||
* Nesting the related entity inside the parent representation.
|
||||
* Some other custom representation.
|
||||
|
||||
REST framework supports all of these styles, and can apply them across forward or reverse relationships, or apply them across custom managers such as generic foreign keys.
|
||||
|
||||
In this case we'd like to use a hyperlinked style between entities. In order to do so, we'll modify our serializers to extend `HyperlinkedModelSerializer` instead of the existing `ModelSerializer`.
|
||||
|
||||
The `HyperlinkedModelSerializer` has the following differences from `ModelSerializer`:
|
||||
|
||||
* It does not include the `pk` field by default.
|
||||
* It includes a `url` field, using `HyperlinkedIdentityField`.
|
||||
* Relationships use `HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField`,
|
||||
instead of `PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField`.
|
||||
|
||||
We can easily re-write our existing serializers to use hyperlinking.
|
||||
|
||||
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
|
||||
owner = serializers.Field(source='owner.username')
|
||||
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
|
||||
|
||||
class Meta:
|
||||
model = models.Snippet
|
||||
fields = ('url', 'highlight', 'owner',
|
||||
'title', 'code', 'linenos', 'language', 'style')
|
||||
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
snippets = serializers.ManyHyperlinkedRelatedField(view_name='snippet-detail')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('url', 'username', 'snippets')
|
||||
|
||||
Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern.
|
||||
|
||||
Because we've included format suffixed URLs such as `'.json'`, we also need to indicate on the `highlight` field that any format suffixed hyperlinks it returns should use the `'.html'` suffix.
|
||||
|
||||
## Making sure our URL patterns are named
|
||||
|
||||
If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name.
|
||||
|
||||
* The root of our API refers to `'user-list'` and `'snippet-list'`.
|
||||
* Our snippet serializer includes a field that refers to `'snippet-highlight'`.
|
||||
* Our user serializer includes a field that refers to `'snippet-detail'`.
|
||||
* Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`.
|
||||
|
||||
After adding all those names into our URLconf, our final `'urls.py'` file should look something like this:
|
||||
|
||||
# API endpoints
|
||||
urlpatterns = format_suffix_patterns(patterns('snippets.views',
|
||||
url(r'^$', 'api_root'),
|
||||
url(r'^snippets/$',
|
||||
views.SnippetList.as_view(),
|
||||
name='snippet-list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/$',
|
||||
views.SnippetDetail.as_view(),
|
||||
name='snippet-detail'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$'
|
||||
views.SnippetHighlight.as_view(),
|
||||
name='snippet-highlight'),
|
||||
url(r'^users/$',
|
||||
views.UserList.as_view(),
|
||||
name='user-list'),
|
||||
url(r'^users/(?P<pk>[0-9]+)/$',
|
||||
views.UserInstance.as_view(),
|
||||
name='user-detail')
|
||||
))
|
||||
|
||||
# Login and logout views for the browsable API
|
||||
urlpatterns += patterns('',
|
||||
url(r'^api-auth/', include('rest_framework.urls',
|
||||
namespace='rest_framework'))
|
||||
)
|
||||
|
||||
## Adding pagination
|
||||
|
||||
The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages.
|
||||
|
||||
We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATE_BY': 10
|
||||
}
|
||||
|
||||
Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well seperated from your other project settings.
|
||||
|
||||
We could also customize the pagination style if we needed too, but in this case we'll just stick with the default.
|
||||
|
||||
## Reviewing our work
|
||||
|
||||
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
|
||||
|
||||
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the hightlighted code HTML representations.
|
||||
|
||||
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
||||
|
||||
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
|
||||
|
||||
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
|
||||
|
||||
## Onwards and upwards
|
||||
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
||||
|
||||
* Contribute on [GitHub][github] by reviewing and subitting issues, and making pull requests.
|
||||
* Join the [REST framework discussion group][group], and help build the community.
|
||||
* Follow the author [on Twitter][twitter] and say hi.
|
||||
|
||||
**Now go build awesome things.**
|
||||
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[twitter]: https://twitter.com/_tomchristie
|
|
@ -1,76 +0,0 @@
|
|||
# Tutorial 6 - Resources
|
||||
|
||||
Resource classes are just View classes that don't have any handler methods bound to them. The actions on a resource are defined,
|
||||
|
||||
This allows us to:
|
||||
|
||||
* Encapsulate common behaviour across a class of views, in a single Resource class.
|
||||
* Separate out the actions of a Resource from the specfics of how those actions should be bound to a particular set of URLs.
|
||||
|
||||
## Refactoring to use Resources, not Views
|
||||
|
||||
For instance, we can re-write our 4 sets of views into something more compact...
|
||||
|
||||
resources.py
|
||||
|
||||
class BlogPostResource(ModelResource):
|
||||
serializer_class = BlogPostSerializer
|
||||
model = BlogPost
|
||||
permissions_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
throttle_classes = (throttles.UserRateThrottle,)
|
||||
|
||||
class CommentResource(ModelResource):
|
||||
serializer_class = CommentSerializer
|
||||
model = Comment
|
||||
permissions_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
throttle_classes = (throttles.UserRateThrottle,)
|
||||
|
||||
## Binding Resources to URLs explicitly
|
||||
The handler methods only get bound to the actions when we define the URLConf. Here's our urls.py:
|
||||
|
||||
comment_root = CommentResource.as_view(actions={
|
||||
'get': 'list',
|
||||
'post': 'create'
|
||||
})
|
||||
comment_instance = CommentInstance.as_view(actions={
|
||||
'get': 'retrieve',
|
||||
'put': 'update',
|
||||
'delete': 'destroy'
|
||||
})
|
||||
... # And for blog post
|
||||
|
||||
urlpatterns = patterns('blogpost.views',
|
||||
url(r'^$', comment_root),
|
||||
url(r'^(?P<pk>[0-9]+)$', comment_instance)
|
||||
... # And for blog post
|
||||
)
|
||||
|
||||
## Using Routers
|
||||
|
||||
Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
|
||||
|
||||
from blog import resources
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(resources.BlogPostResource)
|
||||
router.register(resources.CommentResource)
|
||||
urlpatterns = router.urlpatterns
|
||||
|
||||
## Trade-offs between views vs resources.
|
||||
|
||||
Writing resource-oriented code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, and minimises the amount of code you need to write.
|
||||
|
||||
The trade-off is that the behaviour is less explict. It can be more difficult to determine what code path is being followed, or where to override some behaviour.
|
||||
|
||||
## Onwards and upwards.
|
||||
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
||||
|
||||
* Contribute on GitHub by reviewing issues, and submitting issues or pull requests.
|
||||
* Join the REST framework group, and help build the community.
|
||||
* Follow me [on Twitter][twitter] and say hi.
|
||||
|
||||
**Now go build some awesome things.**
|
||||
|
||||
[twitter]: https://twitter.com/_tomchristie
|
|
@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
|
|||
|
||||
|
||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||
permissions = serializers.ManySlugRelatedField(
|
||||
slug_field='codename',
|
||||
queryset=Permission.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ('url', 'name', 'permissions')
|
||||
|
||||
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
|
||||
|
||||
We've also overridden the `permission` field on the `GroupSerializer`. In this case we don't want to use a hyperlinked representation, but instead use the list of permission codenames associated with the group, so we've used a `ManySlugRelatedField`, using the `codename` field for the representation.
|
||||
|
||||
## Views
|
||||
|
||||
Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
|
||||
|
@ -126,7 +133,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
|
|||
)
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSIONS': ('rest_framework.permissions.IsAdminUser',),
|
||||
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',),
|
||||
'PAGINATE_BY': 10
|
||||
}
|
||||
|
||||
|
@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
|
|||
},
|
||||
{
|
||||
"email": "tom@example.com",
|
||||
"groups": [],
|
||||
"groups": [ ],
|
||||
"url": "http://127.0.0.1:8000/users/2/",
|
||||
"username": "tom"
|
||||
}
|
||||
|
@ -169,4 +176,4 @@ If you want to get a more in depth understanding of how REST framework fits toge
|
|||
|
||||
[image]: ../img/quickstart.png
|
||||
[tutorial]: 1-serialization.md
|
||||
[guide]: ../#api-guide
|
||||
[guide]: ../#api-guide
|
||||
|
|
|
@ -17,14 +17,14 @@ if local:
|
|||
suffix = '.html'
|
||||
index = 'index.html'
|
||||
else:
|
||||
base_url = 'http://tomchristie.github.com/django-rest-framework'
|
||||
suffix = ''
|
||||
base_url = 'http://django-rest-framework.org'
|
||||
suffix = '.html'
|
||||
index = ''
|
||||
|
||||
|
||||
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
|
||||
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
|
||||
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/blob/restframework2/rest_framework/\1"><span class="label label-info">\1</span></a>'
|
||||
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/\1"><span class="label label-info">\1</span></a>'
|
||||
|
||||
page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '2.0.0'
|
||||
__version__ = '2.1.2'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
"""
|
||||
The :mod:`compat` module provides support for backwards compatibility with older versions of django/python.
|
||||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of django/python, and compatbility wrappers around optional packages.
|
||||
"""
|
||||
# flake8: noqa
|
||||
import django
|
||||
|
||||
# django-filter is optional
|
||||
try:
|
||||
import django_filters
|
||||
except:
|
||||
django_filters = None
|
||||
|
||||
|
||||
# cStringIO only if it's available, otherwise StringIO
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
|
@ -346,33 +355,6 @@ except ImportError:
|
|||
yaml = None
|
||||
|
||||
|
||||
import unittest
|
||||
try:
|
||||
import unittest.skip
|
||||
except ImportError: # python < 2.7
|
||||
from unittest import TestCase
|
||||
import functools
|
||||
|
||||
def skip(reason):
|
||||
# Pasted from py27/lib/unittest/case.py
|
||||
"""
|
||||
Unconditionally skip a test.
|
||||
"""
|
||||
def decorator(test_item):
|
||||
if not (isinstance(test_item, type) and issubclass(test_item, TestCase)):
|
||||
@functools.wraps(test_item)
|
||||
def skip_wrapper(*args, **kwargs):
|
||||
pass
|
||||
test_item = skip_wrapper
|
||||
|
||||
test_item.__unittest_skip__ = True
|
||||
test_item.__unittest_skip_why__ = reason
|
||||
return test_item
|
||||
return decorator
|
||||
|
||||
unittest.skip = skip
|
||||
|
||||
|
||||
# xml.etree.parse only throws ParseError for python >= 2.7
|
||||
try:
|
||||
from xml.etree import ParseError as ETParseError
|
||||
|
|
|
@ -10,8 +10,18 @@ def api_view(http_method_names):
|
|||
|
||||
def decorator(func):
|
||||
|
||||
class WrappedAPIView(APIView):
|
||||
pass
|
||||
WrappedAPIView = type(
|
||||
'WrappedAPIView',
|
||||
(APIView,),
|
||||
{'__doc__': func.__doc__}
|
||||
)
|
||||
|
||||
# Note, the above allows us to set the docstring.
|
||||
# It is the equivelent of:
|
||||
#
|
||||
# class WrappedAPIView(APIView):
|
||||
# pass
|
||||
# WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
|
||||
|
||||
allowed_methods = set(http_method_names) | set(('options',))
|
||||
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
|
||||
|
|
|
@ -47,14 +47,6 @@ class PermissionDenied(APIException):
|
|||
self.detail = detail or self.default_detail
|
||||
|
||||
|
||||
class InvalidFormat(APIException):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
default_detail = "Format suffix '.%s' not found."
|
||||
|
||||
def __init__(self, format, detail=None):
|
||||
self.detail = (detail or self.default_detail) % format
|
||||
|
||||
|
||||
class MethodNotAllowed(APIException):
|
||||
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
default_detail = "Method '%s' not allowed."
|
||||
|
|
|
@ -5,13 +5,16 @@ import warnings
|
|||
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from django.conf import settings
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import is_protected_type, smart_unicode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.compat import parse_date, parse_datetime
|
||||
from rest_framework.compat import timezone
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
def is_simple_callable(obj):
|
||||
|
@ -37,12 +40,12 @@ class Field(object):
|
|||
|
||||
self.source = source
|
||||
|
||||
def initialize(self, parent):
|
||||
def initialize(self, parent, field_name):
|
||||
"""
|
||||
Called to set up a field prior to field_to_native or field_from_native.
|
||||
|
||||
parent - The parent serializer.
|
||||
model_field - The model field this field corrosponds to, if one exists.
|
||||
model_field - The model field this field corresponds to, if one exists.
|
||||
"""
|
||||
self.parent = parent
|
||||
self.root = parent.root or parent
|
||||
|
@ -70,6 +73,8 @@ class Field(object):
|
|||
value = obj
|
||||
for component in self.source.split('.'):
|
||||
value = getattr(value, component)
|
||||
if is_simple_callable(value):
|
||||
value = value()
|
||||
else:
|
||||
value = getattr(obj, field_name)
|
||||
return self.to_native(value)
|
||||
|
@ -85,6 +90,8 @@ class Field(object):
|
|||
return value
|
||||
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
|
||||
return [self.to_native(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
return dict(map(self.to_native, (k, v)) for k, v in value.items())
|
||||
return smart_unicode(value)
|
||||
|
||||
def attributes(self):
|
||||
|
@ -105,15 +112,20 @@ class WritableField(Field):
|
|||
'required': _('This field is required.'),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
widget = widgets.TextInput
|
||||
default = None
|
||||
|
||||
def __init__(self, source=None, read_only=False, required=None,
|
||||
validators=[], error_messages=None, widget=None,
|
||||
default=None, blank=None):
|
||||
|
||||
def __init__(self, source=None, readonly=False, required=None,
|
||||
validators=[], error_messages=None):
|
||||
super(WritableField, self).__init__(source=source)
|
||||
self.readonly = readonly
|
||||
|
||||
self.read_only = read_only
|
||||
if required is None:
|
||||
self.required = not(readonly)
|
||||
self.required = not(read_only)
|
||||
else:
|
||||
assert not readonly, "Cannot set required=True and readonly=True"
|
||||
assert not read_only, "Cannot set required=True and read_only=True"
|
||||
self.required = required
|
||||
|
||||
messages = {}
|
||||
|
@ -123,6 +135,14 @@ class WritableField(Field):
|
|||
self.error_messages = messages
|
||||
|
||||
self.validators = self.default_validators + validators
|
||||
self.default = default if default is not None else self.default
|
||||
self.blank = blank
|
||||
|
||||
# Widgets are ony used for HTML forms.
|
||||
widget = widget or self.widget
|
||||
if isinstance(widget, type):
|
||||
widget = widget()
|
||||
self.widget = widget
|
||||
|
||||
def validate(self, value):
|
||||
if value in validators.EMPTY_VALUES and self.required:
|
||||
|
@ -151,15 +171,18 @@ class WritableField(Field):
|
|||
Given a dictionary and a field name, updates the dictionary `into`,
|
||||
with the field and it's deserialized value.
|
||||
"""
|
||||
if self.readonly:
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
native = data[field_name]
|
||||
except KeyError:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
if self.default is not None:
|
||||
native = self.default
|
||||
else:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
|
||||
value = self.from_native(native)
|
||||
if self.source == '*':
|
||||
|
@ -179,7 +202,7 @@ class WritableField(Field):
|
|||
|
||||
class ModelField(WritableField):
|
||||
"""
|
||||
A generic field that can be used against an arbirtrary model field.
|
||||
A generic field that can be used against an arbitrary model field.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
|
@ -189,11 +212,11 @@ class ModelField(WritableField):
|
|||
super(ModelField, self).__init__(*args, **kwargs)
|
||||
|
||||
def from_native(self, value):
|
||||
try:
|
||||
rel = self.model_field.rel
|
||||
except:
|
||||
rel = getattr(self.model_field, "rel", None)
|
||||
if rel is not None:
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
else:
|
||||
return self.model_field.to_python(value)
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = self.model_field._get_val_from_obj(obj)
|
||||
|
@ -209,32 +232,119 @@ class ModelField(WritableField):
|
|||
##### Relational fields #####
|
||||
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
If not overridden, this represents a to-one relationship, using the unicode
|
||||
representation of the target.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
default_read_only = True # TODO: Remove this
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
try:
|
||||
manager = getattr(self.parent.opts.model, self.source or field_name)
|
||||
if hasattr(manager, 'related'): # Forward
|
||||
self.queryset = manager.related.model._default_manager.all()
|
||||
else: # Reverse
|
||||
self.queryset = manager.field.rel.to._default_manager.all()
|
||||
except:
|
||||
raise
|
||||
msg = ('Serializer related fields must include a `queryset`' +
|
||||
' argument or set `read_only=True')
|
||||
raise Exception(msg)
|
||||
|
||||
### We need this stuff to make form choices work...
|
||||
|
||||
# def __deepcopy__(self, memo):
|
||||
# result = super(RelatedField, self).__deepcopy__(memo)
|
||||
# result.queryset = result.queryset
|
||||
# return result
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def _get_queryset(self):
|
||||
return self._queryset
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self.widget.choices = self.choices
|
||||
|
||||
queryset = property(_get_queryset, _set_queryset)
|
||||
|
||||
def _get_choices(self):
|
||||
# If self._choices is set, then somebody must have manually set
|
||||
# the property self.choices. In this case, just return self._choices.
|
||||
if hasattr(self, '_choices'):
|
||||
return self._choices
|
||||
|
||||
# Otherwise, execute the QuerySet in self.queryset to determine the
|
||||
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
||||
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
|
||||
# time _get_choices() is called (and, thus, each time self.choices is
|
||||
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
||||
# construct might look complicated but it allows for lazy evaluation of
|
||||
# the queryset.
|
||||
return ModelChoiceIterator(self)
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
### Regular serializier stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return self.to_native(value)
|
||||
|
||||
def field_from_native(self, data, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
value = data.get(field_name)
|
||||
into[(self.source or field_name) + '_id'] = self.from_native(value)
|
||||
into[(self.source or field_name)] = self.from_native(value)
|
||||
|
||||
|
||||
class ManyRelatedMixin(object):
|
||||
"""
|
||||
Mixin to convert a related field to a many related field.
|
||||
"""
|
||||
widget = widgets.SelectMultiple
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
|
||||
def field_from_native(self, data, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
# Form data
|
||||
value = data.getlist(self.source or field_name)
|
||||
|
@ -250,6 +360,9 @@ class ManyRelatedMixin(object):
|
|||
class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
||||
"""
|
||||
Base class for related model managers.
|
||||
|
||||
If not overridden, this represents a to-many relationship, using the unicode
|
||||
representations of the target, and is read-only.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -258,12 +371,38 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
|||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Serializes a related field or related object to a pk value.
|
||||
Represents a to-one relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
|
||||
# TODO: Remove these field hacks...
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
# TODO: Possibly change this to just take `obj`, through prob less performant
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
|
@ -278,8 +417,23 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
|
||||
class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
||||
"""
|
||||
Serializes a to-many related field or related manager to a pk value.
|
||||
Represents a to-many relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
|
@ -294,27 +448,83 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
|||
# Forward relationship
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop('slug_field', None)
|
||||
assert self.slug_field, 'slug_field is required'
|
||||
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
return getattr(obj, self.slug_field)
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(**{self.slug_field: data})
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Object with %s=%s does not exist.' %
|
||||
(self.slug_field, unicode(data)))
|
||||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
pass
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_url_kwarg = 'slug'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
default_read_only = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop('view_name')
|
||||
except:
|
||||
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
||||
|
||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||
|
||||
self.format = kwargs.pop('format', None)
|
||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_slug_field(self):
|
||||
"""
|
||||
Get the name of a slug field to be used to look up by slug.
|
||||
"""
|
||||
return self.slug_field
|
||||
|
||||
def to_native(self, obj):
|
||||
view_name = self.view_name
|
||||
request = self.context.get('request', None)
|
||||
format = self.format or self.context.get('format', None)
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request)
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -325,13 +535,13 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request)
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request)
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -340,6 +550,16 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
def from_native(self, value):
|
||||
# Convert URL -> model instance pk
|
||||
# TODO: Use values_list
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
if value.startswith('http:') or value.startswith('https:'):
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse(value).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = '/' + value[len(prefix):]
|
||||
|
||||
try:
|
||||
match = resolve(value)
|
||||
except:
|
||||
|
@ -353,7 +573,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
# Try explicit primary key.
|
||||
if pk is not None:
|
||||
return pk
|
||||
queryset = self.queryset.filter(pk=pk)
|
||||
# Next, try looking up by slug.
|
||||
elif slug is not None:
|
||||
slug_field = self.get_slug_field()
|
||||
|
@ -366,48 +586,88 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
obj = queryset.get()
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Invalid hyperlink - object does not exist.')
|
||||
return obj.pk
|
||||
return obj
|
||||
|
||||
|
||||
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship, using hyperlinking.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
A field that represents the model's identity using a hyperlink.
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer
|
||||
# set it on-the-fly
|
||||
# TODO: Make view_name mandatory, and have the
|
||||
# HyperlinkedModelSerializer set it on-the-fly
|
||||
self.view_name = kwargs.pop('view_name', None)
|
||||
self.format = kwargs.pop('format', None)
|
||||
|
||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||
|
||||
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
request = self.context.get('request', None)
|
||||
format = self.format or self.context.get('format', None)
|
||||
view_name = self.view_name or self.parent.opts.view_name
|
||||
view_kwargs = {'pk': obj.pk}
|
||||
return reverse(view_name, kwargs=view_kwargs, request=request)
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
|
||||
|
||||
|
||||
##### Typed Fields #####
|
||||
|
||||
class BooleanField(WritableField):
|
||||
type_name = 'BooleanField'
|
||||
widget = widgets.CheckboxInput
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value must be either True or False."),
|
||||
}
|
||||
empty = False
|
||||
|
||||
# Note: we set default to `False` in order to fill in missing value not
|
||||
# supplied by html form. TODO: Fix so that only html form input gets
|
||||
# this behavior.
|
||||
default = False
|
||||
|
||||
def from_native(self, value):
|
||||
if value in (True, False):
|
||||
# if value is 1 or 0 than it's equal to True or False, but we want
|
||||
# to return a true bool for semantic reasons.
|
||||
return bool(value)
|
||||
if value in ('t', 'True', '1'):
|
||||
return True
|
||||
if value in ('f', 'False', '0'):
|
||||
return False
|
||||
raise ValidationError(self.error_messages['invalid'] % value)
|
||||
return bool(value)
|
||||
|
||||
|
||||
class CharField(WritableField):
|
||||
|
@ -421,12 +681,68 @@ class CharField(WritableField):
|
|||
if max_length is not None:
|
||||
self.validators.append(validators.MaxLengthValidator(max_length))
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Validates that the value is supplied (if required).
|
||||
"""
|
||||
# if empty string and allow blank
|
||||
if self.blank and not value:
|
||||
return
|
||||
else:
|
||||
super(CharField, self).validate(value)
|
||||
|
||||
def from_native(self, value):
|
||||
if isinstance(value, basestring) or value is None:
|
||||
return value
|
||||
return smart_unicode(value)
|
||||
|
||||
|
||||
class ChoiceField(WritableField):
|
||||
type_name = 'ChoiceField'
|
||||
widget = widgets.Select
|
||||
default_error_messages = {
|
||||
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
|
||||
}
|
||||
|
||||
def __init__(self, choices=(), *args, **kwargs):
|
||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||
self.choices = choices
|
||||
|
||||
def _get_choices(self):
|
||||
return self._choices
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Validates that the input is in self.choices.
|
||||
"""
|
||||
super(ChoiceField, self).validate(value)
|
||||
if value and not self.valid_value(value):
|
||||
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
|
||||
|
||||
def valid_value(self, value):
|
||||
"""
|
||||
Check to see if the provided value is a valid choice.
|
||||
"""
|
||||
for k, v in self.choices:
|
||||
if isinstance(v, (list, tuple)):
|
||||
# This is an optgroup, so look inside the group for options
|
||||
for k2, v2 in v:
|
||||
if value == smart_unicode(k2):
|
||||
return True
|
||||
else:
|
||||
if value == smart_unicode(k):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class EmailField(CharField):
|
||||
type_name = 'EmailField'
|
||||
|
||||
|
@ -436,7 +752,10 @@ class EmailField(CharField):
|
|||
default_validators = [validators.validate_email]
|
||||
|
||||
def from_native(self, value):
|
||||
return super(EmailField, self).from_native(value).strip()
|
||||
ret = super(EmailField, self).from_native(value)
|
||||
if ret is None:
|
||||
return None
|
||||
return ret.strip()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
result = copy.copy(self)
|
||||
|
@ -458,8 +777,9 @@ class DateField(WritableField):
|
|||
empty = None
|
||||
|
||||
def from_native(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
if timezone and settings.USE_TZ and timezone.is_aware(value):
|
||||
# Convert aware datetimes to the default time zone
|
||||
|
@ -497,8 +817,9 @@ class DateTimeField(WritableField):
|
|||
empty = None
|
||||
|
||||
def from_native(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
|
@ -556,6 +877,7 @@ class IntegerField(WritableField):
|
|||
def from_native(self, value):
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
try:
|
||||
value = int(str(value))
|
||||
except (ValueError, TypeError):
|
||||
|
@ -571,8 +893,9 @@ class FloatField(WritableField):
|
|||
}
|
||||
|
||||
def from_native(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
|
|
59
rest_framework/filters.py
Normal file
59
rest_framework/filters.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from rest_framework.compat import django_filters
|
||||
|
||||
FilterSet = django_filters and django_filters.FilterSet or None
|
||||
|
||||
|
||||
class BaseFilterBackend(object):
|
||||
"""
|
||||
A base class from which all filter backend classes should inherit.
|
||||
"""
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
"""
|
||||
Return a filtered queryset.
|
||||
"""
|
||||
raise NotImplementedError(".filter_queryset() must be overridden.")
|
||||
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
"""
|
||||
A filter backend that uses django-filter.
|
||||
"""
|
||||
default_filter_set = FilterSet
|
||||
|
||||
def __init__(self):
|
||||
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
|
||||
|
||||
def get_filter_class(self, view):
|
||||
"""
|
||||
Return the django-filters `FilterSet` used to filter the queryset.
|
||||
"""
|
||||
filter_class = getattr(view, 'filter_class', None)
|
||||
filter_fields = getattr(view, 'filter_fields', None)
|
||||
view_model = getattr(view, 'model', None)
|
||||
|
||||
if filter_class:
|
||||
filter_model = filter_class.Meta.model
|
||||
|
||||
assert issubclass(filter_model, view_model), \
|
||||
'FilterSet model %s does not match view model %s' % \
|
||||
(filter_model, view_model)
|
||||
|
||||
return filter_class
|
||||
|
||||
if filter_fields:
|
||||
class AutoFilterSet(self.default_filter_set):
|
||||
class Meta:
|
||||
model = view_model
|
||||
fields = filter_fields
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
filter_class = self.get_filter_class(view)
|
||||
|
||||
if filter_class:
|
||||
return filter_class(request.GET, queryset=queryset)
|
||||
|
||||
return queryset
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Generic views that provide commmonly needed behaviour.
|
||||
Generic views that provide commonly needed behaviour.
|
||||
"""
|
||||
|
||||
from rest_framework import views, mixins
|
||||
|
@ -10,12 +10,12 @@ from django.views.generic.list import MultipleObjectMixin
|
|||
|
||||
### Base classes for the generic views ###
|
||||
|
||||
class BaseView(views.APIView):
|
||||
class GenericAPIView(views.APIView):
|
||||
"""
|
||||
Base class for all other generic views.
|
||||
"""
|
||||
serializer_class = None
|
||||
model_serializer_class = api_settings.MODEL_SERIALIZER
|
||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
|
@ -43,21 +43,31 @@ class BaseView(views.APIView):
|
|||
|
||||
return serializer_class
|
||||
|
||||
def get_serializer(self, data=None, files=None, instance=None):
|
||||
def get_serializer(self, instance=None, data=None, files=None):
|
||||
# TODO: add support for files
|
||||
# TODO: add support for seperate serializer/deserializer
|
||||
serializer_class = self.get_serializer_class()
|
||||
context = self.get_serializer_context()
|
||||
return serializer_class(data, instance=instance, context=context)
|
||||
return serializer_class(instance, data=data, context=context)
|
||||
|
||||
|
||||
class MultipleObjectBaseView(MultipleObjectMixin, BaseView):
|
||||
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
||||
"""
|
||||
Base class for generic views onto a queryset.
|
||||
"""
|
||||
|
||||
pagination_serializer_class = api_settings.PAGINATION_SERIALIZER
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
filter_backend = api_settings.FILTER_BACKEND
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not self.filter_backend:
|
||||
return queryset
|
||||
backend = self.filter_backend()
|
||||
return backend.filter_queryset(self.request, queryset, self)
|
||||
|
||||
def get_filtered_queryset(self):
|
||||
return self.filter_queryset(self.get_queryset())
|
||||
|
||||
def get_pagination_serializer_class(self):
|
||||
"""
|
||||
|
@ -75,7 +85,7 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView):
|
|||
return pagination_serializer_class(instance=page, context=context)
|
||||
|
||||
|
||||
class SingleObjectBaseView(SingleObjectMixin, BaseView):
|
||||
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
|
||||
"""
|
||||
Base class for generic views onto a model instance.
|
||||
"""
|
||||
|
@ -86,7 +96,7 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):
|
|||
"""
|
||||
Override default to add support for object-level permissions.
|
||||
"""
|
||||
obj = super(SingleObjectBaseView, self).get_object()
|
||||
obj = super(SingleObjectAPIView, self).get_object()
|
||||
if not self.has_permission(self.request, obj):
|
||||
self.permission_denied(self.request)
|
||||
return obj
|
||||
|
@ -95,8 +105,19 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):
|
|||
### Concrete view classes that provide method handlers ###
|
||||
### by composing the mixin classes with a base view. ###
|
||||
|
||||
|
||||
class CreateAPIView(mixins.CreateModelMixin,
|
||||
GenericAPIView):
|
||||
|
||||
"""
|
||||
Concrete view for creating a model instance.
|
||||
"""
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ListAPIView(mixins.ListModelMixin,
|
||||
MultipleObjectBaseView):
|
||||
MultipleObjectAPIView):
|
||||
"""
|
||||
Concrete view for listing a queryset.
|
||||
"""
|
||||
|
@ -104,9 +125,38 @@ class ListAPIView(mixins.ListModelMixin,
|
|||
return self.list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveAPIView(mixins.RetrieveModelMixin,
|
||||
SingleObjectAPIView):
|
||||
"""
|
||||
Concrete view for retrieving a model instance.
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DestroyAPIView(mixins.DestroyModelMixin,
|
||||
SingleObjectAPIView):
|
||||
|
||||
"""
|
||||
Concrete view for deleting a model instance.
|
||||
"""
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return self.destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UpdateAPIView(mixins.UpdateModelMixin,
|
||||
SingleObjectAPIView):
|
||||
|
||||
"""
|
||||
Concrete view for updating a model instance.
|
||||
"""
|
||||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ListCreateAPIView(mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
MultipleObjectBaseView):
|
||||
MultipleObjectAPIView):
|
||||
"""
|
||||
Concrete view for listing a queryset or creating a model instance.
|
||||
"""
|
||||
|
@ -117,18 +167,9 @@ class ListCreateAPIView(mixins.ListModelMixin,
|
|||
return self.create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveAPIView(mixins.RetrieveModelMixin,
|
||||
SingleObjectBaseView):
|
||||
"""
|
||||
Concrete view for retrieving a model instance.
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
SingleObjectBaseView):
|
||||
SingleObjectAPIView):
|
||||
"""
|
||||
Concrete view for retrieving or deleting a model instance.
|
||||
"""
|
||||
|
@ -142,7 +183,7 @@ class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
|
|||
class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
SingleObjectBaseView):
|
||||
SingleObjectAPIView):
|
||||
"""
|
||||
Concrete view for retrieving, updating or deleting a model instance.
|
||||
"""
|
||||
|
|
|
@ -3,9 +3,6 @@ Basic building blocks for generic class based views.
|
|||
|
||||
We don't bind behaviour to http method handlers yet,
|
||||
which allows mixin classes to be composed in interesting ways.
|
||||
|
||||
Eg. Use mixins to build a Resource class, and have a Router class
|
||||
perform the binding of http methods to actions for us.
|
||||
"""
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
|
@ -20,20 +17,24 @@ class CreateModelMixin(object):
|
|||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA)
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def pre_save(self, obj):
|
||||
pass
|
||||
|
||||
|
||||
class ListModelMixin(object):
|
||||
"""
|
||||
List a queryset.
|
||||
Should be mixed in with `MultipleObjectBaseView`.
|
||||
Should be mixed in with `MultipleObjectAPIView`.
|
||||
"""
|
||||
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
self.object_list = self.get_filtered_queryset()
|
||||
|
||||
# Default is to allow empty querysets. This can be altered by setting
|
||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||
|
@ -46,10 +47,11 @@ class ListModelMixin(object):
|
|||
# which may be `None` to disable pagination.
|
||||
page_size = self.get_paginate_by(self.object_list)
|
||||
if page_size:
|
||||
paginator, page, queryset, is_paginated = self.paginate_queryset(self.object_list, page_size)
|
||||
packed = self.paginate_queryset(self.object_list, page_size)
|
||||
paginator, page, queryset, is_paginated = packed
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
else:
|
||||
serializer = self.get_serializer(instance=self.object_list)
|
||||
serializer = self.get_serializer(self.object_list)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -61,7 +63,7 @@ class RetrieveModelMixin(object):
|
|||
"""
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
serializer = self.get_serializer(instance=self.object)
|
||||
serializer = self.get_serializer(self.object)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
@ -73,26 +75,25 @@ class UpdateModelMixin(object):
|
|||
def update(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
success_status = status.HTTP_200_OK
|
||||
except Http404:
|
||||
self.object = None
|
||||
success_status = status.HTTP_201_CREATED
|
||||
|
||||
serializer = self.get_serializer(data=request.DATA, instance=self.object)
|
||||
serializer = self.get_serializer(self.object, data=request.DATA)
|
||||
|
||||
if serializer.is_valid():
|
||||
if self.object is None:
|
||||
# If PUT occurs to a non existant object, we need to set any
|
||||
# attributes on the object that are implicit in the URL.
|
||||
self.update_urlconf_attributes(serializer.object)
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.data, status=success_status)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def update_urlconf_attributes(self, obj):
|
||||
def pre_save(self, obj):
|
||||
"""
|
||||
When update (re)creates an object, we need to set any attributes that
|
||||
are tied to the URLconf.
|
||||
Set any attributes on the object that are implicit in the request.
|
||||
"""
|
||||
# pk and/or slug attributes are implicit in the URL.
|
||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||
if pk:
|
||||
setattr(obj, 'pk', pk)
|
||||
|
|
|
@ -1,48 +1,38 @@
|
|||
from django.http import Http404
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
def negotiate(self, request, renderers, format=None, force=False):
|
||||
raise NotImplementedError('.negotiate() must be implemented')
|
||||
def select_parser(self, request, parsers):
|
||||
raise NotImplementedError('.select_parser() must be implemented')
|
||||
|
||||
def select_renderer(self, request, renderers, format_suffix=None):
|
||||
raise NotImplementedError('.select_renderer() must be implemented')
|
||||
|
||||
|
||||
class DefaultContentNegotiation(object):
|
||||
class DefaultContentNegotiation(BaseContentNegotiation):
|
||||
settings = api_settings
|
||||
|
||||
def select_parser(self, parsers, media_type):
|
||||
def select_parser(self, request, parsers):
|
||||
"""
|
||||
Given a list of parsers and a media type, return the appropriate
|
||||
parser to handle the incoming request.
|
||||
"""
|
||||
for parser in parsers:
|
||||
if media_type_matches(parser.media_type, media_type):
|
||||
if media_type_matches(parser.media_type, request.content_type):
|
||||
return parser
|
||||
return None
|
||||
|
||||
def negotiate(self, request, renderers, format=None, force=False):
|
||||
def select_renderer(self, request, renderers, format_suffix=None):
|
||||
"""
|
||||
Given a request and a list of renderers, return a two-tuple of:
|
||||
(renderer, media type).
|
||||
|
||||
If force is set, then suppress exceptions, and forcibly return a
|
||||
fallback renderer and media_type.
|
||||
"""
|
||||
try:
|
||||
return self.unforced_negotiate(request, renderers, format)
|
||||
except (exceptions.InvalidFormat, exceptions.NotAcceptable):
|
||||
if force:
|
||||
return (renderers[0], renderers[0].media_type)
|
||||
raise
|
||||
|
||||
def unforced_negotiate(self, request, renderers, format=None):
|
||||
"""
|
||||
As `.negotiate()`, but does not take the optional `force` agument,
|
||||
or suppress exceptions.
|
||||
"""
|
||||
# Allow URL style format override. eg. "?format=json
|
||||
format = format or request.GET.get(self.settings.URL_FORMAT_OVERRIDE)
|
||||
format_query_param = self.settings.URL_FORMAT_OVERRIDE
|
||||
format = format_suffix or request.GET.get(format_query_param)
|
||||
|
||||
if format:
|
||||
renderers = self.filter_renderers(renderers, format)
|
||||
|
@ -77,7 +67,7 @@ class DefaultContentNegotiation(object):
|
|||
renderers = [renderer for renderer in renderers
|
||||
if renderer.format == format]
|
||||
if not renderers:
|
||||
raise exceptions.InvalidFormat(format)
|
||||
raise Http404
|
||||
return renderers
|
||||
|
||||
def get_accept_list(self, request):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.templatetags.rest_framework import replace_query_param
|
||||
|
||||
# TODO: Support URLconf kwarg-style paging
|
||||
|
||||
|
@ -7,30 +8,30 @@ class NextPageField(serializers.Field):
|
|||
"""
|
||||
Field that returns a link to the next page in paginated results.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
if not value.has_next():
|
||||
return None
|
||||
page = value.next_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
if request:
|
||||
return request.build_absolute_uri(relative_url)
|
||||
return relative_url
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
|
||||
|
||||
class PreviousPageField(serializers.Field):
|
||||
"""
|
||||
Field that returns a link to the previous page in paginated results.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
if not value.has_previous():
|
||||
return None
|
||||
page = value.previous_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
if request:
|
||||
return request.build_absolute_uri('?page=%d' % page)
|
||||
return relative_url
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
|
||||
|
||||
class PaginationSerializerOptions(serializers.SerializerOptions):
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
"""
|
||||
Django supports parsing the content of an HTTP request, but only for form POST requests.
|
||||
That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well
|
||||
to general HTTP requests.
|
||||
Parsers are used to parse the content of incoming HTTP requests.
|
||||
|
||||
We need a method to be able to:
|
||||
|
||||
1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
|
||||
|
||||
2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
|
||||
and multipart/form-data. (eg also handle multipart/json)
|
||||
They give us a generic way of being able to handle various media types
|
||||
on the request, such as form content or json encoded data.
|
||||
"""
|
||||
|
||||
from django.http import QueryDict
|
||||
|
@ -21,7 +15,6 @@ from xml.etree import ElementTree as ET
|
|||
from xml.parsers.expat import ExpatError
|
||||
import datetime
|
||||
import decimal
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class DataAndFiles(object):
|
||||
|
@ -33,29 +26,18 @@ class DataAndFiles(object):
|
|||
class BaseParser(object):
|
||||
"""
|
||||
All parsers should extend `BaseParser`, specifying a `media_type`
|
||||
attribute, and overriding the `.parse_stream()` method.
|
||||
attribute, and overriding the `.parse()` method.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
|
||||
def parse(self, string_or_stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
The main entry point to parsers. This is a light wrapper around
|
||||
`parse_stream`, that instead handles both string and stream objects.
|
||||
"""
|
||||
if isinstance(string_or_stream, basestring):
|
||||
stream = BytesIO(string_or_stream)
|
||||
else:
|
||||
stream = string_or_stream
|
||||
return self.parse_stream(stream, parser_context)
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
"""
|
||||
Given a stream to read from, return the deserialized output.
|
||||
Should return parsed data, or a DataAndFiles object consisting of the
|
||||
Given a stream to read from, return the parsed representation.
|
||||
Should return parsed data, or a `DataAndFiles` object consisting of the
|
||||
parsed data and files.
|
||||
"""
|
||||
raise NotImplementedError(".parse_stream() must be overridden.")
|
||||
raise NotImplementedError(".parse() must be overridden.")
|
||||
|
||||
|
||||
class JSONParser(BaseParser):
|
||||
|
@ -65,7 +47,7 @@ class JSONParser(BaseParser):
|
|||
|
||||
media_type = 'application/json'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
|
@ -85,7 +67,7 @@ class YAMLParser(BaseParser):
|
|||
|
||||
media_type = 'application/yaml'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
|
@ -105,7 +87,7 @@ class FormParser(BaseParser):
|
|||
|
||||
media_type = 'application/x-www-form-urlencoded'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
|
@ -123,7 +105,7 @@ class MultiPartParser(BaseParser):
|
|||
|
||||
media_type = 'multipart/form-data'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a DataAndFiles object.
|
||||
|
||||
|
@ -131,8 +113,10 @@ class MultiPartParser(BaseParser):
|
|||
`.files` will be a `QueryDict` containing all the form files.
|
||||
"""
|
||||
parser_context = parser_context or {}
|
||||
meta = parser_context['meta']
|
||||
upload_handlers = parser_context['upload_handlers']
|
||||
request = parser_context['request']
|
||||
meta = request.META
|
||||
upload_handlers = request.upload_handlers
|
||||
|
||||
try:
|
||||
parser = DjangoMultiPartParser(meta, stream, upload_handlers)
|
||||
data, files = parser.parse()
|
||||
|
@ -148,7 +132,7 @@ class XMLParser(BaseParser):
|
|||
|
||||
media_type = 'application/xml'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
try:
|
||||
tree = ET.parse(stream)
|
||||
except (ExpatError, ETParseError, ValueError), exc:
|
||||
|
|
|
@ -18,6 +18,17 @@ class BasePermission(object):
|
|||
raise NotImplementedError(".has_permission() must be overridden.")
|
||||
|
||||
|
||||
class AllowAny(BasePermission):
|
||||
"""
|
||||
Allow any access.
|
||||
This isn't strictly required, since you could use an empty
|
||||
permission_classes list, but it's useful because it makes the intention
|
||||
more explicit.
|
||||
"""
|
||||
def has_permission(self, request, view, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
class IsAuthenticated(BasePermission):
|
||||
"""
|
||||
Allows access only to authenticated users.
|
||||
|
@ -85,7 +96,7 @@ class DjangoModelPermissions(BasePermission):
|
|||
"""
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': model_cls._meta.module_name
|
||||
'model_name': model_cls._meta.module_name
|
||||
}
|
||||
return [perm % kwargs for perm in self.perms_map[method]]
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
"""
|
||||
Renderers are used to serialize a View's output into specific media types.
|
||||
Renderers are used to serialize a response into specific media types.
|
||||
|
||||
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
|
||||
by serializing the output along with documentation regarding the View, output status and headers,
|
||||
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||
They give us a generic way of being able to handle various media types
|
||||
on the response, such as JSON encoded data or HTML output.
|
||||
|
||||
REST framework also provides an HTML renderer the renders the browseable API.
|
||||
"""
|
||||
import copy
|
||||
import string
|
||||
from django import forms
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.template import RequestContext, loader
|
||||
from django.template import RequestContext, loader, Template
|
||||
from django.utils import simplejson as json
|
||||
from rest_framework.compat import yaml
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
|
@ -23,8 +25,8 @@ from rest_framework import serializers, parsers
|
|||
|
||||
class BaseRenderer(object):
|
||||
"""
|
||||
All renderers must extend this class, set the :attr:`media_type` attribute,
|
||||
and override the :meth:`render` method.
|
||||
All renderers should extend this class, setting the `media_type`
|
||||
and `format` attributes, and override the `.render()` method.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
|
@ -98,7 +100,7 @@ class JSONPRenderer(JSONRenderer):
|
|||
callback = self.get_callback(renderer_context)
|
||||
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
||||
renderer_context)
|
||||
return "%s(%s);" % (callback, json)
|
||||
return u"%s(%s);" % (callback, json)
|
||||
|
||||
|
||||
class XMLRenderer(BaseRenderer):
|
||||
|
@ -137,18 +139,33 @@ class YAMLRenderer(BaseRenderer):
|
|||
return yaml.dump(data, stream=None, Dumper=self.encoder)
|
||||
|
||||
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
A Base class provided for convenience.
|
||||
An HTML renderer for use with templates.
|
||||
|
||||
Render the object simply by using the given template.
|
||||
To create a template renderer, subclass this class, and set
|
||||
the :attr:`media_type` and :attr:`template` attributes.
|
||||
The data supplied to the Response object should be a dictionary that will
|
||||
be used as context for the template.
|
||||
|
||||
The template name is determined by (in order of preference):
|
||||
|
||||
1. An explicit `.template_name` attribute set on the response.
|
||||
2. An explicit `.template_name` attribute set on this class.
|
||||
3. The return result of calling `view.get_template_names()`.
|
||||
|
||||
For example:
|
||||
data = {'users': User.objects.all()}
|
||||
return Response(data, template_name='users.html')
|
||||
|
||||
For pre-rendered HTML, see StaticHTMLRenderer.
|
||||
"""
|
||||
|
||||
media_type = 'text/html'
|
||||
format = 'html'
|
||||
template_name = None
|
||||
exception_template_names = [
|
||||
'%(status_code)s.html',
|
||||
'api_exception.html'
|
||||
]
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -165,15 +182,21 @@ class HTMLRenderer(BaseRenderer):
|
|||
request = renderer_context['request']
|
||||
response = renderer_context['response']
|
||||
|
||||
template_names = self.get_template_names(response, view)
|
||||
template = self.resolve_template(template_names)
|
||||
context = self.resolve_context(data, request)
|
||||
if response.exception:
|
||||
template = self.get_exception_template(response)
|
||||
else:
|
||||
template_names = self.get_template_names(response, view)
|
||||
template = self.resolve_template(template_names)
|
||||
|
||||
context = self.resolve_context(data, request, response)
|
||||
return template.render(context)
|
||||
|
||||
def resolve_template(self, template_names):
|
||||
return loader.select_template(template_names)
|
||||
|
||||
def resolve_context(self, data, request):
|
||||
def resolve_context(self, data, request, response):
|
||||
if response.exception:
|
||||
data['status_code'] = response.status_code
|
||||
return RequestContext(request, data)
|
||||
|
||||
def get_template_names(self, response, view):
|
||||
|
@ -185,6 +208,48 @@ class HTMLRenderer(BaseRenderer):
|
|||
return view.get_template_names()
|
||||
raise ConfigurationError('Returned a template response with no template_name')
|
||||
|
||||
def get_exception_template(self, response):
|
||||
template_names = [name % {'status_code': response.status_code}
|
||||
for name in self.exception_template_names]
|
||||
|
||||
try:
|
||||
# Try to find an appropriate error template
|
||||
return self.resolve_template(template_names)
|
||||
except:
|
||||
# Fall back to using eg '404 Not Found'
|
||||
return Template('%d %s' % (response.status_code,
|
||||
response.status_text.title()))
|
||||
|
||||
|
||||
# Note, subclass TemplateHTMLRenderer simply for the exception behavior
|
||||
class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||
"""
|
||||
An HTML renderer class that simply returns pre-rendered HTML.
|
||||
|
||||
The data supplied to the Response object should be a string representing
|
||||
the pre-rendered HTML content.
|
||||
|
||||
For example:
|
||||
data = '<html><body>example</body></html>'
|
||||
return Response(data)
|
||||
|
||||
For template rendered HTML, see TemplateHTMLRenderer.
|
||||
"""
|
||||
media_type = 'text/html'
|
||||
format = 'html'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
renderer_context = renderer_context or {}
|
||||
response = renderer_context['response']
|
||||
|
||||
if response and response.exception:
|
||||
request = renderer_context['request']
|
||||
template = self.get_exception_template(response)
|
||||
context = self.resolve_context(data, request, response)
|
||||
return template.render(context)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class BrowsableAPIRenderer(BaseRenderer):
|
||||
"""
|
||||
|
@ -222,11 +287,9 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
return content
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
def show_form_for_method(self, view, method, request, obj):
|
||||
"""
|
||||
Get a form, possibly bound to either the input or output data.
|
||||
In the absence on of the Resource having an associated form then
|
||||
provide a form that can be used to submit arbitrary content.
|
||||
Returns True if a form should be shown for this method.
|
||||
"""
|
||||
if not method in view.allowed_methods:
|
||||
return # Not a valid method
|
||||
|
@ -236,24 +299,13 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
request = clone_request(request, method)
|
||||
try:
|
||||
if not view.has_permission(request):
|
||||
if not view.has_permission(request, obj):
|
||||
return # Don't have permission
|
||||
except:
|
||||
return # Don't have permission and exception explicitly raise
|
||||
return True
|
||||
|
||||
if method == 'DELETE' or method == 'OPTIONS':
|
||||
return True # Don't actually need to return a form
|
||||
|
||||
if (not getattr(view, 'get_serializer', None) or
|
||||
not parsers.FormParser in getattr(view, 'parser_classes')):
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
return self.get_generic_content_form(media_types)
|
||||
|
||||
#####
|
||||
# TODO: This is a little bit of a hack. Actually we'd like to remove
|
||||
# this and just render serializer fields to html directly.
|
||||
|
||||
# We need to map our Fields to Django's Fields.
|
||||
def serializer_to_form_fields(self, serializer):
|
||||
field_mapping = {
|
||||
serializers.FloatField: forms.FloatField,
|
||||
serializers.IntegerField: forms.IntegerField,
|
||||
|
@ -261,34 +313,72 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
serializers.DateField: forms.DateField,
|
||||
serializers.EmailField: forms.EmailField,
|
||||
serializers.CharField: forms.CharField,
|
||||
serializers.ChoiceField: forms.ChoiceField,
|
||||
serializers.BooleanField: forms.BooleanField,
|
||||
serializers.PrimaryKeyRelatedField: forms.ModelChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField
|
||||
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
|
||||
serializers.SlugRelatedField: forms.ChoiceField,
|
||||
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
||||
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
||||
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
|
||||
}
|
||||
|
||||
# Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
||||
fields = {}
|
||||
obj, data = None, None
|
||||
if getattr(view, 'object', None):
|
||||
obj = view.object
|
||||
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
for k, v in serializer.get_fields(True).items():
|
||||
if getattr(v, 'readonly', True):
|
||||
if getattr(v, 'read_only', True):
|
||||
continue
|
||||
|
||||
kwargs = {}
|
||||
if getattr(v, 'queryset', None):
|
||||
kwargs['queryset'] = getattr(v, 'queryset', None)
|
||||
kwargs['required'] = v.required
|
||||
|
||||
#if getattr(v, 'queryset', None):
|
||||
# kwargs['queryset'] = v.queryset
|
||||
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
kwargs['choices'] = v.choices
|
||||
|
||||
if getattr(v, 'widget', None):
|
||||
widget = copy.deepcopy(v.widget)
|
||||
kwargs['widget'] = widget
|
||||
|
||||
if getattr(v, 'default', None) is not None:
|
||||
kwargs['initial'] = v.default
|
||||
|
||||
kwargs['label'] = k
|
||||
|
||||
try:
|
||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||
except KeyError:
|
||||
fields[k] = forms.CharField()
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
fields[k] = forms.ChoiceField(**kwargs)
|
||||
else:
|
||||
fields[k] = forms.CharField(**kwargs)
|
||||
return fields
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
"""
|
||||
Get a form, possibly bound to either the input or output data.
|
||||
In the absence on of the Resource having an associated form then
|
||||
provide a form that can be used to submit arbitrary content.
|
||||
"""
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
|
||||
if method == 'DELETE' or method == 'OPTIONS':
|
||||
return True # Don't actually need to return a form
|
||||
|
||||
if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes:
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
return self.get_generic_content_form(media_types)
|
||||
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
fields = self.serializer_to_form_fields(serializer)
|
||||
|
||||
# Creating an on the fly form see:
|
||||
# http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
||||
OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields)
|
||||
if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted
|
||||
data = serializer.data
|
||||
data = (obj is not None) and serializer.data or None
|
||||
form_instance = OnTheFlyForm(data)
|
||||
return form_instance
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@ def is_form_media_type(media_type):
|
|||
Return True if the media type is a valid form media type.
|
||||
"""
|
||||
base_media_type, params = parse_header(media_type)
|
||||
return base_media_type == 'application/x-www-form-urlencoded' or \
|
||||
base_media_type == 'multipart/form-data'
|
||||
return (base_media_type == 'application/x-www-form-urlencoded' or
|
||||
base_media_type == 'multipart/form-data')
|
||||
|
||||
|
||||
class Empty(object):
|
||||
|
@ -88,16 +88,11 @@ class Request(object):
|
|||
self._stream = Empty
|
||||
|
||||
if self.parser_context is None:
|
||||
self.parser_context = self._default_parser_context(request)
|
||||
self.parser_context = {}
|
||||
self.parser_context['request'] = self
|
||||
|
||||
def _default_negotiator(self):
|
||||
return api_settings.DEFAULT_CONTENT_NEGOTIATION()
|
||||
|
||||
def _default_parser_context(self, request):
|
||||
return {
|
||||
'upload_handlers': request.upload_handlers,
|
||||
'meta': request.META,
|
||||
}
|
||||
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
|
@ -265,15 +260,19 @@ class Request(object):
|
|||
|
||||
May raise an `UnsupportedMediaType`, or `ParseError` exception.
|
||||
"""
|
||||
if self.stream is None or self.content_type is None:
|
||||
stream = self.stream
|
||||
media_type = self.content_type
|
||||
|
||||
if stream is None or media_type is None:
|
||||
return (None, None)
|
||||
|
||||
parser = self.negotiator.select_parser(self.parsers, self.content_type)
|
||||
parser = self.negotiator.select_parser(self, self.parsers)
|
||||
|
||||
if not parser:
|
||||
raise exceptions.UnsupportedMediaType(self.content_type)
|
||||
raise exceptions.UnsupportedMediaType(media_type)
|
||||
|
||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||
|
||||
parsed = parser.parse(self.stream, self.parser_context)
|
||||
# Parser classes may return the raw data, or a
|
||||
# DataAndFiles object. Unpack the result as required.
|
||||
try:
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY #####
|
||||
|
||||
from functools import update_wrapper
|
||||
import inspect
|
||||
from django.utils.decorators import classonlymethod
|
||||
from rest_framework import views, generics
|
||||
|
||||
|
||||
def wrapped(source, dest):
|
||||
"""
|
||||
Copy public, non-method attributes from source to dest, and return dest.
|
||||
"""
|
||||
for attr in [attr for attr in dir(source)
|
||||
if not attr.startswith('_') and not inspect.ismethod(attr)]:
|
||||
setattr(dest, attr, getattr(source, attr))
|
||||
return dest
|
||||
|
||||
|
||||
##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY #####
|
||||
|
||||
class ResourceMixin(object):
|
||||
"""
|
||||
Clone Django's `View.as_view()` behaviour *except* using REST framework's
|
||||
'method -> action' binding for resources.
|
||||
"""
|
||||
|
||||
@classonlymethod
|
||||
def as_view(cls, actions, **initkwargs):
|
||||
"""
|
||||
Main entry point for a request-response process.
|
||||
"""
|
||||
# sanitize keyword arguments
|
||||
for key in initkwargs:
|
||||
if key in cls.http_method_names:
|
||||
raise TypeError("You tried to pass in the %s method name as a "
|
||||
"keyword argument to %s(). Don't do that."
|
||||
% (key, cls.__name__))
|
||||
if not hasattr(cls, key):
|
||||
raise TypeError("%s() received an invalid keyword %r" % (
|
||||
cls.__name__, key))
|
||||
|
||||
def view(request, *args, **kwargs):
|
||||
self = cls(**initkwargs)
|
||||
|
||||
# Bind methods to actions
|
||||
for method, action in actions.items():
|
||||
handler = getattr(self, action)
|
||||
setattr(self, method, handler)
|
||||
|
||||
# As you were, solider.
|
||||
if hasattr(self, 'get') and not hasattr(self, 'head'):
|
||||
self.head = self.get
|
||||
return self.dispatch(request, *args, **kwargs)
|
||||
|
||||
# take name and docstring from class
|
||||
update_wrapper(view, cls, updated=())
|
||||
|
||||
# and possible attributes set by decorators
|
||||
# like csrf_exempt from dispatch
|
||||
update_wrapper(view, cls.dispatch, assigned=())
|
||||
return view
|
||||
|
||||
|
||||
##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY #####
|
||||
|
||||
class Resource(ResourceMixin, views.APIView):
|
||||
pass
|
||||
|
||||
|
||||
##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY #####
|
||||
|
||||
class ModelResource(ResourceMixin, views.APIView):
|
||||
# TODO: Actually delegation won't work
|
||||
root_class = generics.ListCreateAPIView
|
||||
detail_class = generics.RetrieveUpdateDestroyAPIView
|
||||
|
||||
def root_view(self):
|
||||
return wrapped(self, self.root_class())
|
||||
|
||||
def detail_view(self):
|
||||
return wrapped(self, self.detail_class())
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return self.root_view().list(request, args, kwargs)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
return self.root_view().create(request, args, kwargs)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return self.detail_view().retrieve(request, args, kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
return self.detail_view().update(request, args, kwargs)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return self.detail_view().destroy(request, args, kwargs)
|
|
@ -9,7 +9,8 @@ class Response(SimpleTemplateResponse):
|
|||
"""
|
||||
|
||||
def __init__(self, data=None, status=200,
|
||||
template_name=None, headers=None):
|
||||
template_name=None, headers=None,
|
||||
exception=False):
|
||||
"""
|
||||
Alters the init arguments slightly.
|
||||
For example, drop 'template_name', and instead use 'data'.
|
||||
|
@ -21,6 +22,7 @@ class Response(SimpleTemplateResponse):
|
|||
self.data = data
|
||||
self.headers = headers and headers[:] or []
|
||||
self.template_name = template_name
|
||||
self.exception = exception
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
|
@ -45,3 +47,13 @@ class Response(SimpleTemplateResponse):
|
|||
# TODO: Deprecate and use a template tag instead
|
||||
# TODO: Status code text for RFC 6585 status codes
|
||||
return STATUS_CODE_TEXT.get(self.status_code, '')
|
||||
|
||||
def __getstate__(self):
|
||||
"""
|
||||
Remove attributes from the response that shouldn't be cached
|
||||
"""
|
||||
state = super(Response, self).__getstate__()
|
||||
for key in ('accepted_renderer', 'renderer_context', 'data'):
|
||||
if key in state:
|
||||
del state[key]
|
||||
return state
|
||||
|
|
|
@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse
|
|||
from django.utils.functional import lazy
|
||||
|
||||
|
||||
def reverse(viewname, *args, **kwargs):
|
||||
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||
"""
|
||||
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
|
||||
and returns a fully qualified URL, using the request to get the base URL.
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
url = django_reverse(viewname, *args, **kwargs)
|
||||
if format is not None:
|
||||
kwargs = kwargs or {}
|
||||
kwargs['format'] = format
|
||||
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
|
||||
if request:
|
||||
return request.build_absolute_uri(url)
|
||||
return url
|
||||
|
|
|
@ -32,10 +32,10 @@ def main():
|
|||
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
|
||||
DeprecationWarning
|
||||
)
|
||||
failures = TestRunner(['rest_framework'])
|
||||
failures = TestRunner(['tests'])
|
||||
else:
|
||||
test_runner = TestRunner()
|
||||
failures = test_runner.run_tests(['rest_framework'])
|
||||
failures = test_runner.run_tests(['tests'])
|
||||
cov.stop()
|
||||
|
||||
# Discover the list of all modules that we should test coverage for
|
||||
|
|
|
@ -32,7 +32,7 @@ def main():
|
|||
else:
|
||||
print usage()
|
||||
sys.exit(1)
|
||||
failures = test_runner.run_tests(['rest_framework' + test_case])
|
||||
failures = test_runner.run_tests(['tests' + test_case])
|
||||
|
||||
sys.exit(failures)
|
||||
|
||||
|
|
|
@ -21,6 +21,12 @@ DATABASES = {
|
|||
}
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
|
@ -91,6 +97,7 @@ INSTALLED_APPS = (
|
|||
# 'django.contrib.admindocs',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'rest_framework.tests'
|
||||
)
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
@ -100,13 +107,6 @@ import django
|
|||
if django.VERSION < (1, 3):
|
||||
INSTALLED_APPS += ('staticfiles',)
|
||||
|
||||
# OAuth support is optional, so we only test oauth if it's installed.
|
||||
try:
|
||||
import oauth_provider
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
INSTALLED_APPS += ('oauth_provider',)
|
||||
|
||||
# If we're running on the Jenkins server we want to archive the coverage reports as XML.
|
||||
import os
|
||||
|
|
|
@ -3,8 +3,18 @@ import datetime
|
|||
import types
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.forms import widgets
|
||||
from django.utils.datastructures import SortedDict
|
||||
from rest_framework.compat import get_concrete_model
|
||||
|
||||
# Note: We do the following so that users of the framework can use this style:
|
||||
#
|
||||
# example_field = serializers.CharField(...)
|
||||
#
|
||||
# This helps keep the seperation between model fields, form fields, and
|
||||
# serializer fields more explicit.
|
||||
|
||||
|
||||
from rest_framework.fields import *
|
||||
|
||||
|
||||
|
@ -22,10 +32,6 @@ class SortedDictWithMetadata(SortedDict, DictWithMetadata):
|
|||
pass
|
||||
|
||||
|
||||
class RecursionOccured(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
def _is_protected_type(obj):
|
||||
"""
|
||||
True if the object is a native datatype that does not need to
|
||||
|
@ -33,10 +39,10 @@ def _is_protected_type(obj):
|
|||
"""
|
||||
return isinstance(obj, (
|
||||
types.NoneType,
|
||||
int, long,
|
||||
datetime.datetime, datetime.date, datetime.time,
|
||||
float, Decimal,
|
||||
basestring)
|
||||
int, long,
|
||||
datetime.datetime, datetime.date, datetime.time,
|
||||
float, Decimal,
|
||||
basestring)
|
||||
)
|
||||
|
||||
|
||||
|
@ -73,7 +79,7 @@ class SerializerOptions(object):
|
|||
Meta class options for Serializer
|
||||
"""
|
||||
def __init__(self, meta):
|
||||
self.nested = getattr(meta, 'nested', False)
|
||||
self.depth = getattr(meta, 'depth', 0)
|
||||
self.fields = getattr(meta, 'fields', ())
|
||||
self.exclude = getattr(meta, 'exclude', ())
|
||||
|
||||
|
@ -85,14 +91,13 @@ class BaseSerializer(Field):
|
|||
_options_class = SerializerOptions
|
||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations.
|
||||
|
||||
def __init__(self, data=None, instance=None, context=None, **kwargs):
|
||||
def __init__(self, instance=None, data=None, context=None, **kwargs):
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
self.fields = copy.deepcopy(self.base_fields)
|
||||
self.opts = self._options_class(self.Meta)
|
||||
self.fields = copy.deepcopy(self.base_fields)
|
||||
self.parent = None
|
||||
self.root = None
|
||||
|
||||
self.stack = []
|
||||
self.context = context or {}
|
||||
|
||||
self.init_data = data
|
||||
|
@ -104,13 +109,13 @@ class BaseSerializer(Field):
|
|||
#####
|
||||
# Methods to determine which fields to use when (de)serializing objects.
|
||||
|
||||
def default_fields(self, serialize, obj=None, data=None, nested=False):
|
||||
def default_fields(self, nested=False):
|
||||
"""
|
||||
Return the complete set of default fields for the object, as a dict.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_fields(self, serialize, obj=None, data=None, nested=False):
|
||||
def get_fields(self, nested=False):
|
||||
"""
|
||||
Returns the complete set of fields for the object as a dict.
|
||||
|
||||
|
@ -123,10 +128,10 @@ class BaseSerializer(Field):
|
|||
for key, field in self.fields.items():
|
||||
ret[key] = field
|
||||
# Set up the field
|
||||
field.initialize(parent=self)
|
||||
field.initialize(parent=self, field_name=key)
|
||||
|
||||
# Add in the default fields
|
||||
fields = self.default_fields(serialize, obj, data, nested)
|
||||
fields = self.default_fields(nested)
|
||||
for key, val in fields.items():
|
||||
if key not in ret:
|
||||
ret[key] = val
|
||||
|
@ -148,17 +153,14 @@ class BaseSerializer(Field):
|
|||
#####
|
||||
# Field methods - used when the serializer class is itself used as a field.
|
||||
|
||||
def initialize(self, parent):
|
||||
def initialize(self, parent, field_name):
|
||||
"""
|
||||
Same behaviour as usual Field, except that we need to keep track
|
||||
of state so that we can deal with handling maximum depth and recursion.
|
||||
of state so that we can deal with handling maximum depth.
|
||||
"""
|
||||
super(BaseSerializer, self).initialize(parent)
|
||||
self.stack = parent.stack[:]
|
||||
if parent.opts.nested and not isinstance(parent.opts.nested, bool):
|
||||
self.opts.nested = parent.opts.nested - 1
|
||||
else:
|
||||
self.opts.nested = parent.opts.nested
|
||||
super(BaseSerializer, self).initialize(parent, field_name)
|
||||
if parent.opts.depth:
|
||||
self.opts.depth = parent.opts.depth - 1
|
||||
|
||||
#####
|
||||
# Methods to convert or revert from objects <--> primative representations.
|
||||
|
@ -174,21 +176,13 @@ class BaseSerializer(Field):
|
|||
Core of serialization.
|
||||
Convert an object into a dictionary of serialized field values.
|
||||
"""
|
||||
if obj in self.stack and not self.source == '*':
|
||||
raise RecursionOccured()
|
||||
self.stack.append(obj)
|
||||
|
||||
ret = self._dict_class()
|
||||
ret.fields = {}
|
||||
|
||||
fields = self.get_fields(serialize=True, obj=obj, nested=self.opts.nested)
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
for field_name, field in fields.items():
|
||||
key = self.get_field_key(field_name)
|
||||
try:
|
||||
value = field.field_to_native(obj, field_name)
|
||||
except RecursionOccured:
|
||||
field = self.get_fields(serialize=True, obj=obj, nested=False)[field_name]
|
||||
value = field.field_to_native(obj, field_name)
|
||||
value = field.field_to_native(obj, field_name)
|
||||
ret[key] = value
|
||||
ret.fields[key] = field
|
||||
return ret
|
||||
|
@ -198,7 +192,7 @@ class BaseSerializer(Field):
|
|||
Core of deserialization, together with `restore_object`.
|
||||
Converts a dictionary of data into a dictionary of deserialized fields.
|
||||
"""
|
||||
fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested)
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
reverted_data = {}
|
||||
for field_name, field in fields.items():
|
||||
try:
|
||||
|
@ -208,6 +202,35 @@ class BaseSerializer(Field):
|
|||
|
||||
return reverted_data
|
||||
|
||||
def perform_validation(self, attrs):
|
||||
"""
|
||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
||||
"""
|
||||
# TODO: refactor this so we're not determining the fields again
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
|
||||
for field_name, field in fields.items():
|
||||
try:
|
||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
||||
if validate_method:
|
||||
source = field.source or field_name
|
||||
attrs = validate_method(attrs, source)
|
||||
except ValidationError as err:
|
||||
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
||||
|
||||
try:
|
||||
attrs = self.validate(attrs)
|
||||
except ValidationError as err:
|
||||
self._errors['non_field_errors'] = err.messages
|
||||
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Stub method, to be overridden in Serializer subclasses
|
||||
"""
|
||||
return attrs
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
Deserialize a dictionary of attributes into an object instance.
|
||||
|
@ -223,11 +246,8 @@ class BaseSerializer(Field):
|
|||
"""
|
||||
Serialize objects -> primatives.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return dict([(key, self.to_native(val))
|
||||
for (key, val) in obj.items()])
|
||||
elif hasattr(obj, '__iter__'):
|
||||
return [self.to_native(item) for item in obj]
|
||||
if hasattr(obj, '__iter__'):
|
||||
return [self.convert_object(item) for item in obj]
|
||||
return self.convert_object(obj)
|
||||
|
||||
def from_native(self, data):
|
||||
|
@ -241,17 +261,31 @@ class BaseSerializer(Field):
|
|||
self._errors = {}
|
||||
if data is not None:
|
||||
attrs = self.restore_fields(data)
|
||||
attrs = self.perform_validation(attrs)
|
||||
else:
|
||||
self._errors['non_field_errors'] = 'No input provided'
|
||||
self._errors['non_field_errors'] = ['No input provided']
|
||||
|
||||
if not self._errors:
|
||||
return self.restore_object(attrs, instance=getattr(self, 'object', None))
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
"""
|
||||
Override default so that we can apply ModelSerializer as a nested
|
||||
field to relationships.
|
||||
"""
|
||||
obj = getattr(obj, self.source or field_name)
|
||||
|
||||
# If the object has an "all" method, assume it's a relationship
|
||||
if is_simple_callable(getattr(obj, 'all', None)):
|
||||
return [self.to_native(item) for item in obj.all()]
|
||||
|
||||
return self.to_native(obj)
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
"""
|
||||
Run deserialization and return error data,
|
||||
setting self.object if no errors occured.
|
||||
setting self.object if no errors occurred.
|
||||
"""
|
||||
if self._errors is None:
|
||||
obj = self.from_native(self.init_data)
|
||||
|
@ -295,17 +329,7 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
_options_class = ModelSerializerOptions
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
"""
|
||||
Override default so that we can apply ModelSerializer as a nested
|
||||
field to relationships.
|
||||
"""
|
||||
obj = getattr(obj, self.source or field_name)
|
||||
if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'):
|
||||
return [self.to_native(item) for item in obj.all()]
|
||||
return self.to_native(obj)
|
||||
|
||||
def default_fields(self, serialize, obj=None, data=None, nested=False):
|
||||
def default_fields(self, nested=False):
|
||||
"""
|
||||
Return all the fields that should be serialized for the model.
|
||||
"""
|
||||
|
@ -342,7 +366,7 @@ class ModelSerializer(Serializer):
|
|||
field = self.get_field(model_field)
|
||||
|
||||
if field:
|
||||
field.initialize(parent=self)
|
||||
field.initialize(parent=self, field_name=model_field.name)
|
||||
ret[model_field.name] = field
|
||||
|
||||
return ret
|
||||
|
@ -374,6 +398,25 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
Creates a default instance of a basic non-relational field.
|
||||
"""
|
||||
kwargs = {}
|
||||
|
||||
kwargs['blank'] = model_field.blank
|
||||
|
||||
if model_field.null:
|
||||
kwargs['required'] = False
|
||||
|
||||
if model_field.has_default():
|
||||
kwargs['required'] = False
|
||||
kwargs['default'] = model_field.get_default()
|
||||
|
||||
if model_field.__class__ == models.TextField:
|
||||
kwargs['widget'] = widgets.Textarea
|
||||
|
||||
# TODO: TypedChoiceField?
|
||||
if model_field.flatchoices: # This ModelField contains choices
|
||||
kwargs['choices'] = model_field.flatchoices
|
||||
return ChoiceField(**kwargs)
|
||||
|
||||
field_mapping = {
|
||||
models.FloatField: FloatField,
|
||||
models.IntegerField: IntegerField,
|
||||
|
@ -389,14 +432,9 @@ class ModelSerializer(Serializer):
|
|||
models.BooleanField: BooleanField,
|
||||
}
|
||||
try:
|
||||
ret = field_mapping[model_field.__class__]()
|
||||
return field_mapping[model_field.__class__](**kwargs)
|
||||
except KeyError:
|
||||
ret = ModelField(model_field=model_field)
|
||||
|
||||
if model_field.default:
|
||||
ret.required = False
|
||||
|
||||
return ret
|
||||
return ModelField(model_field=model_field, **kwargs)
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
|
@ -409,6 +447,13 @@ class ModelSerializer(Serializer):
|
|||
setattr(instance, key, val)
|
||||
return instance
|
||||
|
||||
# Reverse relations
|
||||
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
|
||||
field_name = obj.field.related_query_name()
|
||||
if field_name in attrs:
|
||||
self.m2m_data[field_name] = attrs.pop(field_name)
|
||||
|
||||
# Forward relations
|
||||
for field in self.opts.model._meta.many_to_many:
|
||||
if field.name in attrs:
|
||||
self.m2m_data[field.name] = attrs.pop(field.name)
|
||||
|
@ -420,7 +465,7 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
self.object.save()
|
||||
|
||||
if self.m2m_data and save_m2m:
|
||||
if getattr(self, 'm2m_data', None) and save_m2m:
|
||||
for accessor_name, object_list in self.m2m_data.items():
|
||||
setattr(self.object, accessor_name, object_list)
|
||||
self.m2m_data = {}
|
||||
|
|
|
@ -3,11 +3,11 @@ Settings for REST framework are all namespaced in the REST_FRAMEWORK setting.
|
|||
For example your project's `settings.py` file might look like this:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERERS': (
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.YAMLRenderer',
|
||||
)
|
||||
'DEFAULT_PARSERS': (
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.parsers.YAMLParser',
|
||||
)
|
||||
|
@ -24,31 +24,38 @@ from django.utils import importlib
|
|||
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
|
||||
|
||||
DEFAULTS = {
|
||||
'DEFAULT_RENDERERS': (
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
),
|
||||
'DEFAULT_PARSERS': (
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.parsers.FormParser',
|
||||
'rest_framework.parsers.MultiPartParser'
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION': (
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication'
|
||||
),
|
||||
'DEFAULT_PERMISSIONS': (),
|
||||
'DEFAULT_THROTTLES': (),
|
||||
'DEFAULT_CONTENT_NEGOTIATION':
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.AllowAny',
|
||||
),
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
),
|
||||
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS':
|
||||
'rest_framework.negotiation.DefaultContentNegotiation',
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS':
|
||||
'rest_framework.serializers.ModelSerializer',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
|
||||
'rest_framework.pagination.PaginationSerializer',
|
||||
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'user': None,
|
||||
'anon': None,
|
||||
},
|
||||
|
||||
'MODEL_SERIALIZER': 'rest_framework.serializers.ModelSerializer',
|
||||
'PAGINATION_SERIALIZER': 'rest_framework.pagination.PaginationSerializer',
|
||||
'PAGINATE_BY': None,
|
||||
'FILTER_BACKEND': None,
|
||||
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
@ -65,14 +72,15 @@ DEFAULTS = {
|
|||
|
||||
# List of settings that may be in string import notation.
|
||||
IMPORT_STRINGS = (
|
||||
'DEFAULT_RENDERERS',
|
||||
'DEFAULT_PARSERS',
|
||||
'DEFAULT_AUTHENTICATION',
|
||||
'DEFAULT_PERMISSIONS',
|
||||
'DEFAULT_THROTTLES',
|
||||
'DEFAULT_CONTENT_NEGOTIATION',
|
||||
'MODEL_SERIALIZER',
|
||||
'PAGINATION_SERIALIZER',
|
||||
'DEFAULT_RENDERER_CLASSES',
|
||||
'DEFAULT_PARSER_CLASSES',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES',
|
||||
'DEFAULT_PERMISSION_CLASSES',
|
||||
'DEFAULT_THROTTLE_CLASSES',
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'FILTER_BACKEND',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
)
|
||||
|
@ -111,7 +119,7 @@ class APISettings(object):
|
|||
For example:
|
||||
|
||||
from rest_framework.settings import api_settings
|
||||
print api_settings.DEFAULT_RENDERERS
|
||||
print api_settings.DEFAULT_RENDERER_CLASSES
|
||||
|
||||
Any setting with string import paths will be automatically resolved
|
||||
and return the class, rather than the string literal.
|
||||
|
@ -136,8 +144,15 @@ class APISettings(object):
|
|||
if val and attr in self.import_strings:
|
||||
val = perform_import(val, attr)
|
||||
|
||||
self.validate_setting(attr, val)
|
||||
|
||||
# Cache the result
|
||||
setattr(self, attr, val)
|
||||
return val
|
||||
|
||||
def validate_setting(self, attr, val):
|
||||
if attr == 'FILTER_BACKEND' and val is not None:
|
||||
# Make sure we can initilize the class
|
||||
val()
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||
|
|
|
@ -32,6 +32,17 @@ h2, h3 {
|
|||
margin-right: 1em;
|
||||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
margin: 58px 0 0 0;
|
||||
}
|
||||
|
||||
form select, form input, form textarea {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
form select[multiple] {
|
||||
height: 150px;
|
||||
}
|
||||
/* To allow tooltips to work on disabled elements */
|
||||
.disabled-tooltip-shield {
|
||||
position: absolute;
|
||||
|
@ -55,6 +66,7 @@ pre {
|
|||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,7 +77,7 @@ html{
|
|||
background: none;
|
||||
}
|
||||
|
||||
body, .navbar .navbar-inner .container-fluid{
|
||||
body, .navbar .navbar-inner .container-fluid {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -76,13 +88,14 @@ body{
|
|||
}
|
||||
|
||||
#content{
|
||||
margin: 40px 0 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
/* custom navigation styles */
|
||||
.wrapper .navbar{
|
||||
width:100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left:0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner{
|
||||
|
|
|
@ -49,4 +49,4 @@ HTTP_502_BAD_GATEWAY = 502
|
|||
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
||||
HTTP_511_NETWORD_AUTHENTICATION_REQUIRED = 511
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
|
||||
<div class="content-main">
|
||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||
<p class="resource-description">{{ description }}</p>
|
||||
{{ description }}
|
||||
|
||||
<div class="request-info">
|
||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||
|
@ -131,12 +131,12 @@
|
|||
{% csrf_token %}
|
||||
{{ post_form.non_field_errors }}
|
||||
{% for field in post_form %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
<div class="controls">
|
||||
{{ field|add_class:"input-xlarge" }}
|
||||
{{ field }}
|
||||
<span class="help-inline">{{ field.help_text }}</span>
|
||||
{{ field.errors|add_class:"help-block" }}
|
||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -156,12 +156,12 @@
|
|||
{% csrf_token %}
|
||||
{{ put_form.non_field_errors }}
|
||||
{% for field in put_form %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
<div class="controls">
|
||||
{{ field|add_class:"input-xlarge" }}
|
||||
{{ field }}
|
||||
<span class='help-inline'>{{ field.help_text }}</span>
|
||||
{{ field.errors|add_class:"help-block" }}
|
||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -3,42 +3,50 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/>
|
||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
|
||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
|
||||
</head>
|
||||
|
||||
<body class="login">
|
||||
<body class="container">
|
||||
|
||||
<div id="container">
|
||||
|
||||
<div id="header">
|
||||
<div id="branding">
|
||||
<h1 id="site-name">Django REST framework</h1>
|
||||
<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>
|
||||
<h3 style="margin: 0 0 20px;">Django REST framework</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /row fluid -->
|
||||
|
||||
<div id="content" class="colM">
|
||||
<div id="content-main">
|
||||
<form method="post" action="{% url 'rest_framework:login' %}" id="login-form">
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<label for="id_username">Username:</label> {{ form.username }}
|
||||
<div id="div_id_username" class="clearfix control-group">
|
||||
<div class="controls" style="height: 30px">
|
||||
<Label class="span4" style="margin-top: 3px">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 class="form-row">
|
||||
<label for="id_password">Password:</label> {{ form.password }}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<div id="div_id_password" class="clearfix control-group">
|
||||
<div class="controls" style="height: 30px">
|
||||
<Label class="span4" style="margin-top: 3px">Password:</label>
|
||||
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label> </label><input type="submit" value="Log in">
|
||||
<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>
|
||||
<script type="text/javascript">
|
||||
document.getElementById('id_username').focus()
|
||||
</script>
|
||||
</div>
|
||||
<br class="clear">
|
||||
</div>
|
||||
</div><!-- /row fluid -->
|
||||
</div><!--/span-->
|
||||
|
||||
<div id="footer"></div>
|
||||
</div><!-- /.row-fluid -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -11,6 +11,18 @@ import string
|
|||
register = template.Library()
|
||||
|
||||
|
||||
def replace_query_param(url, key, val):
|
||||
"""
|
||||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlsplit(url)
|
||||
query_dict = QueryDict(query).copy()
|
||||
query_dict[key] = val
|
||||
query = query_dict.urlencode()
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
# Regex for adding classes to html snippets
|
||||
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
||||
|
||||
|
@ -31,19 +43,6 @@ hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|
|
|||
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
||||
|
||||
|
||||
# Helper function for 'add_query_param'
|
||||
def replace_query_param(url, key, val):
|
||||
"""
|
||||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlsplit(url)
|
||||
query_dict = QueryDict(query).copy()
|
||||
query_dict[key] = val
|
||||
query = query_dict.urlencode()
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
# And the template tags themselves...
|
||||
|
||||
@register.simple_tag
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
"""
|
||||
Force import of all modules in this package in order to get the standard test
|
||||
runner to pick up the tests. Yowzers.
|
||||
"""
|
||||
import os
|
||||
|
||||
modules = [filename.rsplit('.', 1)[0]
|
||||
for filename in os.listdir(os.path.dirname(__file__))
|
||||
if filename.endswith('.py') and not filename.startswith('_')]
|
||||
__test__ = dict()
|
||||
|
||||
for module in modules:
|
||||
exec("from rest_framework.tests.%s import *" % module)
|
168
rest_framework/tests/filterset.py
Normal file
168
rest_framework/tests/filterset.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.tests.models import FilterableItem, BasicModel
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
||||
if django_filters:
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_fields = ['decimal', 'date']
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
# These class are used to test a filter class.
|
||||
class SeveralFieldsFilter(django_filters.FilterSet):
|
||||
text = django_filters.CharFilter(lookup_type='icontains')
|
||||
decimal = django_filters.NumberFilter(lookup_type='lt')
|
||||
date = django_filters.DateFilter(lookup_type='gt')
|
||||
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterClassRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_class = SeveralFieldsFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
# These classes are used to test a misconfigured filter class.
|
||||
class MisconfiguredFilter(django_filters.FilterSet):
|
||||
text = django_filters.CharFilter(lookup_type='icontains')
|
||||
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
fields = ['text']
|
||||
|
||||
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_class = MisconfiguredFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
|
||||
class IntegrationTestFiltering(TestCase):
|
||||
"""
|
||||
Integration tests for filtered list views.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 10 FilterableItem instances.
|
||||
"""
|
||||
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
|
||||
for i in range(10):
|
||||
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
|
||||
decimal = base_data[1] + i
|
||||
date = base_data[2] - datetime.timedelta(days=i * 2)
|
||||
FilterableItem(text=text, decimal=decimal, date=date).save()
|
||||
|
||||
self.objects = FilterableItem.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_filtered_fields_root_view(self):
|
||||
"""
|
||||
GET requests to paginated ListCreateAPIView should return paginated results.
|
||||
"""
|
||||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
# Basic test with no filter.
|
||||
request = factory.get('/')
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter works.
|
||||
search_date = datetime.date(2012, 9, 22)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] == search_date]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_filtered_class_root_view(self):
|
||||
"""
|
||||
GET requests to filtered ListCreateAPIView that have a filter_class set
|
||||
should return filtered results.
|
||||
"""
|
||||
view = FilterClassRootView.as_view()
|
||||
|
||||
# Basic test with no filter.
|
||||
request = factory.get('/')
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
# Tests that the decimal filter set with 'lt' in the filter class works.
|
||||
search_decimal = Decimal('4.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] < search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter set with 'gt' in the filter class works.
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the text filter set with 'icontains' in the filter class works.
|
||||
search_text = 'ff'
|
||||
request = factory.get('/?text=%s' % search_text)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if search_text in f['text'].lower()]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that multiple filters works.
|
||||
search_decimal = Decimal('5.25')
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date and
|
||||
f['decimal'] < search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_incorrectly_configured_filter(self):
|
||||
"""
|
||||
An error should be displayed when the filter class is misconfigured.
|
||||
"""
|
||||
view = IncorrectlyConfiguredRootView.as_view()
|
||||
|
||||
request = factory.get('/')
|
||||
self.assertRaises(AssertionError, view, request)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_unknown_filter(self):
|
||||
"""
|
||||
GET requests with filters that aren't configured should return 200.
|
||||
"""
|
||||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
search_integer = 10
|
||||
request = factory.get('/?integer=%s' % search_integer)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
|
@ -25,7 +25,7 @@ class TestGenericRelations(TestCase):
|
|||
model = Bookmark
|
||||
exclude = ('id',)
|
||||
|
||||
serializer = BookmarkSerializer(instance=self.bookmark)
|
||||
serializer = BookmarkSerializer(self.bookmark)
|
||||
expected = {
|
||||
'tags': [u'django', u'python'],
|
||||
'url': u'https://www.djangoproject.com/'
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
from django.utils import simplejson as json
|
||||
from rest_framework import generics, serializers, status
|
||||
from rest_framework.tests.models import BasicModel, Comment
|
||||
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
||||
|
||||
|
||||
factory = RequestFactory()
|
||||
|
@ -22,6 +22,22 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
|||
model = BasicModel
|
||||
|
||||
|
||||
class SlugSerializer(serializers.ModelSerializer):
|
||||
slug = serializers.Field() # read only
|
||||
|
||||
class Meta:
|
||||
model = SlugBasedModel
|
||||
exclude = ('id',)
|
||||
|
||||
|
||||
class SlugBasedInstanceView(InstanceView):
|
||||
"""
|
||||
A model with a slug-field.
|
||||
"""
|
||||
model = SlugBasedModel
|
||||
serializer_class = SlugSerializer
|
||||
|
||||
|
||||
class TestRootView(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -129,6 +145,7 @@ class TestInstanceView(TestCase):
|
|||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = InstanceView.as_view()
|
||||
self.slug_based_view = SlugBasedInstanceView.as_view()
|
||||
|
||||
def test_get_instance_view(self):
|
||||
"""
|
||||
|
@ -198,7 +215,7 @@ class TestInstanceView(TestCase):
|
|||
|
||||
def test_put_cannot_set_id(self):
|
||||
"""
|
||||
POST 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.
|
||||
"""
|
||||
content = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.put('/1', json.dumps(content),
|
||||
|
@ -219,11 +236,39 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEquals(updated.text, 'foobar')
|
||||
|
||||
def test_put_as_create_on_id_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if it doesn't exist.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
# pk fields can not be created on demand, only the database can set th pk for a new object
|
||||
request = factory.put('/5', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
new_obj = self.objects.get(pk=5)
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
||||
def test_put_as_create_on_slug_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.put('/test_slug', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
||||
|
||||
# Regression test for #285
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.template import TemplateDoesNotExist, Template
|
||||
import django.template.loader
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework.renderers import HTMLRenderer
|
||||
from rest_framework.renderers import TemplateHTMLRenderer
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
@api_view(('GET',))
|
||||
@renderer_classes((HTMLRenderer,))
|
||||
@renderer_classes((TemplateHTMLRenderer,))
|
||||
def example(request):
|
||||
"""
|
||||
A view that can returns an HTML representation.
|
||||
|
@ -17,12 +19,26 @@ def example(request):
|
|||
return Response(data, template_name='example.html')
|
||||
|
||||
|
||||
@api_view(('GET',))
|
||||
@renderer_classes((TemplateHTMLRenderer,))
|
||||
def permission_denied(request):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@api_view(('GET',))
|
||||
@renderer_classes((TemplateHTMLRenderer,))
|
||||
def not_found(request):
|
||||
raise Http404()
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', example),
|
||||
url(r'^permission_denied$', permission_denied),
|
||||
url(r'^not_found$', not_found),
|
||||
)
|
||||
|
||||
|
||||
class HTMLRendererTests(TestCase):
|
||||
class TemplateHTMLRendererTests(TestCase):
|
||||
urls = 'rest_framework.tests.htmlrenderer'
|
||||
|
||||
def setUp(self):
|
||||
|
@ -48,3 +64,52 @@ class HTMLRendererTests(TestCase):
|
|||
response = self.client.get('/')
|
||||
self.assertContains(response, "example: foobar")
|
||||
self.assertEquals(response['Content-Type'], 'text/html')
|
||||
|
||||
def test_not_found_html_view(self):
|
||||
response = self.client.get('/not_found')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
self.assertEquals(response.content, "404 Not Found")
|
||||
self.assertEquals(response['Content-Type'], 'text/html')
|
||||
|
||||
def test_permission_denied_html_view(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEquals(response.content, "403 Forbidden")
|
||||
self.assertEquals(response['Content-Type'], 'text/html')
|
||||
|
||||
|
||||
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||
urls = 'rest_framework.tests.htmlrenderer'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Monkeypatch get_template
|
||||
"""
|
||||
self.get_template = django.template.loader.get_template
|
||||
|
||||
def get_template(template_name):
|
||||
if template_name == '404.html':
|
||||
return Template("404: {{ detail }}")
|
||||
if template_name == '403.html':
|
||||
return Template("403: {{ detail }}")
|
||||
raise TemplateDoesNotExist(template_name)
|
||||
|
||||
django.template.loader.get_template = get_template
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Revert monkeypatching
|
||||
"""
|
||||
django.template.loader.get_template = self.get_template
|
||||
|
||||
def test_not_found_html_view_with_template(self):
|
||||
response = self.client.get('/not_found')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
self.assertEquals(response.content, "404: Not found")
|
||||
self.assertEquals(response['Content-Type'], 'text/html')
|
||||
|
||||
def test_permission_denied_html_view_with_template(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEquals(response.content, "403: Permission denied")
|
||||
self.assertEquals(response['Content-Type'], 'text/html')
|
||||
|
|
|
@ -2,11 +2,28 @@ from django.conf.urls.defaults import patterns, url
|
|||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status, serializers
|
||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel
|
||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
text = serializers.CharField()
|
||||
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
||||
|
||||
class Meta:
|
||||
model = BlogPostComment
|
||||
fields = ('text', 'blog_post_url')
|
||||
|
||||
|
||||
class PhotoSerializer(serializers.Serializer):
|
||||
description = serializers.CharField()
|
||||
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title')
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
return Photo(**attrs)
|
||||
|
||||
|
||||
class BasicList(generics.ListCreateAPIView):
|
||||
model = BasicModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
|
@ -32,12 +49,34 @@ class ManyToManyDetail(generics.RetrieveAPIView):
|
|||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
|
||||
|
||||
class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
||||
model = BlogPostComment
|
||||
serializer_class = BlogPostCommentSerializer
|
||||
|
||||
|
||||
class BlogPostDetail(generics.RetrieveAPIView):
|
||||
model = BlogPost
|
||||
|
||||
|
||||
class PhotoListCreate(generics.ListCreateAPIView):
|
||||
model = Photo
|
||||
model_serializer_class = PhotoSerializer
|
||||
|
||||
|
||||
class AlbumDetail(generics.RetrieveAPIView):
|
||||
model = Album
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
||||
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
||||
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
||||
url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
||||
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list')
|
||||
)
|
||||
|
||||
|
||||
|
@ -124,3 +163,51 @@ class TestManyToManyHyperlinkedView(TestCase):
|
|||
response = self.detail_view(request, pk=1).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, self.data[0])
|
||||
|
||||
|
||||
class TestCreateWithForeignKeys(TestCase):
|
||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a blog post
|
||||
"""
|
||||
self.post = BlogPost.objects.create(title="Test post")
|
||||
self.create_view = BlogPostCommentListCreate.as_view()
|
||||
|
||||
def test_create_comment(self):
|
||||
|
||||
data = {
|
||||
'text': 'A test comment',
|
||||
'blog_post_url': 'http://testserver/posts/1/'
|
||||
}
|
||||
|
||||
request = factory.post('/comments/', data=data)
|
||||
response = self.create_view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
||||
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
||||
|
||||
|
||||
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an Album
|
||||
"""
|
||||
self.post = Album.objects.create(title='test-album')
|
||||
self.list_create_view = PhotoListCreate.as_view()
|
||||
|
||||
def test_create_photo(self):
|
||||
|
||||
data = {
|
||||
'description': 'A test photo',
|
||||
'album_url': 'http://testserver/albums/test-album/'
|
||||
}
|
||||
|
||||
request = factory.post('/photos/', data=data)
|
||||
response = self.list_create_view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(self.post.photo_set.count(), 1)
|
||||
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
||||
|
|
|
@ -40,7 +40,7 @@ class RESTFrameworkModel(models.Model):
|
|||
Base for test models that sets app_label, so they play nicely.
|
||||
"""
|
||||
class Meta:
|
||||
app_label = 'rest_framework'
|
||||
app_label = 'tests'
|
||||
abstract = True
|
||||
|
||||
|
||||
|
@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel):
|
|||
text = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class SlugBasedModel(RESTFrameworkModel):
|
||||
text = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=32)
|
||||
|
||||
|
||||
class DefaultValueModel(RESTFrameworkModel):
|
||||
text = models.CharField(default='foobar', max_length=100)
|
||||
|
||||
|
@ -63,6 +68,11 @@ class CallableDefaultValueModel(RESTFrameworkModel):
|
|||
class ManyToManyModel(RESTFrameworkModel):
|
||||
rel = models.ManyToManyField(Anchor)
|
||||
|
||||
|
||||
class ReadOnlyManyToManyModel(RESTFrameworkModel):
|
||||
text = models.CharField(max_length=100, default='anchor')
|
||||
rel = models.ManyToManyField(Anchor)
|
||||
|
||||
# Models to test generic relations
|
||||
|
||||
|
||||
|
@ -85,9 +95,57 @@ class Bookmark(RESTFrameworkModel):
|
|||
tags = GenericRelation(TaggedItem)
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
class Comment(RESTFrameworkModel):
|
||||
email = models.EmailField()
|
||||
content = models.CharField(max_length=200)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class ActionItem(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=200)
|
||||
done = models.BooleanField(default=False)
|
||||
|
||||
|
||||
# Models for reverse relations
|
||||
class BlogPost(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class BlogPostComment(RESTFrameworkModel):
|
||||
text = models.TextField()
|
||||
blog_post = models.ForeignKey(BlogPost)
|
||||
|
||||
|
||||
class Album(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
|
||||
|
||||
class Photo(RESTFrameworkModel):
|
||||
description = models.TextField()
|
||||
album = models.ForeignKey(Album)
|
||||
|
||||
|
||||
class Person(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=10)
|
||||
age = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'age': self.age,
|
||||
}
|
||||
|
||||
|
||||
# Model for issue #324
|
||||
class BlankFieldModel(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100, blank=True)
|
||||
|
|
|
@ -18,20 +18,20 @@ class TestAcceptedMediaType(TestCase):
|
|||
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
|
||||
self.negotiator = DefaultContentNegotiation()
|
||||
|
||||
def negotiate(self, request):
|
||||
return self.negotiator.negotiate(request, self.renderers)
|
||||
def select_renderer(self, request):
|
||||
return self.negotiator.select_renderer(request, self.renderers)
|
||||
|
||||
def test_client_without_accept_use_renderer(self):
|
||||
request = factory.get('/')
|
||||
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||
accepted_renderer, accepted_media_type = self.select_renderer(request)
|
||||
self.assertEquals(accepted_media_type, 'application/json')
|
||||
|
||||
def test_client_underspecifies_accept_use_renderer(self):
|
||||
request = factory.get('/', HTTP_ACCEPT='*/*')
|
||||
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||
accepted_renderer, accepted_media_type = self.select_renderer(request)
|
||||
self.assertEquals(accepted_media_type, 'application/json')
|
||||
|
||||
def test_client_overspecifies_accept_use_client(self):
|
||||
request = factory.get('/', HTTP_ACCEPT='application/json; indent=8')
|
||||
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||
accepted_renderer, accepted_media_type = self.select_renderer(request)
|
||||
self.assertEquals(accepted_media_type, 'application/json; indent=8')
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.core.paginator import Paginator
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status, pagination
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, pagination, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.tests.models import BasicModel, FilterableItem
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
@ -15,6 +19,21 @@ class RootView(generics.ListCreateAPIView):
|
|||
paginate_by = 10
|
||||
|
||||
|
||||
if django_filters:
|
||||
class DecimalFilter(django_filters.FilterSet):
|
||||
decimal = django_filters.NumberFilter(lookup_type='lt')
|
||||
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
paginate_by = 10
|
||||
filter_class = DecimalFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
|
||||
class IntegrationTestPagination(TestCase):
|
||||
"""
|
||||
Integration tests for paginated list views.
|
||||
|
@ -22,7 +41,7 @@ class IntegrationTestPagination(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 26 BasicModel intances.
|
||||
Create 26 BasicModel instances.
|
||||
"""
|
||||
for char in 'abcdefghijklmnopqrstuvwxyz':
|
||||
BasicModel(text=char * 3).save()
|
||||
|
@ -62,6 +81,58 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEquals(response.data['previous'], None)
|
||||
|
||||
|
||||
class IntegrationTestPaginationAndFiltering(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 50 FilterableItem instances.
|
||||
"""
|
||||
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
|
||||
for i in range(26):
|
||||
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
|
||||
decimal = base_data[1] + i
|
||||
date = base_data[2] - datetime.timedelta(days=i * 2)
|
||||
FilterableItem(text=text, decimal=decimal, date=date).save()
|
||||
|
||||
self.objects = FilterableItem.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = FilterFieldsRootView.as_view()
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_paginated_filtered_root_view(self):
|
||||
"""
|
||||
GET requests to paginated filtered ListCreateAPIView should return
|
||||
paginated results. The next and previous links should preserve the
|
||||
filtered parameters.
|
||||
"""
|
||||
request = factory.get('/?decimal=15.20')
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[:10])
|
||||
self.assertNotEquals(response.data['next'], None)
|
||||
self.assertEquals(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[10:15])
|
||||
self.assertEquals(response.data['next'], None)
|
||||
self.assertNotEquals(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[:10])
|
||||
self.assertNotEquals(response.data['next'], None)
|
||||
self.assertEquals(response.data['previous'], None)
|
||||
|
||||
|
||||
class UnitTestPagination(TestCase):
|
||||
"""
|
||||
Unit tests for pagination of primative objects.
|
||||
|
@ -74,13 +145,13 @@ class UnitTestPagination(TestCase):
|
|||
self.last_page = paginator.page(3)
|
||||
|
||||
def test_native_pagination(self):
|
||||
serializer = pagination.PaginationSerializer(instance=self.first_page)
|
||||
serializer = pagination.PaginationSerializer(self.first_page)
|
||||
self.assertEquals(serializer.data['count'], 26)
|
||||
self.assertEquals(serializer.data['next'], '?page=2')
|
||||
self.assertEquals(serializer.data['previous'], None)
|
||||
self.assertEquals(serializer.data['results'], self.objects[:10])
|
||||
|
||||
serializer = pagination.PaginationSerializer(instance=self.last_page)
|
||||
serializer = pagination.PaginationSerializer(self.last_page)
|
||||
self.assertEquals(serializer.data['count'], 26)
|
||||
self.assertEquals(serializer.data['next'], None)
|
||||
self.assertEquals(serializer.data['previous'], '?page=2')
|
||||
|
|
205
rest_framework/tests/pk_relations.py
Normal file
205
rest_framework/tests/pk_relations.py
Normal file
|
@ -0,0 +1,205 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# ManyToMany
|
||||
|
||||
class ManyToManyTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManySource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
|
||||
|
||||
|
||||
class ManyToManyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = ManyToManyTarget
|
||||
|
||||
|
||||
class ManyToManySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
|
||||
|
||||
# ForeignKey
|
||||
|
||||
class ForeignKeyTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ForeignKeySource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
|
||||
|
||||
|
||||
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ForeignKeyTarget
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||
|
||||
class PrimaryKeyManyToManyTests(TestCase):
|
||||
def setUp(self):
|
||||
for idx in range(1, 4):
|
||||
target = ManyToManyTarget(name='target-%d' % idx)
|
||||
target.save()
|
||||
source = ManyToManySource(name='source-%d' % idx)
|
||||
source.save()
|
||||
for target in ManyToManyTarget.objects.all():
|
||||
source.targets.add(target)
|
||||
|
||||
def test_many_to_many_retrieve(self):
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'targets': [1]},
|
||||
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
|
||||
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_many_to_many_retrieve(self):
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
|
||||
{'id': 3, 'name': u'target-3', 'sources': [3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_many_to_many_update(self):
|
||||
data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}
|
||||
instance = ManyToManySource.objects.get(pk=1)
|
||||
serializer = ManyToManySourceSerializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure source 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
|
||||
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_many_to_many_update(self):
|
||||
data = {'id': 1, 'name': u'target-1', 'sources': [1]}
|
||||
instance = ManyToManyTarget.objects.get(pk=1)
|
||||
serializer = ManyToManyTargetSerializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure target 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
|
||||
{'id': 3, 'name': u'target-3', 'sources': [3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_many_to_many_create(self):
|
||||
data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
|
||||
serializer = ManyToManyTargetSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEquals(serializer.data, data)
|
||||
self.assertEqual(obj.name, u'target-4')
|
||||
|
||||
# Ensure target 4 is added, and everything else is as expected
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
|
||||
{'id': 3, 'name': u'target-3', 'sources': [3]},
|
||||
{'id': 4, 'name': u'target-4', 'sources': [1, 3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
class PrimaryKeyForeignKeyTests(TestCase):
|
||||
def setUp(self):
|
||||
target = ForeignKeyTarget(name='target-1')
|
||||
target.save()
|
||||
new_target = ForeignKeyTarget(name='target-2')
|
||||
new_target.save()
|
||||
for idx in range(1, 4):
|
||||
source = ForeignKeySource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer = ForeignKeySourceSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'target': 1},
|
||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeyTarget.objects.all()
|
||||
serializer = ForeignKeyTargetSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': []},
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_foreign_key_update(self):
|
||||
data = {'id': 1, 'name': u'source-1', 'target': 2}
|
||||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# # Ensure source 1 is updated, and everything else is as expected
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer = ForeignKeySourceSerializer(queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'target': 2},
|
||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
# reverse foreign keys MUST be read_only
|
||||
# In the general case they do not provide .remove() or .clear()
|
||||
# and cannot be arbitrarily set.
|
||||
|
||||
# def test_reverse_foreign_key_update(self):
|
||||
# data = {'id': 1, 'name': u'target-1', 'sources': [1]}
|
||||
# instance = ForeignKeyTarget.objects.get(pk=1)
|
||||
# serializer = ForeignKeyTargetSerializer(instance, data=data)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEquals(serializer.data, data)
|
||||
# serializer.save()
|
||||
|
||||
# # Ensure target 1 is updated, and everything else is as expected
|
||||
# queryset = ForeignKeyTarget.objects.all()
|
||||
# serializer = ForeignKeyTargetSerializer(queryset)
|
||||
# expected = [
|
||||
# {'id': 1, 'name': u'target-1', 'sources': [1]},
|
||||
# {'id': 2, 'name': u'target-2', 'sources': []},
|
||||
# ]
|
||||
# self.assertEquals(serializer.data, expected)
|
|
@ -1,6 +1,8 @@
|
|||
import pickle
|
||||
import re
|
||||
|
||||
from django.conf.urls.defaults import patterns, url, include
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
|
@ -83,6 +85,7 @@ class HTMLView1(APIView):
|
|||
urlpatterns = patterns('',
|
||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^cache$', MockGETView.as_view()),
|
||||
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
|
||||
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
|
||||
url(r'^html$', HTMLView.as_view()),
|
||||
|
@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase):
|
|||
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
|
||||
self.assertTrue(xml.endswith('</root>'))
|
||||
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
||||
|
||||
|
||||
# Tests for caching issue, #346
|
||||
class CacheRenderTest(TestCase):
|
||||
"""
|
||||
Tests specific to caching responses
|
||||
"""
|
||||
|
||||
urls = 'rest_framework.tests.renderers'
|
||||
|
||||
cache_key = 'just_a_cache_key'
|
||||
|
||||
@classmethod
|
||||
def _get_pickling_errors(cls, obj, seen=None):
|
||||
""" Return any errors that would be raised if `obj' is pickled
|
||||
Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
|
||||
"""
|
||||
if seen == None:
|
||||
seen = []
|
||||
try:
|
||||
state = obj.__getstate__()
|
||||
except AttributeError:
|
||||
return
|
||||
if state == None:
|
||||
return
|
||||
if isinstance(state,tuple):
|
||||
if not isinstance(state[0],dict):
|
||||
state=state[1]
|
||||
else:
|
||||
state=state[0].update(state[1])
|
||||
result = {}
|
||||
for i in state:
|
||||
try:
|
||||
pickle.dumps(state[i],protocol=2)
|
||||
except pickle.PicklingError:
|
||||
if not state[i] in seen:
|
||||
seen.append(state[i])
|
||||
result[i] = cls._get_pickling_errors(state[i],seen)
|
||||
return result
|
||||
|
||||
def http_resp(self, http_method, url):
|
||||
"""
|
||||
Simple wrapper for Client http requests
|
||||
Removes the `client' and `request' attributes from as they are
|
||||
added by django.test.client.Client and not part of caching
|
||||
responses outside of tests.
|
||||
"""
|
||||
method = getattr(self.client, http_method)
|
||||
resp = method(url)
|
||||
del resp.client, resp.request
|
||||
return resp
|
||||
|
||||
def test_obj_pickling(self):
|
||||
"""
|
||||
Test that responses are properly pickled
|
||||
"""
|
||||
resp = self.http_resp('get', '/cache')
|
||||
|
||||
# Make sure that no pickling errors occurred
|
||||
self.assertEqual(self._get_pickling_errors(resp), {})
|
||||
|
||||
# Unfortunately LocMem backend doesn't raise PickleErrors but returns
|
||||
# None instead.
|
||||
cache.set(self.cache_key, resp)
|
||||
self.assertTrue(cache.get(self.cache_key) is not None)
|
||||
|
||||
def test_head_caching(self):
|
||||
"""
|
||||
Test caching of HEAD requests
|
||||
"""
|
||||
resp = self.http_resp('head', '/cache')
|
||||
cache.set(self.cache_key, resp)
|
||||
|
||||
cached_resp = cache.get(self.cache_key)
|
||||
self.assertIsInstance(cached_resp, Response)
|
||||
|
||||
def test_get_caching(self):
|
||||
"""
|
||||
Test caching of GET requests
|
||||
"""
|
||||
resp = self.http_resp('get', '/cache')
|
||||
cache.set(self.cache_key, resp)
|
||||
|
||||
cached_resp = cache.get(self.cache_key)
|
||||
self.assertIsInstance(cached_resp, Response)
|
||||
self.assertEqual(cached_resp.content, resp.content)
|
||||
|
|
|
@ -27,7 +27,7 @@ factory = RequestFactory()
|
|||
class PlainTextParser(BaseParser):
|
||||
media_type = 'text/plain'
|
||||
|
||||
def parse_stream(self, stream, parser_context=None):
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
|
|
|
@ -131,12 +131,6 @@ class RendererIntegrationTests(TestCase):
|
|||
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
@unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse')
|
||||
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
||||
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
||||
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||
self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE)
|
||||
|
||||
def test_specified_renderer_serializes_content_on_format_query(self):
|
||||
"""If a 'format' query is specified, the renderer with the matching
|
||||
format attribute should serialize the response."""
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import datetime
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import *
|
||||
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel)
|
||||
|
||||
|
||||
class SubComment(object):
|
||||
def __init__(self, sub_comment):
|
||||
self.sub_comment = sub_comment
|
||||
|
||||
|
||||
class Comment(object):
|
||||
|
@ -14,11 +21,16 @@ class Comment(object):
|
|||
return all([getattr(self, attr) == getattr(other, attr)
|
||||
for attr in ('email', 'content', 'created')])
|
||||
|
||||
def get_sub_comment(self):
|
||||
sub_comment = SubComment('And Merry Christmas!')
|
||||
return sub_comment
|
||||
|
||||
|
||||
class CommentSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
content = serializers.CharField(max_length=1000)
|
||||
created = serializers.DateTimeField()
|
||||
sub_comment = serializers.Field(source='get_sub_comment.sub_comment')
|
||||
|
||||
def restore_object(self, data, instance=None):
|
||||
if instance is None:
|
||||
|
@ -28,6 +40,19 @@ class CommentSerializer(serializers.Serializer):
|
|||
return instance
|
||||
|
||||
|
||||
class ActionItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ActionItem
|
||||
|
||||
|
||||
class PersonSerializer(serializers.ModelSerializer):
|
||||
info = serializers.Field(source='info')
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ('name', 'age', 'info')
|
||||
|
||||
|
||||
class BasicTests(TestCase):
|
||||
def setUp(self):
|
||||
self.comment = Comment(
|
||||
|
@ -38,36 +63,63 @@ class BasicTests(TestCase):
|
|||
self.data = {
|
||||
'email': 'tom@example.com',
|
||||
'content': 'Happy new year!',
|
||||
'created': datetime.datetime(2012, 1, 1)
|
||||
'created': datetime.datetime(2012, 1, 1),
|
||||
'sub_comment': 'This wont change'
|
||||
}
|
||||
self.expected = {
|
||||
'email': 'tom@example.com',
|
||||
'content': 'Happy new year!',
|
||||
'created': datetime.datetime(2012, 1, 1),
|
||||
'sub_comment': 'And Merry Christmas!'
|
||||
}
|
||||
self.person_data = {'name': 'dwight', 'age': 35}
|
||||
self.person = Person(**self.person_data)
|
||||
self.person.save()
|
||||
|
||||
def test_empty(self):
|
||||
serializer = CommentSerializer()
|
||||
expected = {
|
||||
'email': '',
|
||||
'content': '',
|
||||
'created': None
|
||||
'created': None,
|
||||
'sub_comment': ''
|
||||
}
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_retrieve(self):
|
||||
serializer = CommentSerializer(instance=self.comment)
|
||||
expected = self.data
|
||||
self.assertEquals(serializer.data, expected)
|
||||
serializer = CommentSerializer(self.comment)
|
||||
self.assertEquals(serializer.data, self.expected)
|
||||
|
||||
def test_create(self):
|
||||
serializer = CommentSerializer(self.data)
|
||||
serializer = CommentSerializer(data=self.data)
|
||||
expected = self.comment
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.object, expected)
|
||||
self.assertFalse(serializer.object is expected)
|
||||
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||
|
||||
def test_update(self):
|
||||
serializer = CommentSerializer(self.data, instance=self.comment)
|
||||
serializer = CommentSerializer(self.comment, data=self.data)
|
||||
expected = self.comment
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.object, expected)
|
||||
self.assertTrue(serializer.object is expected)
|
||||
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||
|
||||
def test_model_fields_as_expected(self):
|
||||
""" Make sure that the fields returned are the same as defined
|
||||
in the Meta data
|
||||
"""
|
||||
serializer = PersonSerializer(self.person)
|
||||
self.assertEquals(set(serializer.data.keys()),
|
||||
set(['name', 'age', 'info']))
|
||||
|
||||
def test_field_with_dictionary(self):
|
||||
""" Make sure that dictionaries from fields are left intact
|
||||
"""
|
||||
serializer = PersonSerializer(self.person)
|
||||
expected = self.person_data
|
||||
self.assertEquals(serializer.data['info'], expected)
|
||||
|
||||
|
||||
class ValidationTests(TestCase):
|
||||
|
@ -82,14 +134,16 @@ class ValidationTests(TestCase):
|
|||
'content': 'x' * 1001,
|
||||
'created': datetime.datetime(2012, 1, 1)
|
||||
}
|
||||
self.actionitem = ActionItem('Some to do item',
|
||||
)
|
||||
|
||||
def test_create(self):
|
||||
serializer = CommentSerializer(self.data)
|
||||
serializer = CommentSerializer(data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
|
||||
|
||||
def test_update(self):
|
||||
serializer = CommentSerializer(self.data, instance=self.comment)
|
||||
serializer = CommentSerializer(self.comment, data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
|
||||
|
||||
|
@ -98,10 +152,78 @@ class ValidationTests(TestCase):
|
|||
'content': 'xxx',
|
||||
'created': datetime.datetime(2012, 1, 1)
|
||||
}
|
||||
serializer = CommentSerializer(data, instance=self.comment)
|
||||
serializer = CommentSerializer(self.comment, data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'email': [u'This field is required.']})
|
||||
|
||||
def test_missing_bool_with_default(self):
|
||||
"""Make sure that a boolean value with a 'False' value is not
|
||||
mistaken for not having a default."""
|
||||
data = {
|
||||
'title': 'Some action item',
|
||||
#No 'done' value.
|
||||
}
|
||||
serializer = ActionItemSerializer(self.actionitem, data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.errors, {})
|
||||
|
||||
def test_field_validation(self):
|
||||
|
||||
class CommentSerializerWithFieldValidator(CommentSerializer):
|
||||
|
||||
def validate_content(self, attrs, source):
|
||||
value = attrs[source]
|
||||
if "test" not in value:
|
||||
raise serializers.ValidationError("Test not in value")
|
||||
return attrs
|
||||
|
||||
data = {
|
||||
'email': 'tom@example.com',
|
||||
'content': 'A test comment',
|
||||
'created': datetime.datetime(2012, 1, 1)
|
||||
}
|
||||
|
||||
serializer = CommentSerializerWithFieldValidator(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
data['content'] = 'This should not validate'
|
||||
|
||||
serializer = CommentSerializerWithFieldValidator(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'content': [u'Test not in value']})
|
||||
|
||||
def test_cross_field_validation(self):
|
||||
|
||||
class CommentSerializerWithCrossFieldValidator(CommentSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs["email"] not in attrs["content"]:
|
||||
raise serializers.ValidationError("Email address not in content")
|
||||
return attrs
|
||||
|
||||
data = {
|
||||
'email': 'tom@example.com',
|
||||
'content': 'A comment from tom@example.com',
|
||||
'created': datetime.datetime(2012, 1, 1)
|
||||
}
|
||||
|
||||
serializer = CommentSerializerWithCrossFieldValidator(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
data['content'] = 'A comment from foo@bar.com'
|
||||
|
||||
serializer = CommentSerializerWithCrossFieldValidator(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']})
|
||||
|
||||
def test_null_is_true_fields(self):
|
||||
"""
|
||||
Omitting a value for null-field should validate.
|
||||
"""
|
||||
serializer = PersonSerializer(data={'name': 'marko'})
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.errors, {})
|
||||
|
||||
|
||||
class MetadataTests(TestCase):
|
||||
def test_empty(self):
|
||||
|
@ -148,7 +270,7 @@ class ManyToManyTests(TestCase):
|
|||
Create an instance of a model with a ManyToMany relationship.
|
||||
"""
|
||||
data = {'rel': [self.anchor.id]}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ManyToManyModel.objects.all()), 2)
|
||||
|
@ -162,7 +284,7 @@ class ManyToManyTests(TestCase):
|
|||
new_anchor = Anchor()
|
||||
new_anchor.save()
|
||||
data = {'rel': [self.anchor.id, new_anchor.id]}
|
||||
serializer = self.serializer_class(data, instance=self.instance)
|
||||
serializer = self.serializer_class(self.instance, data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ManyToManyModel.objects.all()), 1)
|
||||
|
@ -175,7 +297,7 @@ class ManyToManyTests(TestCase):
|
|||
containing no items.
|
||||
"""
|
||||
data = {'rel': []}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ManyToManyModel.objects.all()), 2)
|
||||
|
@ -190,7 +312,7 @@ class ManyToManyTests(TestCase):
|
|||
new_anchor = Anchor()
|
||||
new_anchor.save()
|
||||
data = {'rel': []}
|
||||
serializer = self.serializer_class(data, instance=self.instance)
|
||||
serializer = self.serializer_class(self.instance, data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ManyToManyModel.objects.all()), 1)
|
||||
|
@ -204,7 +326,7 @@ class ManyToManyTests(TestCase):
|
|||
lists (eg form data).
|
||||
"""
|
||||
data = {'rel': ''}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ManyToManyModel.objects.all()), 2)
|
||||
|
@ -212,6 +334,61 @@ class ManyToManyTests(TestCase):
|
|||
self.assertEquals(list(instance.rel.all()), [])
|
||||
|
||||
|
||||
class ReadOnlyManyToManyTests(TestCase):
|
||||
def setUp(self):
|
||||
class ReadOnlyManyToManySerializer(serializers.ModelSerializer):
|
||||
rel = serializers.ManyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ReadOnlyManyToManyModel
|
||||
|
||||
self.serializer_class = ReadOnlyManyToManySerializer
|
||||
|
||||
# An anchor instance to use for the relationship
|
||||
self.anchor = Anchor()
|
||||
self.anchor.save()
|
||||
|
||||
# A model instance with a many to many relationship to the anchor
|
||||
self.instance = ReadOnlyManyToManyModel()
|
||||
self.instance.save()
|
||||
self.instance.rel.add(self.anchor)
|
||||
|
||||
# A serialized representation of the model instance
|
||||
self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'}
|
||||
|
||||
def test_update(self):
|
||||
"""
|
||||
Attempt to update an instance of a model with a ManyToMany
|
||||
relationship. Not updated due to read_only=True
|
||||
"""
|
||||
new_anchor = Anchor()
|
||||
new_anchor.save()
|
||||
data = {'rel': [self.anchor.id, new_anchor.id]}
|
||||
serializer = self.serializer_class(self.instance, data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1)
|
||||
self.assertEquals(instance.pk, 1)
|
||||
# rel is still as original (1 entry)
|
||||
self.assertEquals(list(instance.rel.all()), [self.anchor])
|
||||
|
||||
def test_update_without_relationship(self):
|
||||
"""
|
||||
Attempt to update an instance of a model where many to ManyToMany
|
||||
relationship is not supplied. Not updated due to read_only=True
|
||||
"""
|
||||
new_anchor = Anchor()
|
||||
new_anchor.save()
|
||||
data = {}
|
||||
serializer = self.serializer_class(self.instance, data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1)
|
||||
self.assertEquals(instance.pk, 1)
|
||||
# rel is still as original (1 entry)
|
||||
self.assertEquals(list(instance.rel.all()), [self.anchor])
|
||||
|
||||
|
||||
class DefaultValueTests(TestCase):
|
||||
def setUp(self):
|
||||
class DefaultValueSerializer(serializers.ModelSerializer):
|
||||
|
@ -223,7 +400,7 @@ class DefaultValueTests(TestCase):
|
|||
|
||||
def test_create_using_default(self):
|
||||
data = {}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(self.objects.all()), 1)
|
||||
|
@ -232,7 +409,7 @@ class DefaultValueTests(TestCase):
|
|||
|
||||
def test_create_overriding_default(self):
|
||||
data = {'text': 'overridden'}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(self.objects.all()), 1)
|
||||
|
@ -251,7 +428,7 @@ class CallableDefaultValueTests(TestCase):
|
|||
|
||||
def test_create_using_default(self):
|
||||
data = {}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(self.objects.all()), 1)
|
||||
|
@ -260,9 +437,87 @@ class CallableDefaultValueTests(TestCase):
|
|||
|
||||
def test_create_overriding_default(self):
|
||||
data = {'text': 'overridden'}
|
||||
serializer = self.serializer_class(data)
|
||||
serializer = self.serializer_class(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(len(self.objects.all()), 1)
|
||||
self.assertEquals(instance.pk, 1)
|
||||
self.assertEquals(instance.text, 'overridden')
|
||||
|
||||
|
||||
class ManyRelatedTests(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
class BlogPostCommentSerializer(serializers.Serializer):
|
||||
text = serializers.CharField()
|
||||
|
||||
class BlogPostSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
||||
|
||||
self.serializer_class = BlogPostSerializer
|
||||
|
||||
def test_reverse_relations(self):
|
||||
post = BlogPost.objects.create(title="Test blog post")
|
||||
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
serializer = self.serializer_class(instance=post)
|
||||
expected = {
|
||||
'title': 'Test blog post',
|
||||
'comments': [
|
||||
{'text': 'I hate this blog post'},
|
||||
{'text': 'I love this blog post'}
|
||||
]
|
||||
}
|
||||
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
# Test for issue #324
|
||||
class BlankFieldTests(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
class BlankFieldModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BlankFieldModel
|
||||
|
||||
class BlankFieldSerializer(serializers.Serializer):
|
||||
title = serializers.CharField(blank=True)
|
||||
|
||||
class NotBlankFieldModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
class NotBlankFieldSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
|
||||
self.model_serializer_class = BlankFieldModelSerializer
|
||||
self.serializer_class = BlankFieldSerializer
|
||||
self.not_blank_model_serializer_class = NotBlankFieldModelSerializer
|
||||
self.not_blank_serializer_class = NotBlankFieldSerializer
|
||||
self.data = {'title': ''}
|
||||
|
||||
def test_create_blank_field(self):
|
||||
serializer = self.serializer_class(data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
|
||||
def test_create_model_blank_field(self):
|
||||
serializer = self.model_serializer_class(data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
|
||||
def test_create_not_blank_field(self):
|
||||
"""
|
||||
Test to ensure blank data in a field not marked as blank=True
|
||||
is considered invalid in a non-model serializer
|
||||
"""
|
||||
serializer = self.not_blank_serializer_class(data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
|
||||
def test_create_model_not_blank_field(self):
|
||||
"""
|
||||
Test to ensure blank data in a field not marked as blank=True
|
||||
is considered invalid in a model serializer
|
||||
"""
|
||||
serializer = self.not_blank_model_serializer_class(data=self.data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
|
|
13
rest_framework/tests/tests.py
Normal file
13
rest_framework/tests/tests.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""
|
||||
Force import of all modules in this package in order to get the standard test
|
||||
runner to pick up the tests. Yowzers.
|
||||
"""
|
||||
import os
|
||||
|
||||
modules = [filename.rsplit('.', 1)[0]
|
||||
for filename in os.listdir(os.path.dirname(__file__))
|
||||
if filename.endswith('.py') and not filename.startswith('_')]
|
||||
__test__ = dict()
|
||||
|
||||
for module in modules:
|
||||
exec("from rest_framework.tests.%s import *" % module)
|
|
@ -285,7 +285,7 @@
|
|||
# uiop = models.CharField(max_length=256, blank=True)
|
||||
|
||||
# @property
|
||||
# def readonly(self):
|
||||
# def read_only(self):
|
||||
# return 'read only'
|
||||
|
||||
# class MockResource(ModelResource):
|
||||
|
@ -298,7 +298,7 @@
|
|||
|
||||
# def test_property_fields_are_allowed_on_model_forms(self):
|
||||
# """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||
# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||
# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only'}
|
||||
# self.assertEqual(self.validator.validate_request(content, None), content)
|
||||
|
||||
# def test_property_fields_are_not_required_on_model_forms(self):
|
||||
|
@ -310,19 +310,19 @@
|
|||
# """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
# It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
# broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||
# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only', 'extra': 'extra'}
|
||||
# self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||
|
||||
# def test_validate_requires_fields_on_model_forms(self):
|
||||
# """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
# It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
# broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
# content = {'readonly': 'read only'}
|
||||
# content = {'read_only': 'read only'}
|
||||
# self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||
|
||||
# def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||
# """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||
# content = {'qwerty': 'example', 'readonly': 'read only'}
|
||||
# content = {'qwerty': 'example', 'read_only': 'read only'}
|
||||
# self.validator.validate_request(content, None)
|
||||
|
||||
# def test_model_form_validator_uses_model_forms(self):
|
||||
|
|
|
@ -16,7 +16,7 @@ class BaseThrottle(object):
|
|||
|
||||
def wait(self):
|
||||
"""
|
||||
Optionally, return a recommeded number of seconds to wait before
|
||||
Optionally, return a recommended number of seconds to wait before
|
||||
the next request.
|
||||
"""
|
||||
return None
|
||||
|
@ -60,7 +60,7 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
Determine the string representation of the allowed request rate.
|
||||
"""
|
||||
if not getattr(self, 'scope', None):
|
||||
msg = ("You must set either `.scope` or `.rate` for '%s' thottle" %
|
||||
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
|
||||
self.__class__.__name__)
|
||||
raise exceptions.ConfigurationError(msg)
|
||||
|
||||
|
@ -137,7 +137,7 @@ class AnonRateThrottle(SimpleRateThrottle):
|
|||
"""
|
||||
Limits the rate of API calls that may be made by a anonymous users.
|
||||
|
||||
The IP address of the request will be used as the unqiue cache key.
|
||||
The IP address of the request will be used as the unique cache key.
|
||||
"""
|
||||
scope = 'anon'
|
||||
|
||||
|
|
|
@ -2,26 +2,23 @@ from django.conf.urls.defaults import url
|
|||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
def format_suffix_patterns(urlpatterns, suffix_required=False,
|
||||
suffix_kwarg=None, allowed=None):
|
||||
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||
"""
|
||||
Supplement existing urlpatterns with corrosponding patterns that also
|
||||
include a '.format' suffix. Retains urlpattern ordering.
|
||||
|
||||
urlpatterns:
|
||||
A list of URL patterns.
|
||||
|
||||
suffix_required:
|
||||
If `True`, only suffixed URLs will be generated, and non-suffixed
|
||||
URLs will not be used. Defaults to `False`.
|
||||
|
||||
suffix_kwarg:
|
||||
The name of the kwarg that will be passed to the view.
|
||||
Defaults to 'format'.
|
||||
|
||||
allowed:
|
||||
An optional tuple/list of allowed suffixes. eg ['json', 'api']
|
||||
Defaults to `None`, which allows any suffix.
|
||||
|
||||
"""
|
||||
suffix_kwarg = suffix_kwarg or api_settings.FORMAT_SUFFIX_KWARG
|
||||
suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG
|
||||
if allowed:
|
||||
if len(allowed) == 1:
|
||||
allowed_pattern = allowed[0]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from rest_framework.compat import StringIO
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ def get_breadcrumbs(url):
|
|||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
def breadcrumbs_recursive(url, breadcrumbs_list, prefix):
|
||||
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
|
||||
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
|
||||
|
||||
try:
|
||||
|
@ -16,7 +16,11 @@ def get_breadcrumbs(url):
|
|||
else:
|
||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||
if isinstance(getattr(view, 'cls_instance', None), APIView):
|
||||
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
|
||||
# Don't list the same view twice in a row.
|
||||
# Probably an optional trailing slash.
|
||||
if not seen or seen[-1] != view:
|
||||
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
|
||||
seen.append(view)
|
||||
|
||||
if url == '':
|
||||
# All done
|
||||
|
@ -24,11 +28,11 @@ def get_breadcrumbs(url):
|
|||
|
||||
elif url.endswith('/'):
|
||||
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
|
||||
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list, prefix)
|
||||
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list, prefix, seen)
|
||||
|
||||
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
|
||||
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list, prefix)
|
||||
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list, prefix, seen)
|
||||
|
||||
prefix = get_script_prefix().rstrip('/')
|
||||
url = url[len(prefix):]
|
||||
return breadcrumbs_recursive(url, [], prefix)
|
||||
return breadcrumbs_recursive(url, [], prefix, [])
|
||||
|
|
|
@ -54,12 +54,12 @@ def _camelcase_to_spaces(content):
|
|||
class APIView(View):
|
||||
settings = api_settings
|
||||
|
||||
renderer_classes = api_settings.DEFAULT_RENDERERS
|
||||
parser_classes = api_settings.DEFAULT_PARSERS
|
||||
authentication_classes = api_settings.DEFAULT_AUTHENTICATION
|
||||
throttle_classes = api_settings.DEFAULT_THROTTLES
|
||||
permission_classes = api_settings.DEFAULT_PERMISSIONS
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
|
||||
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
|
||||
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
|
@ -158,12 +158,15 @@ class APIView(View):
|
|||
|
||||
def get_parser_context(self, http_request):
|
||||
"""
|
||||
Returns a dict that is passed through to Parser.parse_stream(),
|
||||
Returns a dict that is passed through to Parser.parse(),
|
||||
as the `parser_context` keyword argument.
|
||||
"""
|
||||
# Note: Additionally `request` will also be added to the context
|
||||
# by the Request object.
|
||||
return {
|
||||
'upload_handlers': http_request.upload_handlers,
|
||||
'meta': http_request.META,
|
||||
'view': self,
|
||||
'args': getattr(self, 'args', ()),
|
||||
'kwargs': getattr(self, 'kwargs', {})
|
||||
}
|
||||
|
||||
def get_renderer_context(self):
|
||||
|
@ -171,13 +174,13 @@ class APIView(View):
|
|||
Returns a dict that is passed through to Renderer.render(),
|
||||
as the `renderer_context` keyword argument.
|
||||
"""
|
||||
# Note: Additionally 'response' will also be set on the context,
|
||||
# Note: Additionally 'response' will also be added to the context,
|
||||
# by the Response object.
|
||||
return {
|
||||
'view': self,
|
||||
'request': self.request,
|
||||
'args': self.args,
|
||||
'kwargs': self.kwargs
|
||||
'args': getattr(self, 'args', ()),
|
||||
'kwargs': getattr(self, 'kwargs', {}),
|
||||
'request': getattr(self, 'request', None)
|
||||
}
|
||||
|
||||
# API policy instantiation methods
|
||||
|
@ -215,7 +218,7 @@ class APIView(View):
|
|||
|
||||
def get_throttles(self):
|
||||
"""
|
||||
Instantiates and returns the list of thottles that this view uses.
|
||||
Instantiates and returns the list of throttles that this view uses.
|
||||
"""
|
||||
return [throttle() for throttle in self.throttle_classes]
|
||||
|
||||
|
@ -235,7 +238,13 @@ class APIView(View):
|
|||
"""
|
||||
renderers = self.get_renderers()
|
||||
conneg = self.get_content_negotiator()
|
||||
return conneg.negotiate(request, renderers, self.format_kwarg, force)
|
||||
|
||||
try:
|
||||
return conneg.select_renderer(request, renderers, self.format_kwarg)
|
||||
except:
|
||||
if force:
|
||||
return (renderers[0], renderers[0].media_type)
|
||||
raise
|
||||
|
||||
def has_permission(self, request, obj=None):
|
||||
"""
|
||||
|
@ -311,13 +320,17 @@ class APIView(View):
|
|||
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
return Response({'detail': exc.detail}, status=exc.status_code)
|
||||
return Response({'detail': exc.detail},
|
||||
status=exc.status_code,
|
||||
exception=True)
|
||||
elif isinstance(exc, Http404):
|
||||
return Response({'detail': 'Not found'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
exception=True)
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
return Response({'detail': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
exception=True)
|
||||
raise
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
|
|
2
setup.py
2
setup.py
|
@ -52,7 +52,7 @@ if sys.argv[-1] == 'publish':
|
|||
|
||||
|
||||
setup(
|
||||
name='rest_framework',
|
||||
name='djangorestframework',
|
||||
version=version,
|
||||
url='http://django-rest-framework.org',
|
||||
download_url='http://pypi.python.org/pypi/rest_framework/',
|
||||
|
|
6
tox.ini
6
tox.ini
|
@ -8,23 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
|||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = https://github.com/django/django/zipball/master
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django==1.4.1
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
||||
[testenv:py2.7-django1.3]
|
||||
basepython = python2.7
|
||||
deps = django==1.3.3
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = https://github.com/django/django/zipball/master
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django==1.4.1
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
||||
[testenv:py2.6-django1.3]
|
||||
basepython = python2.6
|
||||
deps = django==1.3.3
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
|
Loading…
Reference in New Issue
Block a user