mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 00:04:16 +03:00
Merge branch 'master' into many-fields
Conflicts: rest_framework/relations.py
This commit is contained in:
commit
be6df3ae3c
|
@ -177,7 +177,7 @@ If successfully authenticated, `SessionAuthentication` provides the following cr
|
||||||
|
|
||||||
Unauthenticated responses that are denied permission will result in an `HTTP 403 Forbidden` response.
|
Unauthenticated responses that are denied permission will result in an `HTTP 403 Forbidden` response.
|
||||||
|
|
||||||
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
|
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
|
||||||
|
|
||||||
# Custom authentication
|
# Custom authentication
|
||||||
|
|
||||||
|
@ -190,9 +190,27 @@ Typically the approach you should take is:
|
||||||
* If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked.
|
* If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked.
|
||||||
* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, without checking any other authentication schemes.
|
* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, without checking any other authentication schemes.
|
||||||
|
|
||||||
You *may* also override the `.authentication_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response.
|
You *may* also override the `.authenticate_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response.
|
||||||
|
|
||||||
|
If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
|
||||||
|
|
||||||
|
class ExampleAuthentication(authentication.BaseAuthentication):
|
||||||
|
def has_permission(self, request, view, obj=None):
|
||||||
|
username = request.META.get('X_USERNAME')
|
||||||
|
if not username:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise authenticate.AuthenticationFailed('No such user')
|
||||||
|
|
||||||
|
return (user, None)
|
||||||
|
|
||||||
If the `.authentication_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
|
|
||||||
|
|
||||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||||
|
|
|
@ -193,6 +193,16 @@ A date and time representation.
|
||||||
|
|
||||||
Corresponds to `django.db.models.fields.DateTimeField`
|
Corresponds to `django.db.models.fields.DateTimeField`
|
||||||
|
|
||||||
|
When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default.
|
||||||
|
|
||||||
|
If you want to override this behavior, you'll need to declare the `DateTimeField` explicitly on the serializer. For example:
|
||||||
|
|
||||||
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
created = serializers.DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
|
||||||
## IntegerField
|
## IntegerField
|
||||||
|
|
||||||
An integer representation.
|
An integer representation.
|
||||||
|
@ -232,5 +242,7 @@ Signature and validation is the same as with `FileField`.
|
||||||
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
|
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
|
||||||
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||||
|
|
|
@ -110,6 +110,15 @@ To implement a custom permission, override `BasePermission` and implement the `.
|
||||||
|
|
||||||
The method should return `True` if the request should be granted access, and `False` otherwise.
|
The method should return `True` if the request should be granted access, and `False` otherwise.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
|
||||||
|
|
||||||
|
class BlacklistPermission(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view, obj=None):
|
||||||
|
ip_addr = request.META['REMOTE_ADDR']
|
||||||
|
blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
|
||||||
|
return not blacklisted
|
||||||
|
|
||||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
|
|
|
@ -67,7 +67,7 @@ For example, given the following models:
|
||||||
And a model serializer defined like this:
|
And a model serializer defined like this:
|
||||||
|
|
||||||
class BookmarkSerializer(serializers.ModelSerializer):
|
class BookmarkSerializer(serializers.ModelSerializer):
|
||||||
tags = serializers.ManyRelatedField(source='tags')
|
tags = serializers.ManyRelatedField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
|
|
|
@ -80,7 +80,7 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan
|
||||||
|
|
||||||
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
|
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
|
||||||
|
|
||||||
**Note**: If you require cross-domain AJAX requests, you may also want to consider using [CORS] as an alternative to `JSONP`.
|
**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
|
||||||
|
|
||||||
**.media_type**: `application/javascript`
|
**.media_type**: `application/javascript`
|
||||||
|
|
||||||
|
@ -288,7 +288,8 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||||
[conneg]: content-negotiation.md
|
[conneg]: content-negotiation.md
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
[CORS]: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
|
[cors]: http://www.w3.org/TR/cors/
|
||||||
|
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[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.github+json]: http://developer.github.com/v3/media/
|
||||||
|
|
|
@ -83,13 +83,13 @@ You won't typically need to access this property.
|
||||||
|
|
||||||
# Browser enhancements
|
# Browser enhancements
|
||||||
|
|
||||||
REST framework supports a few browser enhancements such as browser-based `PUT` and `DELETE` forms.
|
REST framework supports a few browser enhancements such as browser-based `PUT`, `PATCH` and `DELETE` forms.
|
||||||
|
|
||||||
## .method
|
## .method
|
||||||
|
|
||||||
`request.method` returns the **uppercased** string representation of the request's HTTP method.
|
`request.method` returns the **uppercased** string representation of the request's HTTP method.
|
||||||
|
|
||||||
Browser-based `PUT` and `DELETE` forms are transparently supported.
|
Browser-based `PUT`, `PATCH` and `DELETE` forms are transparently supported.
|
||||||
|
|
||||||
For more information see the [browser enhancements documentation].
|
For more information see the [browser enhancements documentation].
|
||||||
|
|
||||||
|
|
|
@ -190,18 +190,12 @@ By default field values are treated as mapping to an attribute on the object. I
|
||||||
|
|
||||||
As an example, let's create a field that can be used represent the class name of the object being serialized:
|
As an example, let's create a field that can be used represent the class name of the object being serialized:
|
||||||
|
|
||||||
class ClassNameField(serializers.WritableField):
|
class ClassNameField(serializers.Field):
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
"""
|
"""
|
||||||
Serialize the object's class name, not an attribute of the object.
|
Serialize the object's class name.
|
||||||
"""
|
"""
|
||||||
return obj.__class__.__name__
|
return obj.__class__
|
||||||
|
|
||||||
def field_from_native(self, data, field_name, into):
|
|
||||||
"""
|
|
||||||
We don't want to set anything when we revert this field.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -150,8 +150,16 @@ User requests to either `ContactListView` or `ContactDetailView` would be restri
|
||||||
|
|
||||||
# Custom throttles
|
# 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.
|
To create a custom throttle, override `BaseThrottle` and implement `.allow_request(self, 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 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`.
|
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`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
|
||||||
|
|
||||||
|
class RandomRateThrottle(throttles.BaseThrottle):
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
return random.randint(1, 10) == 1
|
||||||
|
|
||||||
[permissions]: permissions.md
|
[permissions]: permissions.md
|
||||||
|
|
|
@ -85,7 +85,7 @@ The following methods are called before dispatching to the handler method.
|
||||||
## Dispatch methods
|
## Dispatch methods
|
||||||
|
|
||||||
The following methods are called directly by the view's `.dispatch()` method.
|
The following methods are called directly by the view's `.dispatch()` method.
|
||||||
These perform any actions that need to occur before or after calling the handler methods such as `.get()`, `.post()`, `put()` and `.delete()`.
|
These perform any actions that need to occur before or after calling the handler methods such as `.get()`, `.post()`, `put()`, `patch()` and `.delete()`.
|
||||||
|
|
||||||
### .initial(self, request, \*args, **kwargs)
|
### .initial(self, request, \*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,7 @@ The API guide is your complete reference manual to all the functionality provide
|
||||||
|
|
||||||
General guides to using REST framework.
|
General guides to using REST framework.
|
||||||
|
|
||||||
|
* [AJAX, CSRF & CORS][ajax-csrf-cors]
|
||||||
* [Browser enhancements][browser-enhancements]
|
* [Browser enhancements][browser-enhancements]
|
||||||
* [The Browsable API][browsableapi]
|
* [The Browsable API][browsableapi]
|
||||||
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
||||||
|
@ -210,7 +211,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[status]: api-guide/status-codes.md
|
[status]: api-guide/status-codes.md
|
||||||
[settings]: api-guide/settings.md
|
[settings]: api-guide/settings.md
|
||||||
|
|
||||||
[csrf]: topics/csrf.md
|
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
||||||
[browser-enhancements]: topics/browser-enhancements.md
|
[browser-enhancements]: topics/browser-enhancements.md
|
||||||
[browsableapi]: topics/browsable-api.md
|
[browsableapi]: topics/browsable-api.md
|
||||||
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
|
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{{ base_url }}/topics/ajax-csrf-cors{{ suffix }}">AJAX, CSRF & CORS</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</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/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/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
|
||||||
|
|
41
docs/topics/ajax-csrf-cors.md
Normal file
41
docs/topics/ajax-csrf-cors.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Working with AJAX, CSRF & CORS
|
||||||
|
|
||||||
|
> "Take a close look at possible CSRF / XSRF vulnerabilities on your own websites. They're the worst kind of vulnerability — very easy to exploit by attackers, yet not so intuitively easy to understand for software developers, at least until you've been bitten by one."
|
||||||
|
>
|
||||||
|
> — [Jeff Atwood][cite]
|
||||||
|
|
||||||
|
## Javascript clients
|
||||||
|
|
||||||
|
If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
|
||||||
|
|
||||||
|
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
|
||||||
|
|
||||||
|
AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`.
|
||||||
|
|
||||||
|
## CSRF protection
|
||||||
|
|
||||||
|
[Cross Site Request Forgery][csrf] protection is a mechanism of guarding against a particular type of attack, which can occur when a user has not logged out of a web site, and continues to have a valid session. In this circumstance a malicious site may be able to perform actions against the target site, within the context of the logged-in session.
|
||||||
|
|
||||||
|
To guard against these type of attacks, you need to do two things:
|
||||||
|
|
||||||
|
1. Ensure that the 'safe' HTTP operations, such as `GET`, `HEAD` and `OPTIONS` cannot be used to alter any server-side state.
|
||||||
|
2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token.
|
||||||
|
|
||||||
|
If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
|
||||||
|
|
||||||
|
The Django documentation describes how to [include CSRF tokens in AJAX requests][csrf-ajax].
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
|
||||||
|
[Cross-Origin Resource Sharing][cors] is a mechanism for allowing clients to interact with APIs that are hosted on a different domain. CORS works by requiring the server to include a specific set of headers that allow a browser to determine if and when cross-domain requests should be allowed.
|
||||||
|
|
||||||
|
The best way to deal with CORS in REST framework is to add the required response headers in middleware. This ensures that CORS is supported transparently, without having to change any behavior in your views.
|
||||||
|
|
||||||
|
[Otto Yiu][ottoyiu] maintains the [django-cors-headers] package, which is known to work correctly with REST framework APIs.
|
||||||
|
|
||||||
|
[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
|
||||||
|
[csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
|
||||||
|
[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
|
||||||
|
[cors]: http://www.w3.org/TR/cors/
|
||||||
|
[ottoyiu]: https://github.com/ottoyiu/
|
||||||
|
[django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/
|
|
@ -96,6 +96,11 @@ The following people have helped make REST framework great.
|
||||||
* Bruno Renié - [brutasse]
|
* Bruno Renié - [brutasse]
|
||||||
* Kevin Stone - [kevinastone]
|
* Kevin Stone - [kevinastone]
|
||||||
* Guglielmo Celata - [guglielmo]
|
* Guglielmo Celata - [guglielmo]
|
||||||
|
* Mike Tums - [mktums]
|
||||||
|
* Michael Elovskikh - [wronglink]
|
||||||
|
* Michał Jaworski - [swistakm]
|
||||||
|
* Andrea de Marco - [z4r]
|
||||||
|
* Fernando Rocha - [fernandogrd]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -227,3 +232,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[brutasse]: https://github.com/brutasse
|
[brutasse]: https://github.com/brutasse
|
||||||
[kevinastone]: https://github.com/kevinastone
|
[kevinastone]: https://github.com/kevinastone
|
||||||
[guglielmo]: https://github.com/guglielmo
|
[guglielmo]: https://github.com/guglielmo
|
||||||
|
[mktums]: https://github.com/mktums
|
||||||
|
[wronglink]: https://github.com/wronglink
|
||||||
|
[swistakm]: https://github.com/swistakm
|
||||||
|
[z4r]: https://github.com/z4r
|
||||||
|
[fernandogrd]: https://github.com/fernandogrd
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Working with AJAX and CSRF
|
|
||||||
|
|
||||||
> "Take a close look at possible CSRF / XSRF vulnerabilities on your own websites. They're the worst kind of vulnerability -- very easy to exploit by attackers, yet not so intuitively easy to understand for software developers, at least until you've been bitten by one."
|
|
||||||
>
|
|
||||||
> — [Jeff Atwood][cite]
|
|
||||||
|
|
||||||
* Explain need to add CSRF token to AJAX requests.
|
|
||||||
* 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
|
|
|
@ -12,10 +12,26 @@ Medium version numbers (0.x.0) may include minor API changes. You should read t
|
||||||
|
|
||||||
Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned.
|
Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
To upgrade Django REST framework to the latest version, use pip:
|
||||||
|
|
||||||
|
pip install -U djangorestframework
|
||||||
|
|
||||||
|
You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
|
pip freeze | grep djangorestframework
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.1.x series
|
## 2.1.x series
|
||||||
|
|
||||||
|
### Master
|
||||||
|
|
||||||
|
* Bugfix: Fix styling on browsable API login.
|
||||||
|
* Bugfix: Fix issue with deserializing empty to-many relations.
|
||||||
|
* Bugfix: Ensure model field validation is still applied for ModelSerializer subclasses with an custom `.restore_object()` method.
|
||||||
|
|
||||||
### 2.1.17
|
### 2.1.17
|
||||||
|
|
||||||
**Date**: 26th Jan 2013
|
**Date**: 26th Jan 2013
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together.
|
This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together.
|
||||||
|
|
||||||
The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started.<!-- If you just want a quick overview, you should head over to the [quickstart] documentation instead. -->
|
The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -130,11 +130,11 @@ The first thing we need to get started on our Web API is provide a way of serial
|
||||||
"""
|
"""
|
||||||
if instance:
|
if instance:
|
||||||
# Update existing instance
|
# Update existing instance
|
||||||
instance.title = attrs['title']
|
instance.title = attrs.get('title', instance.title)
|
||||||
instance.code = attrs['code']
|
instance.code = attrs.get('code', instance.code)
|
||||||
instance.linenos = attrs['linenos']
|
instance.linenos = attrs.get('linenos', instance.linenos)
|
||||||
instance.language = attrs['language']
|
instance.language = attrs.get('language', instance.language)
|
||||||
instance.style = attrs['style']
|
instance.style = attrs.get('style', instance.style)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
# Create new instance
|
# Create new instance
|
||||||
|
|
|
@ -32,6 +32,7 @@ class Field(object):
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
empty = ''
|
empty = ''
|
||||||
type_name = None
|
type_name = None
|
||||||
|
partial = False
|
||||||
_use_files = None
|
_use_files = None
|
||||||
form_field_class = forms.CharField
|
form_field_class = forms.CharField
|
||||||
|
|
||||||
|
@ -53,7 +54,8 @@ class Field(object):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.root = parent.root or parent
|
self.root = parent.root or parent
|
||||||
self.context = self.root.context
|
self.context = self.root.context
|
||||||
if self.root.partial:
|
self.partial = self.root.partial
|
||||||
|
if self.partial:
|
||||||
self.required = False
|
self.required = False
|
||||||
|
|
||||||
def field_from_native(self, data, files, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
|
@ -186,7 +188,7 @@ class WritableField(Field):
|
||||||
else:
|
else:
|
||||||
native = data[field_name]
|
native = data[field_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.default is not None and not self.root.partial:
|
if self.default is not None and not self.partial:
|
||||||
# Note: partial updates shouldn't set defaults
|
# Note: partial updates shouldn't set defaults
|
||||||
native = self.default
|
native = self.default
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,8 +17,7 @@ class RelatedField(WritableField):
|
||||||
"""
|
"""
|
||||||
Base class for related model fields.
|
Base class for related model fields.
|
||||||
|
|
||||||
If not overridden, this represents a to-one relationship, using the unicode
|
This represents a relationship using the unicode representation of the target.
|
||||||
representation of the target.
|
|
||||||
"""
|
"""
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
many_widget = widgets.SelectMultiple
|
many_widget = widgets.SelectMultiple
|
||||||
|
@ -31,13 +30,18 @@ class RelatedField(WritableField):
|
||||||
many = False
|
many = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# 'null' will be deprecated in favor of 'required'
|
||||||
|
if 'null' in kwargs:
|
||||||
|
kwargs['required'] = not kwargs.pop('null')
|
||||||
|
|
||||||
self.queryset = kwargs.pop('queryset', None)
|
self.queryset = kwargs.pop('queryset', None)
|
||||||
self.null = kwargs.pop('null', False)
|
|
||||||
self.many = kwargs.pop('many', self.many)
|
self.many = kwargs.pop('many', self.many)
|
||||||
super(RelatedField, self).__init__(*args, **kwargs)
|
super(RelatedField, self).__init__(*args, **kwargs)
|
||||||
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
||||||
if self.many:
|
if self.many:
|
||||||
self.widget = self.many_widget
|
self.widget = self.many_widget
|
||||||
|
self.form_field_class = self.many_form_field_class
|
||||||
|
|
||||||
def initialize(self, parent, field_name):
|
def initialize(self, parent, field_name):
|
||||||
super(RelatedField, self).initialize(parent, field_name)
|
super(RelatedField, self).initialize(parent, field_name)
|
||||||
|
@ -56,11 +60,6 @@ class RelatedField(WritableField):
|
||||||
|
|
||||||
### We need this stuff to make form choices work...
|
### 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):
|
def prepare_value(self, obj):
|
||||||
return self.to_native(obj)
|
return self.to_native(obj)
|
||||||
|
|
||||||
|
@ -138,13 +137,13 @@ class RelatedField(WritableField):
|
||||||
else:
|
else:
|
||||||
value = data[field_name]
|
value = data[field_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.required:
|
if self.partial:
|
||||||
raise ValidationError(self.error_messages['required'])
|
|
||||||
return
|
return
|
||||||
|
value = [] if self.many else None
|
||||||
|
|
||||||
if value in (None, '') and not self.null:
|
if value in (None, '') and self.required:
|
||||||
raise ValidationError('Value may not be null')
|
raise ValidationError(self.error_messages['required'])
|
||||||
elif value in (None, '') and self.null:
|
elif value in (None, ''):
|
||||||
into[(self.source or field_name)] = None
|
into[(self.source or field_name)] = None
|
||||||
elif self.many:
|
elif self.many:
|
||||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||||
|
@ -156,7 +155,7 @@ class RelatedField(WritableField):
|
||||||
|
|
||||||
class PrimaryKeyRelatedField(RelatedField):
|
class PrimaryKeyRelatedField(RelatedField):
|
||||||
"""
|
"""
|
||||||
Represents a to-one relationship as a pk value.
|
Represents a relationship as a pk value.
|
||||||
"""
|
"""
|
||||||
default_read_only = False
|
default_read_only = False
|
||||||
|
|
||||||
|
@ -229,6 +228,9 @@ class PrimaryKeyRelatedField(RelatedField):
|
||||||
|
|
||||||
|
|
||||||
class SlugRelatedField(RelatedField):
|
class SlugRelatedField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represents a relationship using a unique field on the target.
|
||||||
|
"""
|
||||||
default_read_only = False
|
default_read_only = False
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -262,7 +264,7 @@ class SlugRelatedField(RelatedField):
|
||||||
|
|
||||||
class HyperlinkedRelatedField(RelatedField):
|
class HyperlinkedRelatedField(RelatedField):
|
||||||
"""
|
"""
|
||||||
Represents a to-one relationship, using hyperlinking.
|
Represents a relationship using hyperlinking.
|
||||||
"""
|
"""
|
||||||
pk_url_kwarg = 'pk'
|
pk_url_kwarg = 'pk'
|
||||||
slug_field = 'slug'
|
slug_field = 'slug'
|
||||||
|
|
|
@ -332,9 +332,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
|
||||||
kwargs['label'] = k
|
kwargs['label'] = k
|
||||||
|
|
||||||
if getattr(v, 'many', None):
|
|
||||||
fields[k] = v.many_form_field_class(**kwargs)
|
|
||||||
else:
|
|
||||||
fields[k] = v.form_field_class(**kwargs)
|
fields[k] = v.form_field_class(**kwargs)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
|
@ -443,7 +443,7 @@ class ModelSerializer(Serializer):
|
||||||
# TODO: filter queryset using:
|
# TODO: filter queryset using:
|
||||||
# .using(db).complex_filter(self.rel.limit_choices_to)
|
# .using(db).complex_filter(self.rel.limit_choices_to)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'null': model_field.null or model_field.blank,
|
'required': not(model_field.null or model_field.blank),
|
||||||
'queryset': model_field.rel.to._default_manager
|
'queryset': model_field.rel.to._default_manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,7 +469,7 @@ class ModelSerializer(Serializer):
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
kwargs['default'] = model_field.get_default()
|
kwargs['default'] = model_field.get_default()
|
||||||
|
|
||||||
if model_field.__class__ == models.TextField:
|
if issubclass(model_field.__class__, models.TextField):
|
||||||
kwargs['widget'] = widgets.Textarea
|
kwargs['widget'] = widgets.Textarea
|
||||||
|
|
||||||
# TODO: TypedChoiceField?
|
# TODO: TypedChoiceField?
|
||||||
|
@ -513,6 +513,22 @@ class ModelSerializer(Serializer):
|
||||||
exclusions.remove(field_name)
|
exclusions.remove(field_name)
|
||||||
return exclusions
|
return exclusions
|
||||||
|
|
||||||
|
def full_clean(self, instance):
|
||||||
|
"""
|
||||||
|
Perform Django's full_clean, and populate the `errors` dictionary
|
||||||
|
if any validation errors occur.
|
||||||
|
|
||||||
|
Note that we don't perform this inside the `.restore_object()` method,
|
||||||
|
so that subclasses can override `.restore_object()`, and still get
|
||||||
|
the full_clean validation checking.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance.full_clean(exclude=self.get_validation_exclusions())
|
||||||
|
except ValidationError, err:
|
||||||
|
self._errors = err.message_dict
|
||||||
|
return None
|
||||||
|
return instance
|
||||||
|
|
||||||
def restore_object(self, attrs, instance=None):
|
def restore_object(self, attrs, instance=None):
|
||||||
"""
|
"""
|
||||||
Restore the model instance.
|
Restore the model instance.
|
||||||
|
@ -544,14 +560,16 @@ class ModelSerializer(Serializer):
|
||||||
else:
|
else:
|
||||||
instance = self.opts.model(**attrs)
|
instance = self.opts.model(**attrs)
|
||||||
|
|
||||||
try:
|
|
||||||
instance.full_clean(exclude=self.get_validation_exclusions())
|
|
||||||
except ValidationError, err:
|
|
||||||
self._errors = err.message_dict
|
|
||||||
return None
|
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def from_native(self, data, files):
|
||||||
|
"""
|
||||||
|
Override the default method to also include model field validation.
|
||||||
|
"""
|
||||||
|
instance = super(ModelSerializer, self).from_native(data, files)
|
||||||
|
if instance:
|
||||||
|
return self.full_clean(instance)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object and return it.
|
||||||
|
@ -615,7 +633,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
# .using(db).complex_filter(self.rel.limit_choices_to)
|
# .using(db).complex_filter(self.rel.limit_choices_to)
|
||||||
rel = model_field.rel.to
|
rel = model_field.rel.to
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'null': model_field.null,
|
'required': not(model_field.null or model_field.blank),
|
||||||
'queryset': rel._default_manager,
|
'queryset': rel._default_manager,
|
||||||
'view_name': self._get_default_view_name(rel)
|
'view_name': self._get_default_view_name(rel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,14 +25,14 @@
|
||||||
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div id="div_id_username" class="clearfix control-group">
|
<div id="div_id_username" class="clearfix control-group">
|
||||||
<div class="controls" style="height: 30px">
|
<div class="controls">
|
||||||
<Label class="span4" style="margin-top: 3px">Username:</label>
|
<Label class="span4">Username:</label>
|
||||||
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="div_id_password" class="clearfix control-group">
|
<div id="div_id_password" class="clearfix control-group">
|
||||||
<div class="controls" style="height: 30px">
|
<div class="controls">
|
||||||
<Label class="span4" style="margin-top: 3px">Password:</label>
|
<Label class="span4">Password:</label>
|
||||||
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,3 +31,17 @@ class FieldTests(TestCase):
|
||||||
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
|
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestManyRelateMixin(TestCase):
|
||||||
|
def test_missing_many_to_many_related_field(self):
|
||||||
|
'''
|
||||||
|
Regression test for #632
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/pull/632
|
||||||
|
'''
|
||||||
|
field = serializers.ManyRelatedField(read_only=False)
|
||||||
|
|
||||||
|
into = {}
|
||||||
|
field.field_from_native({}, None, 'field_name', into)
|
||||||
|
self.assertEqual(into['field_name'], [])
|
||||||
|
|
|
@ -291,7 +291,7 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
|
self.assertEquals(serializer.errors, {'target': [u'This field is required.']})
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedNullableForeignKeyTests(TestCase):
|
class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
|
|
|
@ -149,7 +149,7 @@ class PKForeignKeyTests(TestCase):
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
|
self.assertEquals(serializer.errors, {'target': [u'This field is required.']})
|
||||||
|
|
||||||
|
|
||||||
class SlugNullableForeignKeyTests(TestCase):
|
class SlugNullableForeignKeyTests(TestCase):
|
||||||
|
|
|
@ -55,6 +55,19 @@ class ActionItemSerializer(serializers.ModelSerializer):
|
||||||
model = ActionItem
|
model = ActionItem
|
||||||
|
|
||||||
|
|
||||||
|
class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ActionItem
|
||||||
|
|
||||||
|
def restore_object(self, data, instance=None):
|
||||||
|
if instance is None:
|
||||||
|
return ActionItem(**data)
|
||||||
|
for key, val in data.items():
|
||||||
|
setattr(instance, key, val)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class PersonSerializer(serializers.ModelSerializer):
|
class PersonSerializer(serializers.ModelSerializer):
|
||||||
info = serializers.Field(source='info')
|
info = serializers.Field(source='info')
|
||||||
|
|
||||||
|
@ -274,6 +287,20 @@ class ValidationTests(TestCase):
|
||||||
self.assertEquals(serializer.is_valid(), False)
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
|
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
|
||||||
|
|
||||||
|
def test_modelserializer_max_length_exceeded_with_custom_restore(self):
|
||||||
|
"""
|
||||||
|
When overriding ModelSerializer.restore_object, validation tests should still apply.
|
||||||
|
Regression test for #623.
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/pull/623
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
'title': 'x' * 201,
|
||||||
|
}
|
||||||
|
serializer = ActionItemSerializerCustomRestore(data=data)
|
||||||
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
|
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
|
||||||
|
|
||||||
def test_default_modelfield_max_length_exceeded(self):
|
def test_default_modelfield_max_length_exceeded(self):
|
||||||
data = {
|
data = {
|
||||||
'title': 'Testing "info" field...',
|
'title': 'Testing "info" field...',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user