aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md77
-rw-r--r--docs/api-guide/authentication.md113
-rw-r--r--docs/api-guide/exceptions.md19
-rw-r--r--docs/api-guide/fields.md14
-rw-r--r--docs/api-guide/generic-views.md14
-rw-r--r--docs/api-guide/pagination.md6
-rw-r--r--docs/api-guide/parsers.md24
-rw-r--r--docs/api-guide/permissions.md9
-rw-r--r--docs/api-guide/relations.md2
-rw-r--r--docs/api-guide/renderers.md25
-rw-r--r--docs/api-guide/requests.md4
-rw-r--r--docs/api-guide/serializers.md12
-rw-r--r--docs/api-guide/settings.md4
-rw-r--r--docs/api-guide/throttling.md10
-rw-r--r--docs/api-guide/views.md2
-rw-r--r--docs/css/default.css17
-rw-r--r--docs/index.md37
-rw-r--r--docs/template.html1
-rw-r--r--docs/topics/ajax-csrf-cors.md41
-rw-r--r--docs/topics/credits.md40
-rw-r--r--docs/topics/csrf.md12
-rw-r--r--docs/topics/release-notes.md59
-rw-r--r--docs/tutorial/1-serialization.md67
-rw-r--r--docs/tutorial/2-requests-and-responses.md5
-rw-r--r--docs/tutorial/3-class-based-views.md2
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md15
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md10
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authentication.py83
-rw-r--r--rest_framework/authtoken/views.py3
-rw-r--r--rest_framework/compat.py6
-rw-r--r--rest_framework/decorators.py9
-rw-r--r--rest_framework/exceptions.py16
-rw-r--r--rest_framework/fields.py5
-rw-r--r--rest_framework/generics.py31
-rw-r--r--rest_framework/mixins.py19
-rw-r--r--rest_framework/pagination.py26
-rw-r--r--rest_framework/parsers.py2
-rw-r--r--rest_framework/relations.py112
-rw-r--r--rest_framework/renderers.py2
-rw-r--r--rest_framework/request.py29
-rw-r--r--rest_framework/serializers.py74
-rw-r--r--rest_framework/settings.py5
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/templates/rest_framework/login.html8
-rw-r--r--rest_framework/templatetags/rest_framework.py2
-rw-r--r--rest_framework/tests/authentication.py48
-rw-r--r--rest_framework/tests/decorators.py39
-rw-r--r--rest_framework/tests/extras/__init__.py0
-rw-r--r--rest_framework/tests/extras/bad_import.py1
-rw-r--r--rest_framework/tests/fields.py49
-rw-r--r--rest_framework/tests/files.py14
-rw-r--r--rest_framework/tests/genericrelations.py96
-rw-r--r--rest_framework/tests/generics.py19
-rw-r--r--rest_framework/tests/hyperlinkedserializers.py2
-rw-r--r--rest_framework/tests/models.py61
-rw-r--r--rest_framework/tests/pagination.py47
-rw-r--r--rest_framework/tests/relations.py47
-rw-r--r--rest_framework/tests/relations_hyperlink.py69
-rw-r--r--rest_framework/tests/relations_nested.py53
-rw-r--r--rest_framework/tests/relations_pk.py65
-rw-r--r--rest_framework/tests/relations_slug.py257
-rw-r--r--rest_framework/tests/request.py2
-rw-r--r--rest_framework/tests/serializer.py145
-rw-r--r--rest_framework/tests/settings.py21
-rw-r--r--rest_framework/tests/urlpatterns.py78
-rw-r--r--rest_framework/tests/utils.py27
-rw-r--r--rest_framework/tests/views.py4
-rw-r--r--rest_framework/urlpatterns.py45
-rw-r--r--rest_framework/utils/encoders.py6
-rw-r--r--rest_framework/views.py21
71 files changed, 1857 insertions, 436 deletions
diff --git a/README.md b/README.md
index e1d85f3c..523b7e74 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,44 @@ To run the tests.
# Changelog
+### 2.1.17
+
+**Date**: 26th Jan 2013
+
+* Support proper 401 Unauthorized responses where appropriate, instead of always using 403 Forbidden.
+* Support json encoding of timedelta objects.
+* `format_suffix_patterns()` now supports `include` style URL patterns.
+* Bugfix: Fix issues with custom pagination serializers.
+* Bugfix: Nested serializers now accept `source='*'` argument.
+* Bugfix: Return proper validation errors when incorrect types supplied for relational fields.
+* Bugfix: Support nullable FKs with `SlugRelatedField`.
+* Bugfix: Don't call custom validation methods if the field has an error.
+
+**Note**: If the primary authentication class is `TokenAuthentication` or `BasicAuthentication`, a view will now correctly return 401 responses to unauthenticated access, with an appropriate `WWW-Authenticate` header, instead of 403 responses.
+
+### 2.1.16
+
+**Date**: 14th Jan 2013
+
+* Deprecate django.utils.simplejson in favor of Python 2.6's built-in json module.
+* Bugfix: `auto_now`, `auto_now_add` and other `editable=False` fields now default to read-only.
+* Bugfix: PK fields now only default to read-only if they are an AutoField or if `editable=False`.
+* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types.
+* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
+* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
+
+### 2.1.15
+
+**Date**: 3rd Jan 2013
+
+* Added `PATCH` support.
+* Added `RetrieveUpdateAPIView`.
+* Relation changes are now persisted in `.save` instead of in `.restore_object`.
+* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`.
+* Tweak behavior of hyperlinked fields with an explicit format suffix.
+* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None.
+* Bugfix: Partial updates should not set default values if field is not included.
+
### 2.1.14
**Date**: 31st Dec 2012
@@ -108,20 +146,20 @@ This change will not affect user code, so long as it's following the recommended
* Bugfix: Fix exception in browseable API on DELETE.
* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg.
-## 2.1.11
+### 2.1.11
**Date**: 17th Dec 2012
* Bugfix: Fix issue with M2M fields in browseable API.
-## 2.1.10
+### 2.1.10
**Date**: 17th Dec 2012
* Bugfix: Ensure read-only fields don't have model validation applied.
* Bugfix: Fix hyperlinked fields in paginated results.
-## 2.1.9
+### 2.1.9
**Date**: 11th Dec 2012
@@ -129,14 +167,14 @@ This change will not affect user code, so long as it's following the recommended
* Bugfix: Fix `Meta.fields` only working as tuple not as list.
* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field.
-## 2.1.8
+### 2.1.8
**Date**: 8th Dec 2012
* Fix for creating nullable Foreign Keys with `''` as well as `None`.
* Added `null=<bool>` related field option.
-## 2.1.7
+### 2.1.7
**Date**: 7th Dec 2012
@@ -148,19 +186,19 @@ This change will not affect user code, so long as it's following the recommended
* Make `Request.user` settable.
* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`
-## 2.1.6
+### 2.1.6
**Date**: 23rd Nov 2012
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
-## 2.1.5
+### 2.1.5
**Date**: 23rd Nov 2012
* Bugfix: Fix DjangoModelPermissions.
-## 2.1.4
+### 2.1.4
**Date**: 22nd Nov 2012
@@ -171,7 +209,7 @@ This change will not affect user code, so long as it's following the recommended
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
-## 2.1.3
+### 2.1.3
**Date**: 16th Nov 2012
@@ -182,14 +220,14 @@ This change will not affect user code, so long as it's following the recommended
* 201 Responses now return a 'Location' header.
* Bugfix: Serializer fields now respect `max_length`.
-## 2.1.2
+### 2.1.2
**Date**: 9th Nov 2012
* **Filtering support.**
* Bugfix: Support creation of objects with reverse M2M relations.
-## 2.1.1
+### 2.1.1
**Date**: 7th Nov 2012
@@ -199,7 +237,7 @@ This change will not affect user code, so long as it's following the recommended
* Bugfix: Make textareas same width as other fields in browsable API.
* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
-## 2.1.0
+### 2.1.0
**Date**: 5th Nov 2012
@@ -212,13 +250,13 @@ This change will not affect user code, so long as it's following the recommended
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
* Bugfixes (Support choice field in Browseable API)
-## 2.0.2
+### 2.0.2
**Date**: 2nd Nov 2012
* Fix issues with pk related fields in the browsable API.
-## 2.0.1
+### 2.0.1
**Date**: 1st Nov 2012
@@ -226,16 +264,16 @@ This change will not affect user code, so long as it's following the recommended
* Added SlugRelatedField and ManySlugRelatedField.
* If PUT creates an instance return '201 Created', instead of '200 OK'.
-## 2.0.0
+### 2.0.0
**Date**: 30th Oct 2012
* Redesign of core components.
-* Fix **all of the things**.
+* **Fix all of the things**.
# License
-Copyright (c) 2011, Tom Christie
+Copyright (c) 2011-2013, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -258,7 +296,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
+[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
@@ -271,5 +309,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
[pyyaml]: http://pypi.python.org/pypi/PyYAML
-[django-filter]: https://github.com/alex/django-filter
-
+[django-filter]: http://pypi.python.org/pypi/django-filter
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 43fc15d2..59afc2b9 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -8,7 +8,7 @@
Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The [permission] and [throttling] policies can then use those credentials to determine if the request should be permitted.
-REST framework provides a number of authentication policies out of the box, and also allows you to implement custom policies.
+REST framework provides a number of authentication schemes out of the box, and also allows you to implement custom schemes.
Authentication will run the first time either the `request.user` or `request.auth` properties are accessed, and determines how those properties are initialized.
@@ -16,17 +16,25 @@ The `request.user` property will typically be set to an instance of the `contrib
The `request.auth` property is used for any additional authentication information, for example, it may be used to represent an authentication token that the request was signed with.
+---
+
+**Note:** Don't forget that **authentication by itself won't allow or disallow an incoming request**, it simply identifies the credentials that the request was made with.
+
+For information on how to setup the permission polices for your API please see the [permissions documentation][permission].
+
+---
+
## How authentication is determined
-The authentication policy is always defined as a list of classes. REST framework will attempt to authenticate with each class in the list, and will set `request.user` and `request.auth` using the return value of the first class that successfully authenticates.
+The authentication schemes are always defined as a list of classes. REST framework will attempt to authenticate with each class in the list, and will set `request.user` and `request.auth` using the return value of the first class that successfully authenticates.
If no class authenticates, `request.user` will be set to an instance of `django.contrib.auth.models.AnonymousUser`, and `request.auth` will be set to `None`.
The value of `request.user` and `request.auth` for unauthenticated requests can be modified using the `UNAUTHENTICATED_USER` and `UNAUTHENTICATED_TOKEN` settings.
-## Setting the authentication policy
+## Setting the authentication scheme
-The default authentication policy may be set globally, using the `DEFAULT_AUTHENTICATION_CLASSES` setting. For example.
+The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
@@ -35,7 +43,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
)
}
-You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
+You can also set the authentication scheme on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
@@ -52,7 +60,7 @@ Or, if you're using the `@api_view` decorator with function based views.
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
- @permissions_classes((IsAuthenticated,))
+ @permission_classes((IsAuthenticated,))
def example_view(request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
@@ -60,24 +68,52 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
+## Unauthorized and Forbidden responses
+
+When an unauthenticated request is denied permission there are two different error codes that may be appropriate.
+
+* [HTTP 401 Unauthorized][http401]
+* [HTTP 403 Permission Denied][http403]
+
+HTTP 401 responses must always include a `WWW-Authenticate` header, that instructs the client how to authenticate. HTTP 403 responses do not include the `WWW-Authenticate` header.
+
+The kind of response that will be used depends on the authentication scheme. Although multiple authentication schemes may be in use, only one scheme may be used to determine the type of response. **The first authentication class set on the view is used when determining the type of response**.
+
+Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
+
+## Apache mod_wsgi specific configuration
+
+Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
+
+If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`.
+
+ # this can go in either server config, virtual host, directory or .htaccess
+ WSGIPassAuthorization On
+
+---
+
# API Reference
## BasicAuthentication
-This policy uses [HTTP Basic Authentication][basicauth], signed against a user's username and password. Basic authentication is generally only appropriate for testing.
+This authentication scheme uses [HTTP Basic Authentication][basicauth], signed against a user's username and password. Basic authentication is generally only appropriate for testing.
If successfully authenticated, `BasicAuthentication` provides the following credentials.
* `request.user` will be a Django `User` instance.
* `request.auth` will be `None`.
+Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
+
+ WWW-Authenticate: Basic realm="api"
+
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
## TokenAuthentication
-This policy uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
+This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
-To use the `TokenAuthentication` policy, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting.
+To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting.
You'll also need to create tokens for your users.
@@ -93,10 +129,15 @@ For clients to authenticate, the token key should be included in the `Authorizat
If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a Django `User` instance.
-* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
+* `request.auth` will be a `rest_framework.authtoken.models.BasicToken` instance.
+
+Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
+
+ WWW-Authenticate: Token
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
+=======
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
@receiver(post_save, sender=User)
@@ -125,32 +166,58 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }
-<!--
-## OAuthAuthentication
-
-This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf.
-
-If successfully authenticated, `OAuthAuthentication` provides the following credentials.
-
-* `request.user` will be a Django `User` instance.
-* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
--->
-
## SessionAuthentication
-This policy uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website.
+This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website.
If successfully authenticated, `SessionAuthentication` provides the following credentials.
* `request.user` will be a Django `User` instance.
* `request.auth` will be `None`.
+Unauthenticated responses that are denied permission will result in an `HTTP 403 Forbidden` response.
+
+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
-To implement a custom authentication policy, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
+To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
+
+In some circumstances instead of returning `None`, you may want to raise an `AuthenticationFailed` exception from the `.authenticate()` method.
+
+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 `.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)
+
[cite]: http://jacobian.org/writing/rest-worst-practices/
+[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
+[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
[basicauth]: http://tools.ietf.org/html/rfc2617
[oauth]: http://oauth.net/2/
[permission]: permissions.md
[throttling]: throttling.md
+[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
+[mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index ba57fde8..8b3e50f1 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -53,11 +53,27 @@ Raised if the request contains malformed data when accessing `request.DATA` or `
By default this exception results in a response with the HTTP status code "400 Bad Request".
+## AuthenticationFailed
+
+**Signature:** `AuthenticationFailed(detail=None)`
+
+Raised when an incoming request includes incorrect authentication.
+
+By default this exception results in a response with the HTTP status code "401 Unauthenticated", but it may also result in a "403 Forbidden" response, depending on the authentication scheme in use. See the [authentication documentation][authentication] for more details.
+
+## NotAuthenticated
+
+**Signature:** `NotAuthenticated(detail=None)`
+
+Raised when an unauthenticated request fails the permission checks.
+
+By default this exception results in a response with the HTTP status code "401 Unauthenticated", but it may also result in a "403 Forbidden" response, depending on the authentication scheme in use. See the [authentication documentation][authentication] for more details.
+
## PermissionDenied
**Signature:** `PermissionDenied(detail=None)`
-Raised when an incoming request fails the permission checks.
+Raised when an authenticated request fails the permission checks.
By default this exception results in a response with the HTTP status code "403 Forbidden".
@@ -86,3 +102,4 @@ Raised when an incoming request fails the throttling checks.
By default this exception results in a response with the HTTP status code "429 Too Many Requests".
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
+[authentication]: authentication.md
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/generic-views.md b/docs/api-guide/generic-views.md
index 27c7d3f6..693e210d 100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin]
Used for **update-only** endpoints for a **single model instance**.
-Provides a `put` method handler.
+Provides `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [UpdateModelMixin]
@@ -97,6 +97,14 @@ Provides `get` and `post` method handlers.
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin]
+## RetrieveUpdateAPIView
+
+Used for **read or update** endpoints to represent a **single model instance**.
+
+Provides `get`, `put` and `patch` method handlers.
+
+Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin]
+
## RetrieveDestroyAPIView
Used for **read or delete** endpoints to represent a **single model instance**.
@@ -109,7 +117,7 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]
Used for **read-write-delete** endpoints to represent a **single model instance**.
-Provides `get`, `put` and `delete` method handlers.
+Provides `get`, `put`, `patch` and `delete` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
@@ -197,6 +205,8 @@ If an object is created, for example when making a `DELETE` request followed by
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
+A boolean `partial` keyword argument may be supplied to the `.update()` method. If `partial` is set to `True`, all fields for the update will be optional. This allows support for HTTP `PATCH` requests.
+
Should be mixed in with [SingleObjectAPIView].
## DestroyModelMixin
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index ab335e6e..51c0fb4b 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -97,6 +97,8 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
paginate_by = 10
paginate_by_param = 'page_size'
+Note that using a `paginate_by` value of `None` will turn off pagination for the view.
+
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
---
@@ -112,8 +114,8 @@ You can also override the name used for the object list field, by setting the `r
For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
class LinksSerializer(serializers.Serializer):
- next = pagination.NextURLField(source='*')
- prev = pagination.PreviousURLField(source='*')
+ next = pagination.NextPageField(source='*')
+ prev = pagination.PreviousPageField(source='*')
class CustomPaginationSerializer(pagination.BasePaginationSerializer):
links = LinksSerializer(source='*') # Takes the page object as the source
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 185b616c..0cd01639 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -14,6 +14,16 @@ REST framework includes a number of built in Parser classes, that allow you to a
The set of valid parsers for a view is always defined as a list of classes. When either `request.DATA` or `request.FILES` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content.
+---
+
+**Note**: When developing client applications always remember to make sure you're setting the `Content-Type` header when sending data in an HTTP request.
+
+If you don't set the content type, most clients will default to using `'application/x-www-form-urlencoded'`, which may not be what you wanted.
+
+As an example, if you are sending `json` encoded data using jQuery with the [.ajax() method][jquery-ajax], you should make sure to include the `contentType: 'application/json'` setting.
+
+---
+
## Setting the parsers
The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content.
@@ -159,4 +169,18 @@ For example:
files = {name: uploaded}
return DataAndFiles(data, files)
+---
+
+# Third party packages
+
+The following third party packages are also available.
+
+## MessagePack
+
+[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
+
+[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
+[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
+[juanriaza]: https://github.com/juanriaza
+[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack \ No newline at end of file
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 374ff0ab..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`
@@ -271,13 +271,32 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
+---
+
+# Third party packages
+
+The following third party packages are also available.
+
+## MessagePack
+
+[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
+
+## CSV
+
+Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
[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/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
-[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views \ No newline at end of file
+[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
+[messagepack]: http://msgpack.org/
+[juanriaza]: https://github.com/juanriaza
+[mjumbewu]: https://github.com/mjumbewu
+[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
+[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv \ No newline at end of file
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/settings.md b/docs/api-guide/settings.md
index 7884d096..a422e5f6 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -65,7 +65,7 @@ Default:
(
'rest_framework.authentication.SessionAuthentication',
- 'rest_framework.authentication.UserBasicAuthentication'
+ 'rest_framework.authentication.BasicAuthentication'
)
## DEFAULT_PERMISSION_CLASSES
@@ -106,7 +106,7 @@ The default page size to use for pagination. If set to `None`, pagination is di
Default: `None`
-## PAGINATE_BY_KWARG
+## PAGINATE_BY_PARAM
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
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/css/default.css b/docs/css/default.css
index 57446ff9..07c4884d 100644
--- a/docs/css/default.css
+++ b/docs/css/default.css
@@ -25,18 +25,29 @@ pre {
margin-top: 9px;
}
+body.index-page #main-content p.badges {
+ padding-bottom: 1px;
+}
+
/* GitHub 'Star' badge */
-body.index-page #main-content iframe {
+body.index-page #main-content iframe.github-star-button {
float: right;
margin-top: -12px;
margin-right: -15px;
}
+/* Tweet button */
+body.index-page #main-content iframe.twitter-share-button {
+ float: right;
+ margin-top: -12px;
+ margin-right: 8px;
+}
+
/* Travis CI badge */
-body.index-page #main-content p:first-of-type {
+body.index-page #main-content img.travis-build-image {
float: right;
margin-right: 8px;
- margin-top: -14px;
+ margin-top: -9px;
margin-bottom: 0px;
}
diff --git a/docs/index.md b/docs/index.md
index 4d50e5d6..453a67b8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,5 +1,11 @@
-<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
-[![Travis build image][travis-build-image]][travis]
+<p class="badges">
+<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
+
+<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Current status: Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none">Tweet</a>
+<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
+
+<img alt="Travis build image" src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
+</p>
# Django REST framework
@@ -111,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]
@@ -132,13 +139,18 @@ Run the tests:
## Support
-For support please see the [REST framework discussion group][group], or try the `#restframework` channel on `irc.freenode.net`.
+For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
+
+[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
-Paid support is also available from [DabApps], and can include work on REST framework core, or support with building your REST framework API. Please contact [Tom Christie][email] if you'd like to discuss commercial support options.
+For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter.
+<a style="padding-top: 10px" href="https://twitter.com/_tomchristie" class="twitter-follow-button" data-show-count="false">Follow @_tomchristie</a>
+<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
+
## License
-Copyright (c) 2011-2012, Tom Christie
+Copyright (c) 2011-2013, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -162,11 +174,11 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
-[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
+[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML
-[django-filter]: https://github.com/alex/django-filter
+[django-filter]: http://pypi.python.org/pypi/django-filter
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[sandbox]: http://restframework.herokuapp.com/
@@ -199,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
@@ -209,5 +221,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[credits]: topics/credits.md
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
-[DabApps]: http://dabapps.com
-[email]: mailto:tom@tomchristie.com
+[stack-overflow]: http://stackoverflow.com/
+[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
+[django-tag]: http://stackoverflow.com/questions/tagged/django
+[paid-support]: http://dabapps.com/services/build/api-development/
+[dabapps]: http://dabapps.com
+[contact-dabapps]: http://dabapps.com/contact/
+[twitter]: https://twitter.com/_tomchristie
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 c4277a23..a67a8169 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -2,7 +2,7 @@
The following people have helped make REST framework great.
-* Tom Christie - [tomchristie]
+* Tom Christie - [tomchristie]
* Marko Tibold - [markotibold]
* Paul Bagwell - [pbgwl]
* Sébastien Piquemal - [sebpiq]
@@ -84,6 +84,23 @@ The following people have helped make REST framework great.
* Roman Akinfold - [akinfold]
* Toran Billups - [toranb]
* Sébastien Béal - [sebastibe]
+* Andrew Hankinson - [ahankinson]
+* Juan Riaza - [juanriaza]
+* Michael Mior - [michaelmior]
+* Marc Tamlyn - [mjtamlyn]
+* Richard Wackerbarth - [wackerbarth]
+* Johannes Spielmann - [shezi]
+* James Cleveland - [radiosilence]
+* Steve Gregory - [steve-gregory]
+* Federico Capoano - [nemesisdesign]
+* 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.
@@ -95,7 +112,7 @@ Project hosting is with [GitHub].
Continuous integration testing is managed with [Travis CI][travis-ci].
-The [live sandbox][sandbox] is hosted on [Heroku].
+The [live sandbox][sandbox] is hosted on [Heroku].
Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects.
@@ -106,7 +123,7 @@ Development of REST framework 2.0 was sponsored by [DabApps].
For usage questions please see the [REST framework discussion group][group].
You can also contact [@_tomchristie][twitter] directly on twitter.
-
+
[email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie
[bootstrap]: http://twitter.github.com/bootstrap/
@@ -203,3 +220,20 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[akinfold]: https://github.com/akinfold
[toranb]: https://github.com/toranb
[sebastibe]: https://github.com/sebastibe
+[ahankinson]: https://github.com/ahankinson
+[juanriaza]: https://github.com/juanriaza
+[michaelmior]: https://github.com/michaelmior
+[mjtamlyn]: https://github.com/mjtamlyn
+[wackerbarth]: https://github.com/wackerbarth
+[shezi]: https://github.com/shezi
+[radiosilence]: https://github.com/radiosilence
+[steve-gregory]: https://github.com/steve-gregory
+[nemesisdesign]: https://github.com/nemesisdesign
+[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 5b34bf3d..70c915b7 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -12,13 +12,65 @@ 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
-* Relation changes are no longer persisted in `.restore_object`
+* 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
+
+* Support proper 401 Unauthorized responses where appropriate, instead of always using 403 Forbidden.
+* Support json encoding of timedelta objects.
+* `format_suffix_patterns()` now supports `include` style URL patterns.
+* Bugfix: Fix issues with custom pagination serializers.
+* Bugfix: Nested serializers now accept `source='*'` argument.
+* Bugfix: Return proper validation errors when incorrect types supplied for relational fields.
+* Bugfix: Support nullable FKs with `SlugRelatedField`.
+* Bugfix: Don't call custom validation methods if the field has an error.
+
+**Note**: If the primary authentication class is `TokenAuthentication` or `BasicAuthentication`, a view will now correctly return 401 responses to unauthenticated access, with an appropriate `WWW-Authenticate` header, instead of 403 responses.
+
+### 2.1.16
+
+**Date**: 14th Jan 2013
+
+* Deprecate `django.utils.simplejson` in favor of Python 2.6's built-in json module.
+* Bugfix: `auto_now`, `auto_now_add` and other `editable=False` fields now default to read-only.
+* Bugfix: PK fields now only default to read-only if they are an AutoField or if `editable=False`.
+* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types.
+* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
+* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
+
+**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details.
+
+### 2.1.15
+
+**Date**: 3rd Jan 2013
+
+* Added `PATCH` support.
+* Added `RetrieveUpdateAPIView`.
+* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`.
+* Tweak behavior of hyperlinked fields with an explicit format suffix.
+* Relation changes are now persisted in `.save()` instead of in `.restore_object()`.
+* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None.
+* Bugfix: Partial updates should not set default values if field is not included.
### 2.1.14
@@ -28,9 +80,9 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi
* Bugfix: Model fields with `blank=True` are now `required=False` by default.
* Bugfix: Nested serializers now support nullable relationships.
-**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`.
+**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to separate them from regular data type fields, such as `CharField` and `IntegerField`.
-This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`.
+This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and referring to fields using the style `serializers.PrimaryKeyRelatedField`.
### 2.1.13
@@ -300,3 +352,4 @@ This change will not affect user code, so long as it's following the recommended
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[announcement]: rest-framework-2-announcement.md
+[#582]: https://github.com/tomchristie/django-rest-framework/issues/582
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index e61fb946..5f292211 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -4,11 +4,11 @@
This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together.
-The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started.<!-- If you just want a quick overview, you should head over to the [quickstart] documentation instead. -->
+The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead.
---
-**Note**: The final code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. There is also a sandbox version for testing, [available here][sandbox].
+**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. As pieces of code are introduced, they are committed to this repository. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
---
@@ -60,7 +60,7 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
INSTALLED_APPS = (
...
'rest_framework',
- 'snippets'
+ 'snippets',
)
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
@@ -73,14 +73,15 @@ Okay, we're ready to roll.
## Creating a model to work with
-For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file.
+For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
-
- LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in get_all_lexers()])
- STYLE_CHOICES = sorted((item, item) for item in list(get_all_styles()))
+
+ LEXERS = [item for item in get_all_lexers() if item[1]]
+ LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
+ STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
class Snippet(models.Model):
@@ -108,7 +109,7 @@ The first thing we need to get started on our Web API is provide a way of serial
from django.forms import widgets
from rest_framework import serializers
- from snippets import models
+ from snippets.models import Snippet
class SnippetSerializer(serializers.Serializer):
@@ -129,15 +130,15 @@ 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
- return models.Snippet(**attrs)
+ return Snippet(**attrs)
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
@@ -202,8 +203,6 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
-
-
## Writing regular Django views using our Serializer
Let's see how we can write some API views using our new Serializer class.
@@ -229,7 +228,6 @@ Edit the `snippet/views.py` file, and add the following.
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
-
The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet.
@csrf_exempt
@@ -288,16 +286,45 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
urlpatterns = patterns('snippets.views',
url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
+ url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail'),
)
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
## Testing our first attempt at a Web API
-**TODO: Describe using runserver and making example requests from console**
+Now we can start up a sample server that serves our snippets.
+
+Quit out of the shell
+
+ quit()
+
+and start up Django's development server
+
+ python manage.py runserver
+
+ Validating models...
+
+ 0 errors found
+ Django version 1.4.3, using settings 'tutorial.settings'
+ Development server is running at http://127.0.0.1:8000/
+ Quit the server with CONTROL-C.
+
+In another terminal window, we can test the server.
+
+We can get a list of all of the snippets (we only have one at the moment)
+
+ curl http://127.0.0.1:8000/snippets/
+
+ [{"id": 1, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}]
+
+or we can get a particular snippet by referencing its id
+
+ curl http://127.0.0.1:8000/snippets/1/
+
+ {"id": 1, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}
-**TODO: Describe opening in a web browser and viewing json output**
+Similarly, you can have the same json displayed by referencing these URLs from your favorite web browser.
## Where are we now
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index 08cf91cd..340ea28e 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -31,7 +31,6 @@ These wrappers provide a few bits of functionality such as making sure you recei
The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input.
-
## Pulling it all together
Okay, let's go ahead and start using these new components to write a few views.
@@ -63,7 +62,6 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
Here is the view for an individual snippet.
@@ -117,7 +115,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
urlpatterns = patterns('snippets.views',
url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
+ url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'),
)
urlpatterns = format_suffix_patterns(urlpatterns)
@@ -138,7 +136,6 @@ Because the API chooses a return format based on what the client asks for, it wi
See the [browsable api][browseable-api] topic for more information about the browsable API feature and how to customize it.
-
## What's next?
In [tutorial part 3][tut-3], we'll start using class based views, and see how generic views reduce the amount of code we need to write.
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index b115b022..290ea5e9 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -70,7 +70,7 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
urlpatterns = patterns('',
url(r'^snippets/$', views.SnippetList.as_view()),
- url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view())
+ url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
)
urlpatterns = format_suffix_patterns(urlpatterns)
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 9576a7f0..e9e5246a 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -22,14 +22,14 @@ We'd also need to make sure that when the model is saved, that we populate the h
We'll need some extra imports:
from pygments.lexers import get_lexer_by_name
- from pygments.formatters import HtmlFormatter
+ from pygments.formatters.html import HtmlFormatter
from pygments import highlight
And now we can add a `.save()` method to our model class:
def save(self, *args, **kwargs):
"""
- Use the `pygments` library to create an highlighted HTML
+ Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
@@ -54,6 +54,8 @@ You might also want to create a few different users, to use for testing the API.
Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy:
+ from django.contrib.auth.models import User
+
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.ManyPrimaryKeyRelatedField()
@@ -77,7 +79,7 @@ We'll also add a couple of views. We'd like to just use read-only views for the
Finally we need to add those views into the API, by referencing them from the URL conf.
url(r'^users/$', views.UserList.as_view()),
- url(r'^users/(?P<pk>[0-9]+)/$', views.UserInstance.as_view())
+ url(r'^users/(?P<pk>[0-9]+)/$', views.UserInstance.as_view()),
## Associating Snippets with Users
@@ -134,7 +136,7 @@ And, at the end of the file, add a pattern to include the login and logout views
urlpatterns += patterns('',
url(r'^api-auth/', include('rest_framework.urls',
- namespace='rest_framework'))
+ namespace='rest_framework')),
)
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
@@ -164,7 +166,8 @@ In the snippets app, create a new file, `permissions.py`
if obj is None:
return True
- # Read permissions are allowed to any request
+ # Read permissions are allowed to any request,
+ # so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
@@ -188,4 +191,4 @@ We've now got a fairly fine-grained set of permissions on our Web API, and end p
In [part 5][tut-5] of the tutorial we'll look at how we can tie everything together by creating an HTML endpoint for our hightlighted snippets, and improve the cohesion of our API by using hyperlinking for the relationships within the system.
-[tut-5]: 5-relationships-and-hyperlinked-apis.md \ No newline at end of file
+[tut-5]: 5-relationships-and-hyperlinked-apis.md
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index 216ca433..de856611 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -15,8 +15,8 @@ Right now we have endpoints for 'snippets' and 'users', but we don't have a sing
@api_view(('GET',))
def api_root(request, format=None):
return Response({
- 'users': reverse('user-list', request=request),
- 'snippets': reverse('snippet-list', request=request)
+ 'users': reverse('user-list', request=request, format=format),
+ 'snippets': reverse('snippet-list', request=request, format=format)
})
Notice that we're using REST framework's `reverse` function in order to return fully-qualified URLs.
@@ -116,7 +116,7 @@ After adding all those names into our URLconf, our final `'urls.py'` file should
url(r'^snippets/(?P<pk>[0-9]+)/$',
views.SnippetDetail.as_view(),
name='snippet-detail'),
- url(r'^snippets/(?P<pk>[0-9]+)/highlight/$'
+ url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
views.SnippetHighlight.as_view(),
name='snippet-highlight'),
url(r'^users/$',
@@ -130,7 +130,7 @@ After adding all those names into our URLconf, our final `'urls.py'` file should
# Login and logout views for the browsable API
urlpatterns += patterns('',
url(r'^api-auth/', include('rest_framework.urls',
- namespace='rest_framework'))
+ namespace='rest_framework')),
)
## Adding pagination
@@ -165,7 +165,7 @@ We've reached the end of our tutorial. If you want to get more involved in the
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
-* [Follow the author on Twitter][twitter] and say hi.
+* Follow [the author][twitter] on Twitter and say hi.
**Now go build awesome things.**
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 151ba832..f9882c57 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.14'
+__version__ = '2.1.17'
VERSION = __version__ # synonym
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index c50bf944..76ee4bd6 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -23,34 +23,47 @@ class BaseAuthentication(object):
"""
raise NotImplementedError(".authenticate() must be overridden.")
+ def authenticate_header(self, request):
+ """
+ Return a string to be used as the value of the `WWW-Authenticate`
+ header in a `401 Unauthenticated` response, or `None` if the
+ authentication scheme should return `403 Permission Denied` responses.
+ """
+ pass
+
class BasicAuthentication(BaseAuthentication):
"""
HTTP Basic authentication against username/password.
"""
+ www_authenticate_realm = 'api'
def authenticate(self, request):
"""
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
- if 'HTTP_AUTHORIZATION' in request.META:
- auth = request.META['HTTP_AUTHORIZATION'].split()
- if len(auth) == 2 and auth[0].lower() == "basic":
- try:
- encoding = api_settings.HTTP_HEADER_ENCODING
- b = base64.b64decode(auth[1].encode(encoding))
- auth_parts = b.decode(encoding).partition(':')
- except TypeError:
- return None
-
- try:
- userid = smart_text(auth_parts[0])
- password = smart_text(auth_parts[2])
- except DjangoUnicodeDecodeError:
- return None
-
- return self.authenticate_credentials(userid, password)
+ auth = request.META.get('HTTP_AUTHORIZATION', '').split()
+
+ if not auth or auth[0].lower() != "basic":
+ return None
+
+ if len(auth) != 2:
+ raise exceptions.AuthenticationFailed('Invalid basic header')
+
+ encoding = api_settings.HTTP_HEADER_ENCODING
+ try:
+ auth_parts = base64.b64decode(auth[1].encode(encoding)).partition(':')
+ except TypeError:
+ raise exceptions.AuthenticationFailed('Invalid basic header')
+
+ try:
+ userid = smart_text(auth_parts[0])
+ password = smart_text(auth_parts[2])
+ except DjangoUnicodeDecodeError:
+ raise exceptions.AuthenticationFailed('Invalid basic header')
+
+ return self.authenticate_credentials(userid, password)
def authenticate_credentials(self, userid, password):
"""
@@ -59,6 +72,10 @@ class BasicAuthentication(BaseAuthentication):
user = authenticate(username=userid, password=password)
if user is not None and user.is_active:
return (user, None)
+ raise exceptions.AuthenticationFailed('Invalid username/password')
+
+ def authenticate_header(self, request):
+ return 'Basic realm="%s"' % self.www_authenticate_realm
class SessionAuthentication(BaseAuthentication):
@@ -78,7 +95,7 @@ class SessionAuthentication(BaseAuthentication):
# Unauthenticated, CSRF validation not required
if not user or not user.is_active:
- return
+ return None
# Enforce CSRF validation for session based authentication.
class CSRFCheck(CsrfViewMiddleware):
@@ -89,7 +106,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(http_request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
- raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
+ raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
# CSRF passed with authenticated user
return (user, None)
@@ -116,14 +133,26 @@ class TokenAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
- if len(auth) == 2 and auth[0].lower() == "token":
- key = auth[1]
- try:
- token = self.model.objects.get(key=key)
- except self.model.DoesNotExist:
- return None
+ if not auth or auth[0].lower() != "token":
+ return None
+
+ if len(auth) != 2:
+ raise exceptions.AuthenticationFailed('Invalid token header')
+
+ return self.authenticate_credentials(auth[1])
+
+ def authenticate_credentials(self, key):
+ try:
+ token = self.model.objects.get(key=key)
+ except self.model.DoesNotExist:
+ raise exceptions.AuthenticationFailed('Invalid token')
+
+ if token.user.is_active:
+ return (token.user, token)
+ raise exceptions.AuthenticationFailed('User inactive or deleted')
+
+ def authenticate_header(self, request):
+ return 'Token'
- if token.user.is_active:
- return (token.user, token)
# TODO: OAuthAuthentication
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index d318c723..7c03cb76 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -12,10 +12,11 @@ class ObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
+ serializer_class = AuthTokenSerializer
model = Token
def post(self, request):
- serializer = AuthTokenSerializer(data=request.DATA)
+ serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 5924cd6d..ef11b85b 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -126,6 +126,12 @@ else:
update_wrapper(view, cls.dispatch, assigned=())
return view
+# Taken from @markotibold's attempt at supporting PATCH.
+# https://github.com/markotibold/django-rest-framework/tree/patch
+http_method_names = set(View.http_method_names)
+http_method_names.add('patch')
+View.http_method_names = list(http_method_names) # PATCH method is not implemented by Django
+
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index 1b710a03..7a4103e1 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -1,4 +1,5 @@
from rest_framework.views import APIView
+import types
def api_view(http_method_names):
@@ -23,6 +24,14 @@ def api_view(http_method_names):
# pass
# WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
+ # api_view applied without (method_names)
+ assert not(isinstance(http_method_names, types.FunctionType)), \
+ '@api_view missing list of allowed HTTP methods'
+
+ # api_view applied with eg. string instead of list of strings
+ assert isinstance(http_method_names, (list, tuple)), \
+ '@api_view expected a list of strings, recieved %s' % type(http_method_names).__name__
+
allowed_methods = set(http_method_names) | set(('options',))
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 89479deb..d635351c 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -23,6 +23,22 @@ class ParseError(APIException):
self.detail = detail or self.default_detail
+class AuthenticationFailed(APIException):
+ status_code = status.HTTP_401_UNAUTHORIZED
+ default_detail = 'Incorrect authentication credentials.'
+
+ def __init__(self, detail=None):
+ self.detail = detail or self.default_detail
+
+
+class NotAuthenticated(APIException):
+ status_code = status.HTTP_401_UNAUTHORIZED
+ default_detail = 'Authentication credentials were not provided.'
+
+ def __init__(self, detail=None):
+ self.detail = detail or self.default_detail
+
+
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.'
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index adea5bf5..a66e1d7c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,4 +1,3 @@
-
from __future__ import unicode_literals
import copy
@@ -185,11 +184,13 @@ class WritableField(Field):
try:
if self._use_files:
+ files = files or {}
native = files[field_name]
else:
native = data[field_name]
except KeyError:
- if self.default is not None:
+ if self.default is not None and not self.root.partial:
+ # Note: partial updates shouldn't set defaults
native = self.default
else:
if self.required:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index dd8dfcf8..19f2b704 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -47,14 +47,16 @@ class GenericAPIView(views.APIView):
return serializer_class
- def get_serializer(self, instance=None, data=None, files=None):
+ def get_serializer(self, instance=None, data=None,
+ files=None, partial=False):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
- return serializer_class(instance, data=data, files=files, context=context)
+ return serializer_class(instance, data=data, files=files,
+ partial=partial, context=context)
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
@@ -171,6 +173,10 @@ class UpdateAPIView(mixins.UpdateModelMixin,
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
+ def patch(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
@@ -185,6 +191,23 @@ class ListCreateAPIView(mixins.ListModelMixin,
return self.create(request, *args, **kwargs)
+class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ SingleObjectAPIView):
+ """
+ Concrete view for retrieving, updating a model instance.
+ """
+ def get(self, request, *args, **kwargs):
+ return self.retrieve(request, *args, **kwargs)
+
+ def put(self, request, *args, **kwargs):
+ return self.update(request, *args, **kwargs)
+
+ def patch(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
+
class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
SingleObjectAPIView):
@@ -211,5 +234,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
+ def patch(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 503376ce..acaf8a71 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -18,11 +18,14 @@ class CreateModelMixin(object):
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save()
headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+ return Response(serializer.data, status=status.HTTP_201_CREATED,
+ headers=headers)
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_success_headers(self, data):
@@ -84,20 +87,21 @@ class UpdateModelMixin(object):
Should be mixed in with `SingleObjectBaseView`.
"""
def update(self, request, *args, **kwargs):
+ partial = kwargs.pop('partial', False)
try:
self.object = self.get_object()
- created = False
+ success_status_code = status.HTTP_200_OK
except Http404:
self.object = None
- created = True
+ success_status_code = status.HTTP_201_CREATED
- serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES)
+ serializer = self.get_serializer(self.object, data=request.DATA,
+ files=request.FILES, partial=partial)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save()
- status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK
- return Response(serializer.data, status=status_code)
+ return Response(serializer.data, status=success_status_code)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -117,7 +121,8 @@ class UpdateModelMixin(object):
# Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg.
- obj.full_clean()
+ if hasattr(obj, 'full_clean'):
+ obj.full_clean()
class DestroyModelMixin(object):
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d241ade7..92d41e0e 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -34,6 +34,17 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page)
+class DefaultObjectSerializer(serializers.Field):
+ """
+ If no object serializer is specified, then this serializer will be applied
+ as the default.
+ """
+
+ def __init__(self, source=None, context=None):
+ # Note: Swallow context kwarg - only required for eg. ModelSerializer.
+ super(DefaultObjectSerializer, self).__init__(source=source)
+
+
class PaginationSerializerOptions(serializers.SerializerOptions):
"""
An object that stores the options that may be provided to a
@@ -44,7 +55,7 @@ class PaginationSerializerOptions(serializers.SerializerOptions):
def __init__(self, meta):
super(PaginationSerializerOptions, self).__init__(meta)
self.object_serializer_class = getattr(meta, 'object_serializer_class',
- serializers.Field)
+ DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer):
@@ -62,14 +73,13 @@ class BasePaginationSerializer(serializers.Serializer):
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
results_field = self.results_field
object_serializer = self.opts.object_serializer_class
- self.fields[results_field] = object_serializer(source='object_list')
- def to_native(self, obj):
- """
- Prevent default behaviour of iterating over elements, and serializing
- each in turn.
- """
- return self.convert_object(obj)
+ if 'context' in kwargs:
+ context_kwarg = {'context': kwargs['context']}
+ else:
+ context_kwarg = {}
+
+ self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 7c01006a..4a2b34a5 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -8,12 +8,12 @@ on the request, such as form content or json encoded data.
from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError
-from django.utils import simplejson as json
from rest_framework.compat import yaml, ETParseError
from rest_framework.exceptions import ParseError
from rest_framework.compat import six
from xml.etree import ElementTree as ET
from xml.parsers.expat import ExpatError
+import json
import datetime
import decimal
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index b7a6e0c1..c4f854ef 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -6,6 +6,7 @@ from django.core.urlresolvers import resolve, get_script_prefix
from django import forms
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
+from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
@@ -103,7 +104,13 @@ class RelatedField(WritableField):
### Regular serializer stuff...
def field_to_native(self, obj, field_name):
- value = getattr(obj, self.source or field_name)
+ try:
+ value = getattr(obj, self.source or field_name)
+ except ObjectDoesNotExist:
+ return None
+
+ if value is None:
+ return None
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
@@ -144,7 +151,7 @@ class ManyRelatedMixin(object):
value = data.getlist(self.source or field_name)
except:
# Non-form data
- value = data.get(self.source or field_name)
+ value = data.get(self.source or field_name, [])
else:
if value == ['']:
value = []
@@ -171,6 +178,11 @@ class PrimaryKeyRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
+ default_error_messages = {
+ 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
+ }
+
# TODO: Remove these field hacks...
def prepare_value(self, obj):
return self.to_native(obj.pk)
@@ -196,7 +208,11 @@ class PrimaryKeyRelatedField(RelatedField):
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
- msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
+ msg = self.error_messages['does_not_exist'] % smart_text(data)
+ raise ValidationError(msg)
+ except (TypeError, ValueError):
+ received = type(data).__name__
+ msg = self.error_messages['incorrect_type'] % received
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
@@ -205,7 +221,10 @@ class PrimaryKeyRelatedField(RelatedField):
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
- obj = getattr(obj, self.source or field_name)
+ try:
+ obj = getattr(obj, self.source or field_name)
+ except ObjectDoesNotExist:
+ return None
return self.to_native(obj.pk)
# Forward relationship
return self.to_native(pk)
@@ -218,6 +237,11 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
default_read_only = False
form_field_class = forms.MultipleChoiceField
+ default_error_messages = {
+ 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
+ }
+
def prepare_value(self, obj):
return self.to_native(obj.pk)
@@ -252,7 +276,11 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
- msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
+ msg = self.error_messages['does_not_exist'] % smart_text(data)
+ raise ValidationError(msg)
+ except (TypeError, ValueError):
+ received = type(data).__name__
+ msg = self.error_messages['incorrect_type'] % received
raise ValidationError(msg)
### Slug relationships
@@ -262,6 +290,11 @@ class SlugRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
+ default_error_messages = {
+ 'does_not_exist': _("Object with %s=%s does not exist."),
+ 'invalid': _('Invalid value.'),
+ }
+
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
@@ -277,8 +310,11 @@ class SlugRelatedField(RelatedField):
try:
return self.queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
- raise ValidationError('Object with %s=%s does not exist.' %
+ raise ValidationError(self.error_messages['does_not_exist'] %
(self.slug_field, unicode(data)))
+ except (TypeError, ValueError):
+ msg = self.error_messages['invalid']
+ raise ValidationError(msg)
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
@@ -297,6 +333,14 @@ class HyperlinkedRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
+ default_error_messages = {
+ 'no_match': _('Invalid hyperlink - No URL match'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
+ 'configuration_error': _('Invalid hyperlink due to configuration error'),
+ 'does_not_exist': _("Invalid hyperlink - object does not exist."),
+ 'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
+ }
+
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
@@ -333,21 +377,21 @@ class HyperlinkedRelatedField(RelatedField):
slug = getattr(obj, self.slug_field, None)
if not slug:
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+ raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+ raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
def from_native(self, value):
# Convert URL -> model instance pk
@@ -355,7 +399,13 @@ class HyperlinkedRelatedField(RelatedField):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
- if value.startswith('http:') or value.startswith('https:'):
+ try:
+ http_prefix = value.startswith('http:') or value.startswith('https:')
+ except AttributeError:
+ msg = self.error_messages['incorrect_type']
+ raise ValidationError(msg % type(value).__name__)
+
+ if http_prefix:
# If needed convert absolute URLs to relative path
value = urlparse.urlparse(value).path
prefix = get_script_prefix()
@@ -365,10 +415,10 @@ class HyperlinkedRelatedField(RelatedField):
try:
match = resolve(value)
except:
- raise ValidationError('Invalid hyperlink - No URL match')
+ raise ValidationError(self.error_messages['no_match'])
- if match.url_name != self.view_name:
- raise ValidationError('Invalid hyperlink - Incorrect URL match')
+ if match.view_name != self.view_name:
+ raise ValidationError(self.error_messages['incorrect_match'])
pk = match.kwargs.get(self.pk_url_kwarg, None)
slug = match.kwargs.get(self.slug_url_kwarg, None)
@@ -380,14 +430,18 @@ class HyperlinkedRelatedField(RelatedField):
elif slug is not None:
slug_field = self.get_slug_field()
queryset = self.queryset.filter(**{slug_field: slug})
- # If none of those are defined, it's an error.
+ # If none of those are defined, it's probably a configuation error.
else:
- raise ValidationError('Invalid hyperlink')
+ raise ValidationError(self.error_messages['configuration_error'])
try:
obj = queryset.get()
except ObjectDoesNotExist:
- raise ValidationError('Invalid hyperlink - object does not exist.')
+ raise ValidationError(self.error_messages['does_not_exist'])
+ except (TypeError, ValueError):
+ msg = self.error_messages['incorrect_type']
+ raise ValidationError(msg % type(value).__name__)
+
return obj
@@ -410,6 +464,7 @@ class HyperlinkedIdentityField(Field):
# TODO: Make view_name mandatory, and have the
# HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
+ # Optionally the format of the target hyperlink may be specified
self.format = kwargs.pop('format', None)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
@@ -421,9 +476,22 @@ class HyperlinkedIdentityField(Field):
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
- format = self.format or self.context.get('format', None)
+ format = self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
kwargs = {self.pk_url_kwarg: obj.pk}
+
+ # By default use whatever format is given for the current context
+ # unless the target is a different type to the source.
+ #
+ # Eg. Consider a HyperlinkedIdentityField pointing from a json
+ # representation to an html property of that representation...
+ #
+ # '/snippets/1/' should link to '/snippets/1/highlight/'
+ # ...but...
+ # '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
+ if format and self.format and self.format != format:
+ format = self.format
+
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
@@ -432,18 +500,18 @@ class HyperlinkedIdentityField(Field):
slug = getattr(obj, self.slug_field, None)
if not slug:
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+ raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+ raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 54930167..b3ee0690 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -10,10 +10,10 @@ from __future__ import unicode_literals
import copy
import string
+import json
from django import forms
from django.http.multipartparser import parse_header
from django.template import RequestContext, loader, Template
-from django.utils import simplejson as json
from rest_framework.compat import yaml
from rest_framework.exceptions import ConfigurationError
from rest_framework.settings import api_settings
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 048a1c41..23e1da87 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -86,6 +86,7 @@ class Request(object):
self._method = Empty
self._content_type = Empty
self._stream = Empty
+ self._authenticator = None
if self.parser_context is None:
self.parser_context = {}
@@ -166,7 +167,7 @@ class Request(object):
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
- self._user, self._auth = self._authenticate()
+ self._authenticator, self._user, self._auth = self._authenticate()
return self._user
@user.setter
@@ -185,7 +186,7 @@ class Request(object):
request, such as an authentication token.
"""
if not hasattr(self, '_auth'):
- self._user, self._auth = self._authenticate()
+ self._authenticator, self._user, self._auth = self._authenticate()
return self._auth
@auth.setter
@@ -196,6 +197,14 @@ class Request(object):
"""
self._auth = value
+ @property
+ def successful_authenticator(self):
+ """
+ Return the instance of the authentication instance class that was used
+ to authenticate the request, or `None`.
+ """
+ return self._authenticator
+
def _load_data_and_files(self):
"""
Parses the request content into self.DATA and self.FILES.
@@ -299,21 +308,23 @@ class Request(object):
def _authenticate(self):
"""
- Attempt to authenticate the request using each authentication instance in turn.
- Returns a two-tuple of (user, authtoken).
+ Attempt to authenticate the request using each authentication instance
+ in turn.
+ Returns a three-tuple of (authenticator, user, authtoken).
"""
for authenticator in self.authenticators:
user_auth_tuple = authenticator.authenticate(self)
if not user_auth_tuple is None:
- return user_auth_tuple
+ user, auth = user_auth_tuple
+ return (authenticator, user, auth)
return self._not_authenticated()
def _not_authenticated(self):
"""
- Return a two-tuple of (user, authtoken), representing an
- unauthenticated request.
+ Return a three-tuple of (authenticator, user, authtoken), representing
+ an unauthenticated request.
- By default this will be (AnonymousUser, None).
+ By default this will be (None, AnonymousUser, None).
"""
if api_settings.UNAUTHENTICATED_USER:
user = api_settings.UNAUTHENTICATED_USER()
@@ -325,7 +336,7 @@ class Request(object):
else:
auth = None
- return (user, auth)
+ return (None, user, auth)
def __getattr__(self, attr):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 663f166b..3d3bcb3c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -2,6 +2,7 @@ import copy
import datetime
import types
from decimal import Decimal
+from django.core.paginator import Page
from django.db import models
from django.forms import widgets
from django.utils.datastructures import SortedDict
@@ -209,6 +210,11 @@ class BaseSerializer(Field):
Converts a dictionary of data into a dictionary of deserialized fields.
"""
reverted_data = {}
+
+ if data is not None and not isinstance(data, dict):
+ self._errors['non_field_errors'] = [u'Invalid data']
+ return None
+
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
try:
@@ -223,6 +229,8 @@ class BaseSerializer(Field):
Run `validate_<fieldname>()` and `validate()` methods on the serializer
"""
for field_name, field in self.fields.items():
+ if field_name in self._errors:
+ continue
try:
validate_method = getattr(self, 'validate_%s' % field_name, None)
if validate_method:
@@ -267,7 +275,11 @@ class BaseSerializer(Field):
"""
Serialize objects -> primitives.
"""
- if hasattr(obj, '__iter__'):
+ # Note: At the moment we have an ugly hack to determine if we should
+ # walk over iterables. At some point, serializers will require an
+ # explicit `many=True` in order to iterate over a set, and this hack
+ # will disappear.
+ if hasattr(obj, '__iter__') and not isinstance(obj, Page):
return [self.convert_object(item) for item in obj]
return self.convert_object(obj)
@@ -277,7 +289,7 @@ class BaseSerializer(Field):
"""
if hasattr(data, '__iter__') and not isinstance(data, dict):
# TODO: error data when deserializing lists
- return (self.from_native(item) for item in data)
+ return [self.from_native(item, None) for item in data]
self._errors = {}
if data is not None or files is not None:
@@ -294,15 +306,21 @@ class BaseSerializer(Field):
Override default so that we can apply ModelSerializer as a nested
field to relationships.
"""
- if self.source:
- for component in self.source.split('.'):
- obj = getattr(obj, component)
+ if self.source == '*':
+ return self.to_native(obj)
+
+ try:
+ if self.source:
+ for component in self.source.split('.'):
+ obj = getattr(obj, component)
+ if is_simple_callable(obj):
+ obj = obj()
+ else:
+ obj = getattr(obj, field_name)
if is_simple_callable(obj):
obj = obj()
- else:
- obj = getattr(obj, field_name)
- if is_simple_callable(obj):
- obj = value()
+ except ObjectDoesNotExist:
+ return None
# If the object has an "all" method, assume it's a relationship
if is_simple_callable(getattr(obj, 'all', None)):
@@ -408,7 +426,7 @@ class ModelSerializer(Serializer):
"""
Returns a default instance of the pk field.
"""
- return Field()
+ return self.get_field(model_field)
def get_nested_field(self, model_field):
"""
@@ -426,7 +444,7 @@ class ModelSerializer(Serializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
kwargs = {
- 'null': model_field.null,
+ 'null': model_field.null or model_field.blank,
'queryset': model_field.rel.to._default_manager
}
@@ -445,11 +463,14 @@ class ModelSerializer(Serializer):
if model_field.null or model_field.blank:
kwargs['required'] = False
+ if isinstance(model_field, models.AutoField) or not model_field.editable:
+ kwargs['read_only'] = True
+
if model_field.has_default():
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?
@@ -458,6 +479,7 @@ class ModelSerializer(Serializer):
return ChoiceField(**kwargs)
field_mapping = {
+ models.AutoField: IntegerField,
models.FloatField: FloatField,
models.IntegerField: IntegerField,
models.PositiveIntegerField: IntegerField,
@@ -492,6 +514,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.
@@ -531,13 +569,21 @@ class ModelSerializer(Serializer):
return instance
- def save(self, save_m2m=True):
+ 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.
"""
self.object.save()
- if getattr(self, 'm2m_data', None) and save_m2m:
+ if getattr(self, 'm2m_data', None):
for accessor_name, object_list in self.m2m_data.items():
setattr(self.object, accessor_name, object_list)
self.m2m_data = {}
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 2358d188..13d03e62 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -119,9 +119,8 @@ def import_from_string(val, setting_name):
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path)
return getattr(module, class_name)
- except:
- raise
- msg = "Could not import '%s' for API setting '%s'" % (val, setting_name)
+ except ImportError as e:
+ msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 42e49cb9..092bf2e4 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -112,7 +112,7 @@
<div class="request-info">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
- <div>
+ </div>
<div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|urlize_quoted_links }}</span>
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/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 4205e57c..cbafbe0e 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -27,7 +27,7 @@ register = template.Library()
# conflicts with this rest_framework template tag module.
try: # Django 1.5+
- from django.contrib.staticfiles.templatetags import StaticFilesNode
+ from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
@register.tag('static')
def do_static(parser, token):
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index 8c0bfc47..ba2042cb 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -1,14 +1,13 @@
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.test import Client, TestCase
-from django.utils import simplejson as json
-
from rest_framework import permissions
from rest_framework.authtoken.models import Token
-from rest_framework.authentication import TokenAuthentication
+from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication
from rest_framework.compat import patterns
from rest_framework.views import APIView
+import json
import base64
@@ -21,10 +20,10 @@ class MockView(APIView):
def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-MockView.authentication_classes += (TokenAuthentication,)
-
urlpatterns = patterns('',
- (r'^$', MockView.as_view()),
+ (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
+ (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
+ (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
)
@@ -42,25 +41,26 @@ class BasicAuthTests(TestCase):
def test_post_form_passing_basic_auth(self):
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
- auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('iso-8859-1')).strip().decode('iso-8859-1')
- response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).encode('iso-8859-1').strip().decode('iso-8859-1')
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_json_passing_basic_auth(self):
"""Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
- auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('iso-8859-1')).strip().decode('iso-8859-1')
- response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
+ auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).encode('iso-8859-1').strip().decode('iso-8859-1')
+ response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_form_failing_basic_auth(self):
"""Ensure POSTing form over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/', {'example': 'example'})
- self.assertEqual(response.status_code, 403)
+ response = self.csrf_client.post('/basic/', {'example': 'example'})
+ self.assertEqual(response.status_code, 401)
def test_post_json_failing_basic_auth(self):
"""Ensure POSTing json over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
- self.assertEqual(response.status_code, 403)
+ response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json')
+ self.assertEqual(response.status_code, 401)
+ self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
class SessionAuthTests(TestCase):
@@ -83,7 +83,7 @@ class SessionAuthTests(TestCase):
Ensure POSTing form over session authentication without CSRF token fails.
"""
self.csrf_client.login(username=self.username, password=self.password)
- response = self.csrf_client.post('/', {'example': 'example'})
+ response = self.csrf_client.post('/session/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
def test_post_form_session_auth_passing(self):
@@ -91,7 +91,7 @@ class SessionAuthTests(TestCase):
Ensure POSTing form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.post('/', {'example': 'example'})
+ response = self.non_csrf_client.post('/session/', {'example': 'example'})
self.assertEqual(response.status_code, 200)
def test_put_form_session_auth_passing(self):
@@ -99,14 +99,14 @@ class SessionAuthTests(TestCase):
Ensure PUTting form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.put('/', {'example': 'example'})
+ response = self.non_csrf_client.put('/session/', {'example': 'example'})
self.assertEqual(response.status_code, 200)
def test_post_form_session_auth_failing(self):
"""
Ensure POSTing form over session authentication without logged in user fails.
"""
- response = self.csrf_client.post('/', {'example': 'example'})
+ response = self.csrf_client.post('/session/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
@@ -127,24 +127,24 @@ class TokenAuthTests(TestCase):
def test_post_form_passing_token_auth(self):
"""Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
auth = "Token " + self.key
- response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_json_passing_token_auth(self):
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
auth = "Token " + self.key
- response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
+ response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_form_failing_token_auth(self):
"""Ensure POSTing form over token auth without correct credentials fails"""
- response = self.csrf_client.post('/', {'example': 'example'})
- self.assertEqual(response.status_code, 403)
+ response = self.csrf_client.post('/token/', {'example': 'example'})
+ self.assertEqual(response.status_code, 401)
def test_post_json_failing_token_auth(self):
"""Ensure POSTing json over token auth without correct credentials fails"""
- response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
- self.assertEqual(response.status_code, 403)
+ response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json')
+ self.assertEqual(response.status_code, 401)
def test_token_has_auto_assigned_key_if_none_provided(self):
"""Ensure creating a token with no key will auto-assign a key"""
diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py
index 8079c8cb..82f912e9 100644
--- a/rest_framework/tests/decorators.py
+++ b/rest_framework/tests/decorators.py
@@ -1,5 +1,4 @@
from django.test import TestCase
-from django.test.client import RequestFactory
from rest_framework import status
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
@@ -17,6 +16,8 @@ from rest_framework.decorators import (
permission_classes,
)
+from rest_framework.tests.utils import RequestFactory
+
class DecoratorTestCase(TestCase):
@@ -27,13 +28,27 @@ class DecoratorTestCase(TestCase):
response.request = request
return APIView.finalize_response(self, request, response, *args, **kwargs)
- def test_wrap_view(self):
+ def test_api_view_incorrect(self):
+ """
+ If @api_view is not applied correct, we should raise an assertion.
+ """
- @api_view(['GET'])
+ @api_view
def view(request):
- return Response({})
+ return Response()
+
+ request = self.factory.get('/')
+ self.assertRaises(AssertionError, view, request)
- self.assertTrue(isinstance(view.cls_instance, APIView))
+ def test_api_view_incorrect_arguments(self):
+ """
+ If @api_view is missing arguments, we should raise an assertion.
+ """
+
+ with self.assertRaises(AssertionError):
+ @api_view('GET')
+ def view(request):
+ return Response()
def test_calling_method(self):
@@ -63,6 +78,20 @@ class DecoratorTestCase(TestCase):
response = view(request)
self.assertEqual(response.status_code, 405)
+ def test_calling_patch_method(self):
+
+ @api_view(['GET', 'PATCH'])
+ def view(request):
+ return Response({})
+
+ request = self.factory.patch('/')
+ response = view(request)
+ self.assertEqual(response.status_code, 200)
+
+ request = self.factory.post('/')
+ response = view(request)
+ self.assertEqual(response.status_code, 405)
+
def test_renderer_classes(self):
@api_view(['GET'])
diff --git a/rest_framework/tests/extras/__init__.py b/rest_framework/tests/extras/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rest_framework/tests/extras/__init__.py
diff --git a/rest_framework/tests/extras/bad_import.py b/rest_framework/tests/extras/bad_import.py
new file mode 100644
index 00000000..68263d94
--- /dev/null
+++ b/rest_framework/tests/extras/bad_import.py
@@ -0,0 +1 @@
+raise ValueError
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
new file mode 100644
index 00000000..8068272d
--- /dev/null
+++ b/rest_framework/tests/fields.py
@@ -0,0 +1,49 @@
+"""
+General serializer field tests.
+"""
+
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+class TimestampedModel(models.Model):
+ added = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+
+
+class CharPrimaryKeyModel(models.Model):
+ id = models.CharField(max_length=20, primary_key=True)
+
+
+class TimestampedModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TimestampedModel
+
+
+class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = CharPrimaryKeyModel
+
+
+class ReadOnlyFieldTests(TestCase):
+ def test_auto_now_fields_read_only(self):
+ """
+ auto_now and auto_now_add fields should be read_only by default.
+ """
+ serializer = TimestampedModelSerializer()
+ self.assertEquals(serializer.fields['added'].read_only, True)
+
+ def test_auto_pk_fields_read_only(self):
+ """
+ AutoField fields should be read_only by default.
+ """
+ serializer = TimestampedModelSerializer()
+ self.assertEquals(serializer.fields['id'].read_only, True)
+
+ def test_non_auto_pk_fields_not_read_only(self):
+ """
+ PK fields other than AutoField fields should not be read_only by default.
+ """
+ serializer = CharPrimaryKeyModelSerializer()
+ self.assertEquals(serializer.fields['id'].read_only, False)
diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py
index ca6bc905..0434f900 100644
--- a/rest_framework/tests/files.py
+++ b/rest_framework/tests/files.py
@@ -26,7 +26,6 @@ class UploadedFileSerializer(serializers.Serializer):
class FileSerializerTests(TestCase):
-
def test_create(self):
now = datetime.datetime.now()
file = BytesIO(six.b('stuff'))
@@ -38,3 +37,16 @@ class FileSerializerTests(TestCase):
self.assertEquals(serializer.object.created, uploaded_file.created)
self.assertEquals(serializer.object.file, uploaded_file.file)
self.assertFalse(serializer.object is uploaded_file)
+
+ def test_creation_failure(self):
+ """
+ Passing files=None should result in an ValidationError
+
+ Regression test for:
+ https://github.com/tomchristie/django-rest-framework/issues/542
+ """
+ now = datetime.datetime.now()
+
+ serializer = UploadedFileSerializer(data={'created': now})
+ self.assertFalse(serializer.is_valid())
+ self.assertIn('file', serializer.errors)
diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py
index ba29dbed..72070a1a 100644
--- a/rest_framework/tests/genericrelations.py
+++ b/rest_framework/tests/genericrelations.py
@@ -1,27 +1,63 @@
from __future__ import unicode_literals
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
+from django.db import models
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import *
+
+
+class Tag(models.Model):
+ """
+ Tags have a descriptive slug, and are attached to an arbitrary object.
+ """
+ tag = models.SlugField()
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ tagged_item = GenericForeignKey('content_type', 'object_id')
+
+ def __unicode__(self):
+ return self.tag
+
+
+class Bookmark(models.Model):
+ """
+ A URL bookmark that may have multiple tags attached.
+ """
+ url = models.URLField()
+ tags = GenericRelation(Tag)
+
+ def __unicode__(self):
+ return 'Bookmark: %s' % self.url
+
+
+class Note(models.Model):
+ """
+ A textual note that may have multiple tags attached.
+ """
+ text = models.TextField()
+ tags = GenericRelation(Tag)
+
+ def __unicode__(self):
+ return 'Note: %s' % self.text
class TestGenericRelations(TestCase):
def setUp(self):
- bookmark = Bookmark(url='https://www.djangoproject.com/')
- bookmark.save()
- django = Tag(tag_name='django')
- django.save()
- python = Tag(tag_name='python')
- python.save()
- t1 = TaggedItem(content_object=bookmark, tag=django)
- t1.save()
- t2 = TaggedItem(content_object=bookmark, tag=python)
- t2.save()
- self.bookmark = bookmark
-
- def test_reverse_generic_relation(self):
+ self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
+ Tag.objects.create(tagged_item=self.bookmark, tag='django')
+ Tag.objects.create(tagged_item=self.bookmark, tag='python')
+ self.note = Note.objects.create(text='Remember the milk')
+ Tag.objects.create(tagged_item=self.note, tag='reminder')
+
+ def test_generic_relation(self):
+ """
+ Test a relationship that spans a GenericRelation field.
+ IE. A reverse generic relationship.
+ """
+
class BookmarkSerializer(serializers.ModelSerializer):
- tags = serializers.ManyRelatedField(source='tags')
+ tags = serializers.ManyRelatedField()
class Meta:
model = Bookmark
@@ -33,3 +69,33 @@ class TestGenericRelations(TestCase):
'url': 'https://www.djangoproject.com/'
}
self.assertEquals(serializer.data, expected)
+
+ def test_generic_fk(self):
+ """
+ Test a relationship that spans a GenericForeignKey field.
+ IE. A forward generic relationship.
+ """
+
+ class TagSerializer(serializers.ModelSerializer):
+ tagged_item = serializers.RelatedField()
+
+ class Meta:
+ model = Tag
+ exclude = ('id', 'content_type', 'object_id')
+
+ serializer = TagSerializer(Tag.objects.all())
+ expected = [
+ {
+ 'tag': u'django',
+ 'tagged_item': u'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': u'python',
+ 'tagged_item': u'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': u'reminder',
+ 'tagged_item': u'Note: Remember the milk'
+ }
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index 215de0c4..fd01312a 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -1,12 +1,11 @@
from __future__ import unicode_literals
-
from django.db import models
from django.test import TestCase
-from django.test.client import RequestFactory
-from django.utils import simplejson as json
from rest_framework import generics, serializers, status
+from rest_framework.tests.utils import RequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
from rest_framework.compat import six
+import json
factory = RequestFactory()
@@ -183,6 +182,20 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1)
self.assertEquals(updated.text, 'foobar')
+ def test_patch_instance_view(self):
+ """
+ PATCH requests to RetrieveUpdateDestroyAPIView should update an object.
+ """
+ content = {'text': 'foobar'}
+ request = factory.patch('/1', json.dumps(content),
+ content_type='application/json')
+
+ response = self.view(request, pk=1).render()
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
+ self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
+ updated = self.objects.get(id=1)
+ self.assertEquals(updated.text, 'foobar')
+
def test_delete_instance_view(self):
"""
DELETE requests to RetrieveUpdateDestroyAPIView should delete an object.
diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py
index ee4d8e57..c6a8224b 100644
--- a/rest_framework/tests/hyperlinkedserializers.py
+++ b/rest_framework/tests/hyperlinkedserializers.py
@@ -1,6 +1,6 @@
+import json
from django.test import TestCase
from django.test.client import RequestFactory
-from django.utils import simplejson as json
from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 0759650a..9ab15328 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -71,6 +71,7 @@ class SlugBasedModel(RESTFrameworkModel):
class DefaultValueModel(RESTFrameworkModel):
text = models.CharField(default='foobar', max_length=100)
+ extra = models.CharField(blank=True, null=True, max_length=100)
class CallableDefaultValueModel(RESTFrameworkModel):
@@ -85,27 +86,6 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor')
rel = models.ManyToManyField(Anchor)
-# Models to test generic relations
-
-
-class Tag(RESTFrameworkModel):
- tag_name = models.SlugField()
-
-
-class TaggedItem(RESTFrameworkModel):
- tag = models.ForeignKey(Tag, related_name='items')
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = GenericForeignKey('content_type', 'object_id')
-
- def __unicode__(self):
- return self.tag.tag_name
-
-
-class Bookmark(RESTFrameworkModel):
- url = models.URLField()
- tags = GenericRelation(TaggedItem)
-
# Model to test filtering.
class FilterableItem(RESTFrameworkModel):
@@ -176,3 +156,42 @@ class OptionalRelationModel(RESTFrameworkModel):
# Model for RegexField
class Book(RESTFrameworkModel):
isbn = models.CharField(max_length=13)
+
+
+# Models for relations tests
+# ManyToMany
+class ManyToManyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+# ForeignKey
+class ForeignKeyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+
+
+# Nullable ForeignKey
+class NullableForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources')
+
+
+# OneToOne
+class OneToOneTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class NullableOneToOneSource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
+ related_name='nullable_source')
diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py
index 81d297a1..697dfb5b 100644
--- a/rest_framework/tests/pagination.py
+++ b/rest_framework/tests/pagination.py
@@ -181,10 +181,10 @@ class UnitTestPagination(TestCase):
"""
Ensure context gets passed through to the object serializer.
"""
- serializer = PassOnContextPaginationSerializer(self.first_page)
+ serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'})
serializer.data
results = serializer.fields[serializer.results_field]
- self.assertTrue(serializer.context is results.context)
+ self.assertEquals(serializer.context, results.context)
class TestUnpaginated(TestCase):
@@ -252,6 +252,8 @@ class TestCustomPaginateByParam(TestCase):
self.assertEquals(response.data['results'], self.data[:5])
+### Tests for context in pagination serializers
+
class CustomField(serializers.Field):
def to_native(self, value):
if not 'view' in self.context:
@@ -262,6 +264,11 @@ class CustomField(serializers.Field):
class BasicModelSerializer(serializers.Serializer):
text = CustomField()
+ def __init__(self, *args, **kwargs):
+ super(BasicModelSerializer, self).__init__(*args, **kwargs)
+ if not 'view' in self.context:
+ raise RuntimeError("context isn't getting passed into serializer init")
+
class TestContextPassedToCustomField(TestCase):
def setUp(self):
@@ -279,3 +286,39 @@ class TestContextPassedToCustomField(TestCase):
self.assertEquals(response.status_code, status.HTTP_200_OK)
+
+### Tests for custom pagination serializers
+
+class LinksSerializer(serializers.Serializer):
+ next = pagination.NextPageField(source='*')
+ prev = pagination.PreviousPageField(source='*')
+
+
+class CustomPaginationSerializer(pagination.BasePaginationSerializer):
+ links = LinksSerializer(source='*') # Takes the page object as the source
+ total_results = serializers.Field(source='paginator.count')
+
+ results_field = 'objects'
+
+
+class TestCustomPaginationSerializer(TestCase):
+ def setUp(self):
+ objects = ['john', 'paul', 'george', 'ringo']
+ paginator = Paginator(objects, 2)
+ self.page = paginator.page(1)
+
+ def test_custom_pagination_serializer(self):
+ request = RequestFactory().get('/foobar')
+ serializer = CustomPaginationSerializer(
+ instance=self.page,
+ context={'request': request}
+ )
+ expected = {
+ 'links': {
+ 'next': 'http://testserver/foobar?page=2',
+ 'prev': None
+ },
+ 'total_results': 4,
+ 'objects': ['john', 'paul']
+ }
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py
new file mode 100644
index 00000000..edc85f9e
--- /dev/null
+++ b/rest_framework/tests/relations.py
@@ -0,0 +1,47 @@
+"""
+General tests for relational fields.
+"""
+
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+class NullModel(models.Model):
+ pass
+
+
+class FieldTests(TestCase):
+ def test_pk_related_field_with_empty_string(self):
+ """
+ Regression test for #446
+
+ https://github.com/tomchristie/django-rest-framework/issues/446
+ """
+ field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
+ self.assertRaises(serializers.ValidationError, field.from_native, '')
+ self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+ def test_hyperlinked_related_field_with_empty_string(self):
+ field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
+ self.assertRaises(serializers.ValidationError, field.from_native, '')
+ self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+ def test_slug_related_field_with_empty_string(self):
+ field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
+ self.assertRaises(serializers.ValidationError, field.from_native, '')
+ self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+
+class TestManyRelateMixin(TestCase):
+ def test_missing_many_to_many_related_field(self):
+ '''
+ Regression test for #632
+
+ https://github.com/tomchristie/django-rest-framework/pull/632
+ '''
+ field = serializers.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 407c04e0..b4ad3166 100644
--- a/rest_framework/tests/relations_hyperlink.py
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
-from django.db import models
from django.test import TestCase
from rest_framework import serializers
from rest_framework.compat import patterns, url
+from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
def dummy_view(request, pk):
@@ -15,20 +15,11 @@ urlpatterns = patterns('',
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
+ url(r'^onetoonetarget/(?P<pk>[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
+ url(r'^nullableonetoonesource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
)
-# ManyToMany
-
-class ManyToManyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManySource(models.Model):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
-
-
class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail')
@@ -41,17 +32,6 @@ class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
model = ManyToManySource
-# ForeignKey
-
-class ForeignKeyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
-
-
class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail')
@@ -65,16 +45,17 @@ class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
# Nullable ForeignKey
+class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = NullableForeignKeySource
-class NullableForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
+# OneToOne
+class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
+ nullable_source = serializers.HyperlinkedRelatedField(view_name='nullableonetoonesource-detail')
-class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
- model = NullableForeignKeySource
+ model = OneToOneTarget
# TODO: Add test that .data cannot be accessed prior to .is_valid
@@ -236,6 +217,13 @@ class HyperlinkedForeignKeyTests(TestCase):
]
self.assertEquals(serializer.data, expected)
+ def test_foreign_key_update_incorrect_type(self):
+ data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': 2}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected url string, received int.']})
+
def test_reverse_foreign_key_update(self):
data = {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
instance = ForeignKeyTarget.objects.get(pk=2)
@@ -248,7 +236,7 @@ class HyperlinkedForeignKeyTests(TestCase):
expected = [
{'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
{'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
- ]
+ ]
self.assertEquals(new_serializer.data, expected)
serializer.save()
@@ -434,3 +422,24 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
# {'id': 2, 'name': 'target-2', 'sources': []},
# ]
# self.assertEquals(serializer.data, expected)
+
+
+class HyperlinkedNullableOneToOneTests(TestCase):
+ urls = 'rest_framework.tests.relations_hyperlink'
+
+ def setUp(self):
+ target = OneToOneTarget(name='target-1')
+ target.save()
+ new_target = OneToOneTarget(name='target-2')
+ new_target.save()
+ source = NullableOneToOneSource(name='source-1', target=target)
+ source.save()
+
+ def test_reverse_foreign_key_retrieve_with_null(self):
+ queryset = OneToOneTarget.objects.all()
+ serializer = NullableOneToOneTargetSerializer(queryset)
+ expected = [
+ {'url': '/onetoonetarget/1/', 'name': u'target-1', 'nullable_source': '/nullableonetoonesource/1/'},
+ {'url': '/onetoonetarget/2/', 'name': u'target-2', 'nullable_source': None},
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py
index 442cbebe..e81f0e42 100644
--- a/rest_framework/tests/relations_nested.py
+++ b/rest_framework/tests/relations_nested.py
@@ -1,19 +1,7 @@
from __future__ import unicode_literals
-
-from django.db import models
from django.test import TestCase
from rest_framework import serializers
-
-
-# ForeignKey
-
-class ForeignKeyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
class ForeignKeySourceSerializer(serializers.ModelSerializer):
@@ -34,20 +22,24 @@ class ForeignKeyTargetSerializer(serializers.ModelSerializer):
model = ForeignKeyTarget
-# Nullable ForeignKey
-
-class NullableForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
-
-
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
depth = 1
model = NullableForeignKeySource
+class NullableOneToOneSourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullableOneToOneSource
+
+
+class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
+ nullable_source = NullableOneToOneSourceSerializer()
+
+ class Meta:
+ model = OneToOneTarget
+
+
class ReverseForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
@@ -102,3 +94,22 @@ class NestedNullableForeignKeyTests(TestCase):
{'id': 3, 'name': 'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)
+
+
+class NestedNullableOneToOneTests(TestCase):
+ def setUp(self):
+ target = OneToOneTarget(name='target-1')
+ target.save()
+ new_target = OneToOneTarget(name='target-2')
+ new_target.save()
+ source = NullableOneToOneSource(name='source-1', target=target)
+ source.save()
+
+ def test_reverse_foreign_key_retrieve_with_null(self):
+ queryset = OneToOneTarget.objects.all()
+ serializer = NullableOneToOneTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'nullable_source': {'id': 1, 'name': u'source-1', 'target': 1}},
+ {'id': 2, 'name': u'target-2', 'nullable_source': None},
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py
index a04c5c80..4d00795a 100644
--- a/rest_framework/tests/relations_pk.py
+++ b/rest_framework/tests/relations_pk.py
@@ -3,17 +3,7 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
-
-
-# ManyToMany
-
-class ManyToManyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManySource(models.Model):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
class ManyToManyTargetSerializer(serializers.ModelSerializer):
@@ -28,17 +18,6 @@ class ManyToManySourceSerializer(serializers.ModelSerializer):
model = ManyToManySource
-# ForeignKey
-
-class ForeignKeyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
-
-
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
sources = serializers.ManyPrimaryKeyRelatedField()
@@ -51,17 +30,17 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer):
model = ForeignKeySource
-# Nullable ForeignKey
+class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullableForeignKeySource
-class NullableForeignKeySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
+# OneToOne
+class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
+ nullable_source = serializers.PrimaryKeyRelatedField()
-class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
- model = NullableForeignKeySource
+ model = OneToOneTarget
# TODO: Add test that .data cannot be accessed prior to .is_valid
@@ -218,6 +197,13 @@ class PKForeignKeyTests(TestCase):
]
self.assertEquals(serializer.data, expected)
+ def test_foreign_key_update_incorrect_type(self):
+ data = {'id': 1, 'name': u'source-1', 'target': 'foo'}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected pk value, received str.']})
+
def test_reverse_foreign_key_update(self):
data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
instance = ForeignKeyTarget.objects.get(pk=2)
@@ -230,7 +216,7 @@ class PKForeignKeyTests(TestCase):
expected = [
{'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': 'target-2', 'sources': []},
- ]
+ ]
self.assertEquals(new_serializer.data, expected)
serializer.save()
@@ -414,3 +400,22 @@ class PKNullableForeignKeyTests(TestCase):
# {'id': 2, 'name': 'target-2', 'sources': []},
# ]
# self.assertEquals(serializer.data, expected)
+
+
+class PKNullableOneToOneTests(TestCase):
+ def setUp(self):
+ target = OneToOneTarget(name='target-1')
+ target.save()
+ new_target = OneToOneTarget(name='target-2')
+ new_target.save()
+ source = NullableOneToOneSource(name='source-1', target=target)
+ source.save()
+
+ def test_reverse_foreign_key_retrieve_with_null(self):
+ queryset = OneToOneTarget.objects.all()
+ serializer = NullableOneToOneTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'nullable_source': 1},
+ {'id': 2, 'name': u'target-2', 'nullable_source': None},
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py
new file mode 100644
index 00000000..37ccc75e
--- /dev/null
+++ b/rest_framework/tests/relations_slug.py
@@ -0,0 +1,257 @@
+from django.test import TestCase
+from rest_framework import serializers
+from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
+
+
+class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+ sources = serializers.ManySlugRelatedField(slug_field='name')
+
+ class Meta:
+ model = ForeignKeyTarget
+
+
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+ target = serializers.SlugRelatedField(slug_field='name')
+
+ class Meta:
+ model = ForeignKeySource
+
+
+class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+ target = serializers.SlugRelatedField(slug_field='name', null=True)
+
+ class Meta:
+ model = NullableForeignKeySource
+
+
+# TODO: M2M Tests, FKTests (Non-nulable), One2One
+class PKForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-1'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': 'target-1'}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve(self):
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update(self):
+ data = {'id': 1, 'name': u'source-1', 'target': 'target-2'}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-2'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': 'target-1'}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_incorrect_type(self):
+ data = {'id': 1, 'name': u'source-1', 'target': 123}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Object with name=123 does not exist.']})
+
+ def test_reverse_foreign_key_update(self):
+ data = {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']}
+ instance = ForeignKeyTarget.objects.get(pk=2)
+ serializer = ForeignKeyTargetSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ # We shouldn't have saved anything to the db yet since save
+ # hasn't been called.
+ queryset = ForeignKeyTarget.objects.all()
+ new_serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(new_serializer.data, expected)
+
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure target 2 is update, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': ['source-2']},
+ {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create(self):
+ data = {'id': 4, 'name': u'source-4', 'target': 'target-2'}
+ serializer = ForeignKeySourceSerializer(data=data)
+ serializer.is_valid()
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is added, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-1'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': 'target-1'},
+ {'id': 4, 'name': u'source-4', 'target': 'target-2'},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_create(self):
+ data = {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']}
+ serializer = ForeignKeyTargetSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'target-3')
+
+ # Ensure target 3 is added, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': ['source-2']},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_invalid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ 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']})
+
+
+class SlugNullableForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ if idx == 3:
+ target = None
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve_with_null(self):
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-1'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_null(self):
+ data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-1'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'id': 4, 'name': u'source-4', 'target': ''}
+ expected_data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, expected_data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 'target-1'},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'id': 1, 'name': u'source-1', 'target': ''}
+ expected_data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, expected_data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 'target-1'},
+ {'id': 3, 'name': u'source-3', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py
index 7d4575bb..92b1bfd8 100644
--- a/rest_framework/tests/request.py
+++ b/rest_framework/tests/request.py
@@ -1,12 +1,12 @@
"""
Tests for content parsing, and form-overloaded content parsing.
"""
+import json
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, Client
from django.test.client import RequestFactory
-from django.utils import simplejson as json
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.compat import patterns
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 6ce7de31..a00626b5 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -56,6 +56,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')
@@ -71,6 +84,7 @@ class AlbumsSerializer(serializers.ModelSerializer):
model = Album
fields = ['title'] # lists are also valid options
+
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = HasPositiveIntegerAsChoice
@@ -163,7 +177,6 @@ class BasicTests(TestCase):
"""
Attempting to update fields set as read_only should have no effect.
"""
-
serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
self.assertEquals(serializer.is_valid(), True)
instance = serializer.save()
@@ -184,8 +197,7 @@ class ValidationTests(TestCase):
'content': 'x' * 1001,
'created': datetime.datetime(2012, 1, 1)
}
- self.actionitem = ActionItem(title='Some to do item',
- )
+ self.actionitem = ActionItem(title='Some to do item',)
def test_create(self):
serializer = CommentSerializer(data=self.data)
@@ -217,30 +229,24 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.errors, {})
- def test_field_validation(self):
-
- class CommentSerializerWithFieldValidator(CommentSerializer):
-
- def validate_content(self, attrs, source):
- value = attrs[source]
- if "test" not in value:
- raise serializers.ValidationError("Test not in value")
- return attrs
-
- data = {
- 'email': 'tom@example.com',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = CommentSerializerWithFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
+ def test_bad_type_data_is_false(self):
+ """
+ Data of the wrong type is not valid.
+ """
+ data = ['i am', 'a', 'list']
+ serializer = CommentSerializer(self.comment, data=data)
+ self.assertEquals(serializer.is_valid(), False)
+ self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
- data['content'] = 'This should not validate'
+ data = 'and i am a string'
+ serializer = CommentSerializer(self.comment, data=data)
+ self.assertEquals(serializer.is_valid(), False)
+ self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
- serializer = CommentSerializerWithFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEquals(serializer.errors, {'content': ['Test not in value']})
+ data = 42
+ serializer = CommentSerializer(self.comment, data=data)
+ self.assertEquals(serializer.is_valid(), False)
+ self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
def test_cross_field_validation(self):
@@ -282,6 +288,20 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'title': ['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...',
@@ -292,12 +312,69 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
+class CustomValidationTests(TestCase):
+ class CommentSerializerWithFieldValidator(CommentSerializer):
+
+ def validate_email(self, attrs, source):
+ value = attrs[source]
+
+ return attrs
+
+ def validate_content(self, attrs, source):
+ value = attrs[source]
+ if "test" not in value:
+ raise serializers.ValidationError("Test not in value")
+ return attrs
+
+ def test_field_validation(self):
+ data = {
+ 'email': 'tom@example.com',
+ 'content': 'A test comment',
+ 'created': datetime.datetime(2012, 1, 1)
+ }
+
+ serializer = self.CommentSerializerWithFieldValidator(data=data)
+ self.assertTrue(serializer.is_valid())
+
+ data['content'] = 'This should not validate'
+
+ serializer = self.CommentSerializerWithFieldValidator(data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'content': [u'Test not in value']})
+
+ def test_missing_data(self):
+ """
+ Make sure that validate_content isn't called if the field is missing
+ """
+ incomplete_data = {
+ 'email': 'tom@example.com',
+ 'created': datetime.datetime(2012, 1, 1)
+ }
+ serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'content': [u'This field is required.']})
+
+ def test_wrong_data(self):
+ """
+ Make sure that validate_content isn't called if the field input is wrong
+ """
+ wrong_data = {
+ 'email': 'not an email',
+ 'content': 'A test comment',
+ 'created': datetime.datetime(2012, 1, 1)
+ }
+ serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'email': [u'Enter a valid e-mail address.']})
+
+
class PositiveIntegerAsChoiceTests(TestCase):
def test_positive_integer_in_json_is_correctly_parsed(self):
- data = {'some_integer':1}
+ data = {'some_integer': 1}
serializer = PositiveIntegerAsChoiceSerializer(data=data)
self.assertEquals(serializer.is_valid(), True)
+
class ModelValidationTests(TestCase):
def test_validate_unique(self):
"""
@@ -342,7 +419,6 @@ class ModelValidationTests(TestCase):
self.assertTrue(photo_serializer.save())
-
class RegexValidationTest(TestCase):
def test_create_failed(self):
serializer = BookSerializer(data={'isbn': '1234567890'})
@@ -553,6 +629,21 @@ class DefaultValueTests(TestCase):
self.assertEquals(instance.pk, 1)
self.assertEquals(instance.text, 'overridden')
+ def test_partial_update_default(self):
+ """ Regression test for issue #532 """
+ data = {'text': 'overridden'}
+ serializer = self.serializer_class(data=data, partial=True)
+ self.assertEquals(serializer.is_valid(), True)
+ instance = serializer.save()
+
+ data = {'extra': 'extra_value'}
+ serializer = self.serializer_class(instance=instance, data=data, partial=True)
+ self.assertEquals(serializer.is_valid(), True)
+ instance = serializer.save()
+
+ self.assertEquals(instance.extra, 'extra_value')
+ self.assertEquals(instance.text, 'overridden')
+
class CallableDefaultValueTests(TestCase):
def setUp(self):
diff --git a/rest_framework/tests/settings.py b/rest_framework/tests/settings.py
new file mode 100644
index 00000000..0293fdc3
--- /dev/null
+++ b/rest_framework/tests/settings.py
@@ -0,0 +1,21 @@
+"""Tests for the settings module"""
+from django.test import TestCase
+
+from rest_framework.settings import APISettings, DEFAULTS, IMPORT_STRINGS
+
+
+class TestSettings(TestCase):
+ """Tests relating to the api settings"""
+
+ def test_non_import_errors(self):
+ """Make sure other errors aren't suppressed."""
+ settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
+ with self.assertRaises(ValueError):
+ settings.DEFAULT_MODEL_SERIALIZER_CLASS
+
+ def test_import_error_message_maintained(self):
+ """Make sure real import errors are captured and raised sensibly."""
+ settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
+ with self.assertRaises(ImportError) as cm:
+ settings.DEFAULT_MODEL_SERIALIZER_CLASS
+ self.assertTrue('ImportError' in str(cm.exception))
diff --git a/rest_framework/tests/urlpatterns.py b/rest_framework/tests/urlpatterns.py
new file mode 100644
index 00000000..43e8ef69
--- /dev/null
+++ b/rest_framework/tests/urlpatterns.py
@@ -0,0 +1,78 @@
+from collections import namedtuple
+
+from django.core import urlresolvers
+
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+from rest_framework.compat import patterns, url, include
+from rest_framework.urlpatterns import format_suffix_patterns
+
+
+# A container class for test paths for the test case
+URLTestPath = namedtuple('URLTestPath', ['path', 'args', 'kwargs'])
+
+
+def dummy_view(request, *args, **kwargs):
+ pass
+
+
+class FormatSuffixTests(TestCase):
+ """
+ Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters.
+ """
+ def _resolve_urlpatterns(self, urlpatterns, test_paths):
+ factory = RequestFactory()
+ try:
+ urlpatterns = format_suffix_patterns(urlpatterns)
+ except:
+ self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns")
+ resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns)
+ for test_path in test_paths:
+ request = factory.get(test_path.path)
+ try:
+ callback, callback_args, callback_kwargs = resolver.resolve(request.path_info)
+ except:
+ self.fail("Failed to resolve URL: %s" % request.path_info)
+ self.assertEquals(callback_args, test_path.args)
+ self.assertEquals(callback_kwargs, test_path.kwargs)
+
+ def test_format_suffix(self):
+ urlpatterns = patterns(
+ '',
+ url(r'^test$', dummy_view),
+ )
+ test_paths = [
+ URLTestPath('/test', (), {}),
+ URLTestPath('/test.api', (), {'format': 'api'}),
+ URLTestPath('/test.asdf', (), {'format': 'asdf'}),
+ ]
+ self._resolve_urlpatterns(urlpatterns, test_paths)
+
+ def test_default_args(self):
+ urlpatterns = patterns(
+ '',
+ url(r'^test$', dummy_view, {'foo': 'bar'}),
+ )
+ test_paths = [
+ URLTestPath('/test', (), {'foo': 'bar', }),
+ URLTestPath('/test.api', (), {'foo': 'bar', 'format': 'api'}),
+ URLTestPath('/test.asdf', (), {'foo': 'bar', 'format': 'asdf'}),
+ ]
+ self._resolve_urlpatterns(urlpatterns, test_paths)
+
+ def test_included_urls(self):
+ nested_patterns = patterns(
+ '',
+ url(r'^path$', dummy_view)
+ )
+ urlpatterns = patterns(
+ '',
+ url(r'^test/', include(nested_patterns), {'foo': 'bar'}),
+ )
+ test_paths = [
+ URLTestPath('/test/path', (), {'foo': 'bar', }),
+ URLTestPath('/test/path.api', (), {'foo': 'bar', 'format': 'api'}),
+ URLTestPath('/test/path.asdf', (), {'foo': 'bar', 'format': 'asdf'}),
+ ]
+ self._resolve_urlpatterns(urlpatterns, test_paths)
diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py
new file mode 100644
index 00000000..3906adb9
--- /dev/null
+++ b/rest_framework/tests/utils.py
@@ -0,0 +1,27 @@
+from django.test.client import RequestFactory, FakePayload
+from django.test.client import MULTIPART_CONTENT
+from urlparse import urlparse
+
+
+class RequestFactory(RequestFactory):
+
+ def __init__(self, **defaults):
+ super(RequestFactory, self).__init__(**defaults)
+
+ def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
+ **extra):
+ "Construct a PATCH request."
+
+ patch_data = self._encode_data(data, content_type)
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_LENGTH': len(patch_data),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': parsed[4],
+ 'REQUEST_METHOD': 'PATCH',
+ 'wsgi.input': FakePayload(patch_data),
+ }
+ r.update(extra)
+ return self.request(**r)
diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py
index e51ca9f3..f2432516 100644
--- a/rest_framework/tests/views.py
+++ b/rest_framework/tests/views.py
@@ -20,7 +20,7 @@ class BasicView(APIView):
return Response({'method': 'POST', 'data': request.DATA})
-@api_view(['GET', 'POST', 'PUT'])
+@api_view(['GET', 'POST', 'PUT', 'PATCH'])
def basic_view(request):
if request.method == 'GET':
return {'method': 'GET'}
@@ -28,6 +28,8 @@ def basic_view(request):
return {'method': 'POST', 'data': request.DATA}
elif request.method == 'PUT':
return {'method': 'PUT', 'data': request.DATA}
+ elif request.method == 'PATCH':
+ return {'method': 'PATCH', 'data': request.DATA}
def sanitise_json_error(error_dict):
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index 143928c9..47789026 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,5 +1,35 @@
-from rest_framework.compat import url
+from rest_framework.compat import url, include
from rest_framework.settings import api_settings
+from django.core.urlresolvers import RegexURLResolver
+
+
+def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
+ ret = []
+ for urlpattern in urlpatterns:
+ if isinstance(urlpattern, RegexURLResolver):
+ # Set of included URL patterns
+ regex = urlpattern.regex.pattern
+ namespace = urlpattern.namespace
+ app_name = urlpattern.app_name
+ kwargs = urlpattern.default_kwargs
+ # Add in the included patterns, after applying the suffixes
+ patterns = apply_suffix_patterns(urlpattern.url_patterns,
+ suffix_pattern,
+ suffix_required)
+ ret.append(url(regex, include(patterns, namespace, app_name), kwargs))
+
+ else:
+ # Regular URL pattern
+ regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern
+ view = urlpattern._callback or urlpattern._callback_str
+ kwargs = urlpattern.default_args
+ name = urlpattern.name
+ # Add in both the existing and the new urlpattern
+ if not suffix_required:
+ ret.append(urlpattern)
+ ret.append(url(regex, view, kwargs, name))
+
+ return ret
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
@@ -28,15 +58,4 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
else:
suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
- ret = []
- for urlpattern in urlpatterns:
- # Form our complementing '.format' urlpattern
- regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern
- view = urlpattern._callback or urlpattern._callback_str
- kwargs = urlpattern.default_args
- name = urlpattern.name
- # Add in both the existing and the new urlpattern
- if not suffix_required:
- ret.append(urlpattern)
- ret.append(url(regex, view, kwargs, name))
- return ret
+ return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 2d1fb353..7afe100a 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -4,7 +4,7 @@ Helper classes for parsers.
import datetime
import decimal
import types
-from django.utils import simplejson as json
+import json
from django.utils.datastructures import SortedDict
from rest_framework.compat import timezone
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
@@ -12,7 +12,7 @@ from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
class JSONEncoder(json.JSONEncoder):
"""
- JSONEncoder subclass that knows how to encode date/time,
+ JSONEncoder subclass that knows how to encode date/time/timedelta,
decimal types, and generators.
"""
def default(self, o):
@@ -34,6 +34,8 @@ class JSONEncoder(json.JSONEncoder):
if o.microsecond:
r = r[:12]
return r
+ elif isinstance(o, datetime.timedelta):
+ return str(o.total_seconds())
elif isinstance(o, decimal.Decimal):
return str(o)
elif hasattr(o, '__iter__'):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 10bdd5a5..ac9b3385 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -148,6 +148,8 @@ class APIView(View):
"""
If request is not permitted, determine what kind of exception to raise.
"""
+ if not self.request.successful_authenticator:
+ raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied()
def throttled(self, request, wait):
@@ -156,6 +158,15 @@ class APIView(View):
"""
raise exceptions.Throttled(wait)
+ def get_authenticate_header(self, request):
+ """
+ If a request is unauthenticated, determine the WWW-Authenticate
+ header to use for 401 responses, if any.
+ """
+ authenticators = self.get_authenticators()
+ if authenticators:
+ return authenticators[0].authenticate_header(request)
+
def get_parser_context(self, http_request):
"""
Returns a dict that is passed through to Parser.parse(),
@@ -319,6 +330,16 @@ class APIView(View):
# Throttle wait header
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
+ if isinstance(exc, (exceptions.NotAuthenticated,
+ exceptions.AuthenticationFailed)):
+ # WWW-Authenticate header for 401 responses, else coerce to 403
+ auth_header = self.get_authenticate_header(self.request)
+
+ if auth_header:
+ self.headers['WWW-Authenticate'] = auth_header
+ else:
+ exc.status_code = status.HTTP_403_FORBIDDEN
+
if isinstance(exc, exceptions.APIException):
return Response({'detail': exc.detail},
status=exc.status_code,