aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2013-01-30 13:41:56 +0000
committerTom Christie2013-01-30 13:41:56 +0000
commitbe6df3ae3ce18bf4b55ae065ebd34198885e48df (patch)
tree3a96bb6a5075584add7e28c6d8d7f251ad785b4e
parent9a4d01d687d57601d37f9a930d37039cb9f6a6f2 (diff)
parent8021bb5d5089955b171173e60dcc0968e13d29ea (diff)
downloaddjango-rest-framework-be6df3ae3ce18bf4b55ae065ebd34198885e48df.tar.bz2
Merge branch 'master' into many-fields
Conflicts: rest_framework/relations.py
-rw-r--r--docs/api-guide/authentication.md24
-rw-r--r--docs/api-guide/fields.md14
-rw-r--r--docs/api-guide/permissions.md9
-rw-r--r--docs/api-guide/relations.md2
-rw-r--r--docs/api-guide/renderers.md5
-rw-r--r--docs/api-guide/requests.md4
-rw-r--r--docs/api-guide/serializers.md12
-rw-r--r--docs/api-guide/throttling.md10
-rw-r--r--docs/api-guide/views.md2
-rw-r--r--docs/index.md3
-rw-r--r--docs/template.html1
-rw-r--r--docs/topics/ajax-csrf-cors.md41
-rw-r--r--docs/topics/credits.md10
-rw-r--r--docs/topics/csrf.md12
-rw-r--r--docs/topics/release-notes.md16
-rw-r--r--docs/tutorial/1-serialization.md12
-rw-r--r--rest_framework/fields.py6
-rw-r--r--rest_framework/relations.py34
-rw-r--r--rest_framework/renderers.py5
-rw-r--r--rest_framework/serializers.py36
-rw-r--r--rest_framework/templates/rest_framework/login.html8
-rw-r--r--rest_framework/tests/relations.py14
-rw-r--r--rest_framework/tests/relations_hyperlink.py2
-rw-r--r--rest_framework/tests/relations_slug.py2
-rw-r--r--rest_framework/tests/serializer.py27
25 files changed, 235 insertions, 76 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index ac690bdc..59afc2b9 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -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.
-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
@@ -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 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/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 5bc8f7f7..3f8a36e2 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -193,6 +193,16 @@ A date and time representation.
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
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.
-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
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index fce68f6d..1814b811 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -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.
+## 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
[authentication]: authentication.md
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 351b5e09..9f5a04b2 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -67,7 +67,7 @@ For example, given the following models:
And a model serializer defined like this:
class BookmarkSerializer(serializers.ModelSerializer):
- tags = serializers.ManyRelatedField(source='tags')
+ tags = serializers.ManyRelatedField()
class Meta:
model = Bookmark
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index b4f7ec3d..4c1fdc53 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -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'`.
-**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`
@@ -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
[conneg]: content-negotiation.md
[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
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[application/vnd.github+json]: http://developer.github.com/v3/media/
diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md
index 72932f5d..39a34fcf 100644
--- a/docs/api-guide/requests.md
+++ b/docs/api-guide/requests.md
@@ -83,13 +83,13 @@ You won't typically need to access this property.
# 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
`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].
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index d98a602f..487502e9 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -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:
- class ClassNameField(serializers.WritableField):
+ class ClassNameField(serializers.Field):
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__
-
- def field_from_native(self, data, field_name, into):
- """
- We don't want to set anything when we revert this field.
- """
- pass
+ return obj.__class__
---
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index b03bc9e0..923593bc 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -150,8 +150,16 @@ User requests to either `ContactListView` or `ContactDetailView` would be restri
# 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`.
+## 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
diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md
index d1e42ec1..574020f9 100644
--- a/docs/api-guide/views.md
+++ b/docs/api-guide/views.md
@@ -85,7 +85,7 @@ The following methods are called before dispatching to the handler method.
## Dispatch methods
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)
diff --git a/docs/index.md b/docs/index.md
index 05c68b25..453a67b8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -117,6 +117,7 @@ The API guide is your complete reference manual to all the functionality provide
General guides to using REST framework.
+* [AJAX, CSRF & CORS][ajax-csrf-cors]
* [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi]
* [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
[settings]: api-guide/settings.md
-[csrf]: topics/csrf.md
+[ajax-csrf-cors]: topics/ajax-csrf-cors.md
[browser-enhancements]: topics/browser-enhancements.md
[browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
diff --git a/docs/template.html b/docs/template.html
index d789cc58..2a87e92b 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -89,6 +89,7 @@
<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/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/browsable-api{{ suffix }}">The Browsable API</a></li>
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md
new file mode 100644
index 00000000..f7d12940
--- /dev/null
+++ b/docs/topics/ajax-csrf-cors.md
@@ -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/
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 7cffbede..a67a8169 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -96,6 +96,11 @@ The following people have helped make REST framework great.
* Bruno Renié - [brutasse]
* Kevin Stone - [kevinastone]
* 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.
@@ -227,3 +232,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[brutasse]: https://github.com/brutasse
[kevinastone]: https://github.com/kevinastone
[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
diff --git a/docs/topics/csrf.md b/docs/topics/csrf.md
deleted file mode 100644
index 043144c1..00000000
--- a/docs/topics/csrf.md
+++ /dev/null
@@ -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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 0c3ebca0..70c915b7 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -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.
+## 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
+### 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
**Date**: 26th Jan 2013
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index f5ff167f..5f292211 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -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.
-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:
# Update existing instance
- instance.title = attrs['title']
- instance.code = attrs['code']
- instance.linenos = attrs['linenos']
- instance.language = attrs['language']
- instance.style = attrs['style']
+ instance.title = attrs.get('title', instance.title)
+ instance.code = attrs.get('code', instance.code)
+ instance.linenos = attrs.get('linenos', instance.linenos)
+ instance.language = attrs.get('language', instance.language)
+ instance.style = attrs.get('style', instance.style)
return instance
# Create new instance
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a788ecf2..d6689c4e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -32,6 +32,7 @@ class Field(object):
creation_counter = 0
empty = ''
type_name = None
+ partial = False
_use_files = None
form_field_class = forms.CharField
@@ -53,7 +54,8 @@ class Field(object):
self.parent = parent
self.root = parent.root or parent
self.context = self.root.context
- if self.root.partial:
+ self.partial = self.root.partial
+ if self.partial:
self.required = False
def field_from_native(self, data, files, field_name, into):
@@ -186,7 +188,7 @@ class WritableField(Field):
else:
native = data[field_name]
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
native = self.default
else:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index aee43206..221c72fb 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -17,8 +17,7 @@ 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.
+ This represents a relationship using the unicode representation of the target.
"""
widget = widgets.Select
many_widget = widgets.SelectMultiple
@@ -31,13 +30,18 @@ class RelatedField(WritableField):
many = False
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.null = kwargs.pop('null', False)
self.many = kwargs.pop('many', self.many)
super(RelatedField, self).__init__(*args, **kwargs)
self.read_only = kwargs.pop('read_only', self.default_read_only)
if self.many:
self.widget = self.many_widget
+ self.form_field_class = self.many_form_field_class
def initialize(self, 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...
- # 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)
@@ -138,13 +137,13 @@ class RelatedField(WritableField):
else:
value = data[field_name]
except KeyError:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+ if self.partial:
+ return
+ value = [] if self.many else None
- if value in (None, '') and not self.null:
- raise ValidationError('Value may not be null')
- elif value in (None, '') and self.null:
+ if value in (None, '') and self.required:
+ raise ValidationError(self.error_messages['required'])
+ elif value in (None, ''):
into[(self.source or field_name)] = None
elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value]
@@ -156,7 +155,7 @@ class RelatedField(WritableField):
class PrimaryKeyRelatedField(RelatedField):
"""
- Represents a to-one relationship as a pk value.
+ Represents a relationship as a pk value.
"""
default_read_only = False
@@ -229,6 +228,9 @@ class PrimaryKeyRelatedField(RelatedField):
class SlugRelatedField(RelatedField):
+ """
+ Represents a relationship using a unique field on the target.
+ """
default_read_only = False
default_error_messages = {
@@ -262,7 +264,7 @@ class SlugRelatedField(RelatedField):
class HyperlinkedRelatedField(RelatedField):
"""
- Represents a to-one relationship, using hyperlinking.
+ Represents a relationship using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 1f6e615f..ed11551b 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -332,10 +332,7 @@ class BrowsableAPIRenderer(BaseRenderer):
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
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 6ecc7b45..d02e1ada 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -443,7 +443,7 @@ class ModelSerializer(Serializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
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
}
@@ -469,7 +469,7 @@ class ModelSerializer(Serializer):
kwargs['required'] = False
kwargs['default'] = model_field.get_default()
- if model_field.__class__ == models.TextField:
+ if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
# TODO: TypedChoiceField?
@@ -513,6 +513,22 @@ class ModelSerializer(Serializer):
exclusions.remove(field_name)
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):
"""
Restore the model instance.
@@ -544,14 +560,16 @@ class ModelSerializer(Serializer):
else:
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
+ 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):
"""
Save the deserialized object and return it.
@@ -615,7 +633,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
kwargs = {
- 'null': model_field.null,
+ 'required': not(model_field.null or model_field.blank),
'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html
index 6e2bd8d4..e10ce20f 100644
--- a/rest_framework/templates/rest_framework/login.html
+++ b/rest_framework/templates/rest_framework/login.html
@@ -25,14 +25,14 @@
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
{% csrf_token %}
<div id="div_id_username" class="clearfix control-group">
- <div class="controls" style="height: 30px">
- <Label class="span4" style="margin-top: 3px">Username:</label>
+ <div class="controls">
+ <Label class="span4">Username:</label>
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
</div>
</div>
<div id="div_id_password" class="clearfix control-group">
- <div class="controls" style="height: 30px">
- <Label class="span4" style="margin-top: 3px">Password:</label>
+ <div class="controls">
+ <Label class="span4">Password:</label>
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
</div>
</div>
diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py
index 91daea8a..edc85f9e 100644
--- a/rest_framework/tests/relations.py
+++ b/rest_framework/tests/relations.py
@@ -31,3 +31,17 @@ class FieldTests(TestCase):
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
self.assertRaises(serializers.ValidationError, field.from_native, '')
self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+
+class TestManyRelateMixin(TestCase):
+ def test_missing_many_to_many_related_field(self):
+ '''
+ Regression test for #632
+
+ https://github.com/tomchristie/django-rest-framework/pull/632
+ '''
+ field = serializers.ManyRelatedField(read_only=False)
+
+ into = {}
+ field.field_from_native({}, None, 'field_name', into)
+ self.assertEqual(into['field_name'], [])
diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py
index 6d137f68..7bc36dee 100644
--- a/rest_framework/tests/relations_hyperlink.py
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -291,7 +291,7 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
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):
diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py
index 37ccc75e..34d1f82a 100644
--- a/rest_framework/tests/relations_slug.py
+++ b/rest_framework/tests/relations_slug.py
@@ -149,7 +149,7 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
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):
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 4724348e..2cbd0c1a 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -55,6 +55,19 @@ class ActionItemSerializer(serializers.ModelSerializer):
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):
info = serializers.Field(source='info')
@@ -274,6 +287,20 @@ class ValidationTests(TestCase):
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_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):
data = {
'title': 'Testing "info" field...',