Merge branch 'master' into many-fields

Conflicts:
	rest_framework/relations.py
This commit is contained in:
Tom Christie 2013-01-30 13:41:56 +00:00
commit be6df3ae3c
25 changed files with 235 additions and 76 deletions

View File

@ -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 `.authentication_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access. 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)
[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

View File

@ -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.
@ -230,7 +240,9 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 &mdash; 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."
>
> &mdash; [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/

View File

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

View File

@ -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."
>
> &mdash; [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

View File

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

View File

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

View File

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

View File

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

View File

@ -332,10 +332,7 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs['label'] = k kwargs['label'] = k
if getattr(v, 'many', None): fields[k] = v.form_field_class(**kwargs)
fields[k] = v.many_form_field_class(**kwargs)
else:
fields[k] = v.form_field_class(**kwargs)
return fields return fields

View File

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

View File

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

View File

@ -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'], [])

View File

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

View File

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

View File

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