mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Merge remote-tracking branch 'tom/master'
Conflicts: rest_framework/serializers.py rest_framework/tests/serializer.py
This commit is contained in:
commit
8ac77eaae8
|
@ -6,8 +6,8 @@ python:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO=https://github.com/django/django/zipball/master
|
- DJANGO=https://github.com/django/django/zipball/master
|
||||||
- DJANGO=django==1.4.1 --use-mirrors
|
- DJANGO=django==1.4.3 --use-mirrors
|
||||||
- DJANGO=django==1.3.3 --use-mirrors
|
- DJANGO=django==1.3.5 --use-mirrors
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
|
|
34
README.md
34
README.md
|
@ -58,6 +58,40 @@ To run the tests.
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
**Date**: 11th Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix broken nested serialization.
|
||||||
|
* 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
|
||||||
|
|
||||||
|
**Date**: 8th Dec 2012
|
||||||
|
|
||||||
|
* Fix for creating nullable Foreign Keys with `''` as well as `None`.
|
||||||
|
* Added `null=<bool>` related field option.
|
||||||
|
|
||||||
|
## 2.1.7
|
||||||
|
|
||||||
|
**Date**: 7th Dec 2012
|
||||||
|
|
||||||
|
* Serializers now properly support nullable Foreign Keys.
|
||||||
|
* Serializer validation now includes model field validation, such as uniqueness constraints.
|
||||||
|
* Support 'true' and 'false' string values for BooleanField.
|
||||||
|
* Added pickle support for serialized data.
|
||||||
|
* Support `source='dotted.notation'` style for nested serializers.
|
||||||
|
* Make `Request.user` settable.
|
||||||
|
* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`
|
||||||
|
|
||||||
## 2.1.6
|
## 2.1.6
|
||||||
|
|
||||||
**Date**: 23rd Nov 2012
|
**Date**: 23rd Nov 2012
|
||||||
|
|
|
@ -293,6 +293,7 @@ By default these fields are read-write, although you can change this behaviour u
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||||
|
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||||
|
|
||||||
## SlugRelatedField / ManySlugRelatedField
|
## SlugRelatedField / ManySlugRelatedField
|
||||||
|
|
||||||
|
@ -304,6 +305,7 @@ By default these fields read-write, although you can change this behaviour using
|
||||||
|
|
||||||
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
|
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
|
||||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||||
|
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||||
|
|
||||||
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
|
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
|
||||||
|
|
||||||
|
@ -319,6 +321,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi
|
||||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||||
|
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||||
|
|
||||||
## HyperLinkedIdentityField
|
## HyperLinkedIdentityField
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
|
||||||
Or, if you're using the `@api_view` decorator with function based views.
|
Or, if you're using the `@api_view` decorator with function based views.
|
||||||
|
|
||||||
@api_view('GET')
|
@api_view('GET')
|
||||||
@permission_classes(IsAuthenticated)
|
@permission_classes((IsAuthenticated, ))
|
||||||
def example_view(request, format=None):
|
def example_view(request, format=None):
|
||||||
content = {
|
content = {
|
||||||
'status': 'request was permitted'
|
'status': 'request was permitted'
|
||||||
|
|
|
@ -71,6 +71,14 @@ The following people have helped make REST framework great.
|
||||||
* Fabian Buechler - [fabianbuechler]
|
* Fabian Buechler - [fabianbuechler]
|
||||||
* Mark Hughes - [mhsparks]
|
* Mark Hughes - [mhsparks]
|
||||||
* Michael van de Waeter - [mvdwaeter]
|
* Michael van de Waeter - [mvdwaeter]
|
||||||
|
* Reinout van Rees - [reinout]
|
||||||
|
* Michael Richards - [justanotherbody]
|
||||||
|
* Ben Roberts - [roberts81]
|
||||||
|
* Venkata Subramanian Mahalingam - [annacoder]
|
||||||
|
* George Kappel - [gkappel]
|
||||||
|
* Colin Murtaugh - [cmurtaugh]
|
||||||
|
* Simon Pantzare - [pilt]
|
||||||
|
* Szymon Teżewski - [sunscrapers]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -177,3 +185,11 @@ To contact the author directly:
|
||||||
[fabianbuechler]: https://github.com/fabianbuechler
|
[fabianbuechler]: https://github.com/fabianbuechler
|
||||||
[mhsparks]: https://github.com/mhsparks
|
[mhsparks]: https://github.com/mhsparks
|
||||||
[mvdwaeter]: https://github.com/mvdwaeter
|
[mvdwaeter]: https://github.com/mvdwaeter
|
||||||
|
[reinout]: https://github.com/reinout
|
||||||
|
[justanotherbody]: https://github.com/justanotherbody
|
||||||
|
[roberts81]: https://github.com/roberts81
|
||||||
|
[annacoder]: https://github.com/annacoder
|
||||||
|
[gkappel]: https://github.com/gkappel
|
||||||
|
[cmurtaugh]: https://github.com/cmurtaugh
|
||||||
|
[pilt]: https://github.com/pilt
|
||||||
|
[sunscrapers]: https://github.com/sunscrapers
|
||||||
|
|
|
@ -4,19 +4,55 @@
|
||||||
>
|
>
|
||||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||||
|
|
||||||
## 2.1.6
|
## 2.1.x series
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
**Date**: 11th Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix broken nested serialization.
|
||||||
|
* 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
|
||||||
|
|
||||||
|
**Date**: 8th Dec 2012
|
||||||
|
|
||||||
|
* Fix for creating nullable Foreign Keys with `''` as well as `None`.
|
||||||
|
* Added `null=<bool>` related field option.
|
||||||
|
|
||||||
|
### 2.1.7
|
||||||
|
|
||||||
|
**Date**: 7th Dec 2012
|
||||||
|
|
||||||
|
* Serializers now properly support nullable Foreign Keys.
|
||||||
|
* Serializer validation now includes model field validation, such as uniqueness constraints.
|
||||||
|
* Support 'true' and 'false' string values for BooleanField.
|
||||||
|
* Added pickle support for serialized data.
|
||||||
|
* Support `source='dotted.notation'` style for nested serializers.
|
||||||
|
* Make `Request.user` settable.
|
||||||
|
* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`.
|
||||||
|
|
||||||
|
### 2.1.6
|
||||||
|
|
||||||
**Date**: 23rd Nov 2012
|
**Date**: 23rd Nov 2012
|
||||||
|
|
||||||
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
|
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
|
||||||
|
|
||||||
## 2.1.5
|
### 2.1.5
|
||||||
|
|
||||||
**Date**: 23rd Nov 2012
|
**Date**: 23rd Nov 2012
|
||||||
|
|
||||||
* Bugfix: Fix DjangoModelPermissions.
|
* Bugfix: Fix DjangoModelPermissions.
|
||||||
|
|
||||||
## 2.1.4
|
### 2.1.4
|
||||||
|
|
||||||
**Date**: 22nd Nov 2012
|
**Date**: 22nd Nov 2012
|
||||||
|
|
||||||
|
@ -27,7 +63,7 @@
|
||||||
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
|
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
|
||||||
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
|
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
|
||||||
|
|
||||||
## 2.1.3
|
### 2.1.3
|
||||||
|
|
||||||
**Date**: 16th Nov 2012
|
**Date**: 16th Nov 2012
|
||||||
|
|
||||||
|
@ -38,14 +74,14 @@
|
||||||
* 201 Responses now return a 'Location' header.
|
* 201 Responses now return a 'Location' header.
|
||||||
* Bugfix: Serializer fields now respect `max_length`.
|
* Bugfix: Serializer fields now respect `max_length`.
|
||||||
|
|
||||||
## 2.1.2
|
### 2.1.2
|
||||||
|
|
||||||
**Date**: 9th Nov 2012
|
**Date**: 9th Nov 2012
|
||||||
|
|
||||||
* **Filtering support.**
|
* **Filtering support.**
|
||||||
* Bugfix: Support creation of objects with reverse M2M relations.
|
* Bugfix: Support creation of objects with reverse M2M relations.
|
||||||
|
|
||||||
## 2.1.1
|
### 2.1.1
|
||||||
|
|
||||||
**Date**: 7th Nov 2012
|
**Date**: 7th Nov 2012
|
||||||
|
|
||||||
|
@ -55,7 +91,7 @@
|
||||||
* Bugfix: Make textareas same width as other fields in browsable API.
|
* 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.
|
* 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
|
**Date**: 5th Nov 2012
|
||||||
|
|
||||||
|
@ -69,13 +105,17 @@
|
||||||
* Bugfix: Support choice field in Browseable API.
|
* Bugfix: Support choice field in Browseable API.
|
||||||
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
|
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
|
||||||
|
|
||||||
## 2.0.2
|
---
|
||||||
|
|
||||||
|
## 2.0.x series
|
||||||
|
|
||||||
|
### 2.0.2
|
||||||
|
|
||||||
**Date**: 2nd Nov 2012
|
**Date**: 2nd Nov 2012
|
||||||
|
|
||||||
* Fix issues with pk related fields in the browsable API.
|
* Fix issues with pk related fields in the browsable API.
|
||||||
|
|
||||||
## 2.0.1
|
### 2.0.1
|
||||||
|
|
||||||
**Date**: 1st Nov 2012
|
**Date**: 1st Nov 2012
|
||||||
|
|
||||||
|
@ -83,7 +123,7 @@
|
||||||
* Added SlugRelatedField and ManySlugRelatedField.
|
* Added SlugRelatedField and ManySlugRelatedField.
|
||||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||||
|
|
||||||
## 2.0.0
|
### 2.0.0
|
||||||
|
|
||||||
**Date**: 30th Oct 2012
|
**Date**: 30th Oct 2012
|
||||||
|
|
||||||
|
@ -92,7 +132,9 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.x series
|
||||||
|
|
||||||
|
### 0.4.0
|
||||||
|
|
||||||
* Supports Django 1.5.
|
* Supports Django 1.5.
|
||||||
* Fixes issues with 'HEAD' method.
|
* Fixes issues with 'HEAD' method.
|
||||||
|
@ -104,7 +146,11 @@
|
||||||
* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
|
* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
|
||||||
* Sensible absolute URL generation, not using hacky set_script_prefix
|
* Sensible absolute URL generation, not using hacky set_script_prefix
|
||||||
|
|
||||||
## 0.3.3
|
---
|
||||||
|
|
||||||
|
## 0.3.x series
|
||||||
|
|
||||||
|
### 0.3.3
|
||||||
|
|
||||||
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
|
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
|
||||||
* Use `staticfiles` for css files.
|
* Use `staticfiles` for css files.
|
||||||
|
@ -119,7 +165,7 @@
|
||||||
* Bugfixes:
|
* Bugfixes:
|
||||||
- Bug with PerUserThrottling when user contains unicode chars.
|
- Bug with PerUserThrottling when user contains unicode chars.
|
||||||
|
|
||||||
## 0.3.2
|
### 0.3.2
|
||||||
|
|
||||||
* Bugfixes:
|
* Bugfixes:
|
||||||
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
|
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
|
||||||
|
@ -131,37 +177,41 @@
|
||||||
* get_name, get_description become methods on the view - makes them overridable.
|
* get_name, get_description become methods on the view - makes them overridable.
|
||||||
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
|
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
|
||||||
|
|
||||||
## 0.3.1
|
### 0.3.1
|
||||||
|
|
||||||
* [not documented]
|
* [not documented]
|
||||||
|
|
||||||
## 0.3.0
|
### 0.3.0
|
||||||
|
|
||||||
* JSONP Support
|
* JSONP Support
|
||||||
* Bugfixes, including support for latest markdown release
|
* Bugfixes, including support for latest markdown release
|
||||||
|
|
||||||
## 0.2.4
|
---
|
||||||
|
|
||||||
|
## 0.2.x series
|
||||||
|
|
||||||
|
### 0.2.4
|
||||||
|
|
||||||
* Fix broken IsAdminUser permission.
|
* Fix broken IsAdminUser permission.
|
||||||
* OPTIONS support.
|
* OPTIONS support.
|
||||||
* XMLParser.
|
* XMLParser.
|
||||||
* Drop mentions of Blog, BitBucket.
|
* Drop mentions of Blog, BitBucket.
|
||||||
|
|
||||||
## 0.2.3
|
### 0.2.3
|
||||||
|
|
||||||
* Fix some throttling bugs.
|
* Fix some throttling bugs.
|
||||||
* ``X-Throttle`` header on throttling.
|
* ``X-Throttle`` header on throttling.
|
||||||
* Support for nesting resources on related models.
|
* Support for nesting resources on related models.
|
||||||
|
|
||||||
## 0.2.2
|
### 0.2.2
|
||||||
|
|
||||||
* Throttling support complete.
|
* Throttling support complete.
|
||||||
|
|
||||||
## 0.2.1
|
### 0.2.1
|
||||||
|
|
||||||
* Couple of simple bugfixes over 0.2.0
|
* Couple of simple bugfixes over 0.2.0
|
||||||
|
|
||||||
## 0.2.0
|
### 0.2.0
|
||||||
|
|
||||||
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
|
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
|
||||||
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
|
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
|
||||||
|
@ -185,11 +235,15 @@
|
||||||
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
|
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
|
||||||
You can reuse these mixin classes individually without using the ``View`` class.
|
You can reuse these mixin classes individually without using the ``View`` class.
|
||||||
|
|
||||||
## 0.1.1
|
---
|
||||||
|
|
||||||
|
## 0.1.x series
|
||||||
|
|
||||||
|
### 0.1.1
|
||||||
|
|
||||||
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
|
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
|
||||||
|
|
||||||
## 0.1.0
|
### 0.1.0
|
||||||
|
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components
|
||||||
|
|
||||||
## What REST framework doesn't provide.
|
## What REST framework doesn't provide.
|
||||||
|
|
||||||
What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
|
What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
|
||||||
|
|
||||||
[cite]: http://vimeo.com/channels/restfest/page:2
|
[cite]: http://vimeo.com/channels/restfest/page:2
|
||||||
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
||||||
|
@ -44,6 +44,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format
|
||||||
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
|
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
|
||||||
[maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html
|
[maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html
|
||||||
|
|
||||||
|
[hal]: http://stateless.co/hal_specification.html
|
||||||
[collection]: http://www.amundsen.com/media-types/collection/
|
[collection]: http://www.amundsen.com/media-types/collection/
|
||||||
[microformats]: http://microformats.org/wiki/Main_Page
|
[microformats]: http://microformats.org/wiki/Main_Page
|
||||||
[serialization]: ../api-guide/serializers.md
|
[serialization]: ../api-guide/serializers.md
|
||||||
|
|
|
@ -14,7 +14,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
||||||
|
|
||||||
## Setting up a new environment
|
## Setting up a new environment
|
||||||
|
|
||||||
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on.
|
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
|
||||||
|
|
||||||
:::bash
|
:::bash
|
||||||
mkdir ~/env
|
mkdir ~/env
|
||||||
|
@ -39,7 +39,6 @@ To get started, let's create a new project to work with.
|
||||||
cd tutorial
|
cd tutorial
|
||||||
|
|
||||||
Once that's done we can create an app that we'll use to create a simple Web API.
|
Once that's done we can create an app that we'll use to create a simple Web API.
|
||||||
We're going to create a project that
|
|
||||||
|
|
||||||
python manage.py startapp snippets
|
python manage.py startapp snippets
|
||||||
|
|
||||||
|
@ -64,7 +63,7 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
|
||||||
'snippets'
|
'snippets'
|
||||||
)
|
)
|
||||||
|
|
||||||
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views.
|
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^', include('snippets.urls')),
|
url(r'^', include('snippets.urls')),
|
||||||
|
@ -105,7 +104,7 @@ Don't forget to sync the database for the first time.
|
||||||
|
|
||||||
## Creating a Serializer class
|
## Creating a Serializer class
|
||||||
|
|
||||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||||
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -146,7 +145,7 @@ We can actually also save ourselves some time by using the `ModelSerializer` cla
|
||||||
|
|
||||||
## Working with Serializers
|
## Working with Serializers
|
||||||
|
|
||||||
Before we go any further we'll familiarise ourselves with using our new Serializer class. Let's drop into the Django shell.
|
Before we go any further we'll familiarize ourselves with using our new Serializer class. Let's drop into the Django shell.
|
||||||
|
|
||||||
python manage.py shell
|
python manage.py shell
|
||||||
|
|
||||||
|
@ -166,7 +165,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial
|
||||||
serializer.data
|
serializer.data
|
||||||
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
||||||
|
|
||||||
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
|
At this point we've translated the model instance into python native datatypes. To finalize the serialization process we render the data into `json`.
|
||||||
|
|
||||||
content = JSONRenderer().render(serializer.data)
|
content = JSONRenderer().render(serializer.data)
|
||||||
content
|
content
|
||||||
|
@ -292,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
|
||||||
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
|
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
|
||||||
)
|
)
|
||||||
|
|
||||||
It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
|
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
|
## Testing our first attempt at a Web API
|
||||||
|
|
||||||
|
@ -304,7 +303,7 @@ It's worth noting that there's a couple of edge cases we're not dealing with pro
|
||||||
|
|
||||||
We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views.
|
We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views.
|
||||||
|
|
||||||
Our API views don't do anything particularly special at the moment, beyond serve `json` responses, and there's some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
|
Our API views don't do anything particularly special at the moment, beyond serving `json` responses, and there are some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
|
||||||
|
|
||||||
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
||||||
|
|
||||||
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
|
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.
|
||||||
|
|
||||||
@api_view(['GET', 'PUT', 'DELETE'])
|
@api_view(['GET', 'PUT', 'DELETE'])
|
||||||
def snippet_detail(request, pk):
|
def snippet_detail(request, pk):
|
||||||
"""
|
"""
|
||||||
|
@ -92,7 +94,7 @@ Our instance view is an improvement over the previous example. It's a little mo
|
||||||
snippet.delete()
|
snippet.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
This should all feel very familiar - there's not a lot different to working with regular Django views.
|
This should all feel very familiar - it is not a lot different from working with regular Django views.
|
||||||
|
|
||||||
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
|
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
|
||||||
|
|
||||||
|
@ -128,7 +130,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
|
||||||
|
|
||||||
**TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
|
**TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
|
||||||
|
|
||||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]."
|
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
|
||||||
|
|
||||||
### Browsability
|
### Browsability
|
||||||
|
|
||||||
|
|
|
@ -102,14 +102,14 @@ Let's take a look at how we can compose our views by using the mixin classes.
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return self.create(request, *args, **kwargs)
|
return self.create(request, *args, **kwargs)
|
||||||
|
|
||||||
We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
|
We'll take a moment to examine exactly what's happening here. We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
|
||||||
|
|
||||||
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
|
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
|
||||||
|
|
||||||
class SnippetDetail(mixins.RetrieveModelMixin,
|
class SnippetDetail(mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
generics.SingleObjectBaseView):
|
generics.SingleObjectAPIView):
|
||||||
model = Snippet
|
model = Snippet
|
||||||
serializer_class = SnippetSerializer
|
serializer_class = SnippetSerializer
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ The base class provides the core functionality, and the mixin classes provide th
|
||||||
def delete(self, request, *args, **kwargs):
|
def delete(self, request, *args, **kwargs):
|
||||||
return self.destroy(request, *args, **kwargs)
|
return self.destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
Pretty similar. This time we're using the `SingleObjectBaseView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
|
Pretty similar. This time we're using the `SingleObjectAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
|
||||||
|
|
||||||
## Using generic class based views
|
## Using generic class based views
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ Using the mixin classes we've rewritten the views to use slightly less code than
|
||||||
model = Snippet
|
model = Snippet
|
||||||
serializer_class = SnippetSerializer
|
serializer_class = SnippetSerializer
|
||||||
|
|
||||||
Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django.
|
Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django.
|
||||||
|
|
||||||
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API.
|
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API.
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ Now that we've got some users to work with, we'd better add representations of t
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username', 'snippets')
|
fields = ('id', 'username', 'snippets')
|
||||||
|
|
||||||
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it.
|
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
|
||||||
|
|
||||||
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
|
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
|
||||||
|
|
||||||
|
@ -92,9 +92,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin
|
||||||
|
|
||||||
## Updating our serializer
|
## Updating our serializer
|
||||||
|
|
||||||
Now that snippets are associated with the user that created them, let's update our SnippetSerializer to reflect that.
|
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
|
||||||
|
|
||||||
Add the following field to the serializer definition:
|
|
||||||
|
|
||||||
owner = serializers.Field(source='owner.username')
|
owner = serializers.Field(source='owner.username')
|
||||||
|
|
||||||
|
@ -108,7 +106,7 @@ The field we've added is the untyped `Field` class, in contrast to the other typ
|
||||||
|
|
||||||
## Adding required permissions to views
|
## Adding required permissions to views
|
||||||
|
|
||||||
Now that code snippets are associated with users we want to make sure that only authenticated users are able to create, update and delete code snippets.
|
Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update and delete code snippets.
|
||||||
|
|
||||||
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.
|
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ Notice that we're using REST framework's `reverse` function in order to return f
|
||||||
|
|
||||||
The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.
|
The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.
|
||||||
|
|
||||||
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
|
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
|
||||||
|
|
||||||
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ We could also customize the pagination style if we needed too, but in this case
|
||||||
|
|
||||||
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
|
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
|
||||||
|
|
||||||
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the hightlighted code HTML representations.
|
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations.
|
||||||
|
|
||||||
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
|
||||||
'PAGINATE_BY': 10
|
'PAGINATE_BY': 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Okay, that's us done.
|
Okay, we're done.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '2.1.6'
|
__version__ = '2.1.10'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class AuthTokenSerializer(serializers.Serializer):
|
class AuthTokenSerializer(serializers.Serializer):
|
||||||
username = serializers.CharField()
|
username = serializers.CharField()
|
||||||
password = serializers.CharField()
|
password = serializers.CharField()
|
||||||
|
@ -21,4 +22,3 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError('Must include "username" and "password"')
|
raise serializers.ValidationError('Must include "username" and "password"')
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,12 @@ from rest_framework.response import Response
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
|
||||||
|
|
||||||
class ObtainAuthToken(APIView):
|
class ObtainAuthToken(APIView):
|
||||||
throttle_classes = ()
|
throttle_classes = ()
|
||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
||||||
renderer_classes = (renderers.JSONRenderer,)
|
renderer_classes = (renderers.JSONRenderer,)
|
||||||
model = Token
|
model = Token
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
@ -18,7 +19,7 @@ class ObtainAuthToken(APIView):
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
token, created = Token.objects.get_or_create(user=serializer.object['user'])
|
token, created = Token.objects.get_or_create(user=serializer.object['user'])
|
||||||
return Response({'token': token.key})
|
return Response({'token': token.key})
|
||||||
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
obtain_auth_token = ObtainAuthToken.as_view()
|
obtain_auth_token = ObtainAuthToken.as_view()
|
||||||
|
|
|
@ -19,6 +19,16 @@ except ImportError:
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
# Try to import PIL in either of the two ways it can end up installed.
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import Image
|
||||||
|
except ImportError:
|
||||||
|
Image = None
|
||||||
|
|
||||||
|
|
||||||
def get_concrete_model(model_cls):
|
def get_concrete_model(model_cls):
|
||||||
try:
|
try:
|
||||||
return model_cls._meta.concrete_model
|
return model_cls._meta.concrete_model
|
||||||
|
|
|
@ -32,6 +32,7 @@ def is_simple_callable(obj):
|
||||||
|
|
||||||
|
|
||||||
class Field(object):
|
class Field(object):
|
||||||
|
read_only = True
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
empty = ''
|
empty = ''
|
||||||
type_name = None
|
type_name = None
|
||||||
|
@ -139,7 +140,7 @@ class WritableField(Field):
|
||||||
if required is None:
|
if required is None:
|
||||||
self.required = not(read_only)
|
self.required = not(read_only)
|
||||||
else:
|
else:
|
||||||
assert not read_only, "Cannot set required=True and read_only=True"
|
assert not (read_only and required), "Cannot set required=True and read_only=True"
|
||||||
self.required = required
|
self.required = required
|
||||||
|
|
||||||
messages = {}
|
messages = {}
|
||||||
|
@ -275,6 +276,7 @@ class RelatedField(WritableField):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.queryset = kwargs.pop('queryset', None)
|
self.queryset = kwargs.pop('queryset', None)
|
||||||
|
self.null = kwargs.pop('null', False)
|
||||||
super(RelatedField, self).__init__(*args, **kwargs)
|
super(RelatedField, self).__init__(*args, **kwargs)
|
||||||
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
||||||
|
|
||||||
|
@ -356,7 +358,13 @@ class RelatedField(WritableField):
|
||||||
return
|
return
|
||||||
|
|
||||||
value = data.get(field_name)
|
value = data.get(field_name)
|
||||||
into[(self.source or field_name)] = self.from_native(value)
|
|
||||||
|
if value in (None, '') and not self.null:
|
||||||
|
raise ValidationError('Value may not be null')
|
||||||
|
elif value in (None, '') and self.null:
|
||||||
|
into[(self.source or field_name)] = None
|
||||||
|
else:
|
||||||
|
into[(self.source or field_name)] = self.from_native(value)
|
||||||
|
|
||||||
|
|
||||||
class ManyRelatedMixin(object):
|
class ManyRelatedMixin(object):
|
||||||
|
@ -580,7 +588,7 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
|
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
# Convert URL -> model instance pk
|
# Convert URL -> model instance pk
|
||||||
|
@ -679,7 +687,7 @@ class HyperlinkedIdentityField(Field):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
|
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||||
|
|
||||||
|
|
||||||
##### Typed Fields #####
|
##### Typed Fields #####
|
||||||
|
@ -699,9 +707,9 @@ class BooleanField(WritableField):
|
||||||
default = False
|
default = False
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
if value in ('t', 'True', '1'):
|
if value in ('true', 't', 'True', '1'):
|
||||||
return True
|
return True
|
||||||
if value in ('f', 'False', '0'):
|
if value in ('false', 'f', 'False', '0'):
|
||||||
return False
|
return False
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
@ -823,6 +831,7 @@ class EmailField(CharField):
|
||||||
|
|
||||||
class RegexField(CharField):
|
class RegexField(CharField):
|
||||||
type_name = 'RegexField'
|
type_name = 'RegexField'
|
||||||
|
form_field_class = forms.RegexField
|
||||||
|
|
||||||
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
||||||
super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
|
super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
|
||||||
|
@ -1054,11 +1063,8 @@ class ImageField(FileField):
|
||||||
if f is None:
|
if f is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Try to import PIL in either of the two ways it can end up installed.
|
from compat import Image
|
||||||
try:
|
assert Image is not None, 'PIL must be installed for ImageField support'
|
||||||
from PIL import Image
|
|
||||||
except ImportError:
|
|
||||||
import Image
|
|
||||||
|
|
||||||
# We need to get a file object for PIL. We might have a path or we might
|
# We need to get a file object for PIL. We might have a path or we might
|
||||||
# have to read the data into memory.
|
# have to read the data into memory.
|
||||||
|
|
|
@ -320,6 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
if getattr(v, 'choices', None) is not None:
|
if getattr(v, 'choices', None) is not None:
|
||||||
kwargs['choices'] = v.choices
|
kwargs['choices'] = v.choices
|
||||||
|
|
||||||
|
if getattr(v, 'regex', None) is not None:
|
||||||
|
kwargs['regex'] = v.regex
|
||||||
|
|
||||||
if getattr(v, 'widget', None):
|
if getattr(v, 'widget', None):
|
||||||
widget = copy.deepcopy(v.widget)
|
widget = copy.deepcopy(v.widget)
|
||||||
kwargs['widget'] = widget
|
kwargs['widget'] = widget
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
|
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
"""
|
||||||
|
Need to fix sys path so following works without specifically messing with PYTHONPATH
|
||||||
|
python ./rest_framework/runtests/runtests.py
|
||||||
|
"""
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
@ -22,7 +22,16 @@ class DictWithMetadata(dict):
|
||||||
"""
|
"""
|
||||||
A dict-like object, that can have additional properties attached.
|
A dict-like object, that can have additional properties attached.
|
||||||
"""
|
"""
|
||||||
pass
|
def __getstate__(self):
|
||||||
|
"""
|
||||||
|
Used by pickle (e.g., caching).
|
||||||
|
Overriden to remove metadata from the dict, since it shouldn't be pickled
|
||||||
|
and may in some instances be unpickleable.
|
||||||
|
"""
|
||||||
|
# return an instance of the first dict in MRO that isn't a DictWithMetadata
|
||||||
|
for base in self.__class__.__mro__:
|
||||||
|
if not isinstance(base, DictWithMetadata) and isinstance(base, dict):
|
||||||
|
return base(self)
|
||||||
|
|
||||||
|
|
||||||
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
|
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
|
||||||
|
@ -91,7 +100,8 @@ class BaseSerializer(Field):
|
||||||
_options_class = SerializerOptions
|
_options_class = SerializerOptions
|
||||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
|
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
|
||||||
|
|
||||||
def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
|
def __init__(self, instance=None, data=None, files=None,
|
||||||
|
context=None, partial=False, **kwargs):
|
||||||
super(BaseSerializer, self).__init__(**kwargs)
|
super(BaseSerializer, self).__init__(**kwargs)
|
||||||
self.opts = self._options_class(self.Meta)
|
self.opts = self._options_class(self.Meta)
|
||||||
self.parent = None
|
self.parent = None
|
||||||
|
@ -131,8 +141,6 @@ class BaseSerializer(Field):
|
||||||
base_fields = copy.deepcopy(self.base_fields)
|
base_fields = copy.deepcopy(self.base_fields)
|
||||||
for key, field in base_fields.items():
|
for key, field in base_fields.items():
|
||||||
ret[key] = field
|
ret[key] = field
|
||||||
# Set up the field
|
|
||||||
field.initialize(parent=self, field_name=key)
|
|
||||||
|
|
||||||
# Add in the default fields
|
# Add in the default fields
|
||||||
default_fields = self.get_default_fields()
|
default_fields = self.get_default_fields()
|
||||||
|
@ -166,6 +174,13 @@ class BaseSerializer(Field):
|
||||||
if parent.opts.depth:
|
if parent.opts.depth:
|
||||||
self.opts.depth = parent.opts.depth - 1
|
self.opts.depth = parent.opts.depth - 1
|
||||||
|
|
||||||
|
# We need to call initialize here to ensure any nested
|
||||||
|
# serializers that will have already called initialize on their
|
||||||
|
# descendants get updated with *their* parent.
|
||||||
|
# We could be a bit more smart about this, but it'll do for now.
|
||||||
|
for key, field in self.fields.items():
|
||||||
|
field.initialize(parent=self, field_name=key)
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Methods to convert or revert from objects <--> primitive representations.
|
# Methods to convert or revert from objects <--> primitive representations.
|
||||||
|
|
||||||
|
@ -184,6 +199,7 @@ class BaseSerializer(Field):
|
||||||
ret.fields = {}
|
ret.fields = {}
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
ret[key] = value
|
ret[key] = value
|
||||||
|
@ -197,6 +213,7 @@ class BaseSerializer(Field):
|
||||||
"""
|
"""
|
||||||
reverted_data = {}
|
reverted_data = {}
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
|
field.initialize(parent=self, field_name=field_name)
|
||||||
try:
|
try:
|
||||||
field.field_from_native(data, files, field_name, reverted_data)
|
field.field_from_native(data, files, field_name, reverted_data)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
|
@ -217,10 +234,18 @@ class BaseSerializer(Field):
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
||||||
|
|
||||||
try:
|
# If there are already errors, we don't run .validate() because
|
||||||
attrs = self.validate(attrs)
|
# field-validation failed and thus `attrs` may not be complete.
|
||||||
except ValidationError as err:
|
# which in turn can cause inconsistent validation errors.
|
||||||
self._errors['non_field_errors'] = err.messages
|
if not self._errors:
|
||||||
|
try:
|
||||||
|
attrs = self.validate(attrs)
|
||||||
|
except ValidationError as err:
|
||||||
|
if hasattr(err, 'message_dict'):
|
||||||
|
for field_name, error_messages in err.message_dict.items():
|
||||||
|
self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
|
||||||
|
elif hasattr(err, 'messages'):
|
||||||
|
self._errors['non_field_errors'] = err.messages
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -272,10 +297,15 @@ class BaseSerializer(Field):
|
||||||
Override default so that we can apply ModelSerializer as a nested
|
Override default so that we can apply ModelSerializer as a nested
|
||||||
field to relationships.
|
field to relationships.
|
||||||
"""
|
"""
|
||||||
obj = getattr(obj, self.source or field_name)
|
if self.source:
|
||||||
|
for component in self.source.split('.'):
|
||||||
if is_simple_callable(obj):
|
obj = getattr(obj, component)
|
||||||
obj = obj()
|
if is_simple_callable(obj):
|
||||||
|
obj = obj()
|
||||||
|
else:
|
||||||
|
obj = getattr(obj, field_name)
|
||||||
|
if is_simple_callable(obj):
|
||||||
|
obj = value()
|
||||||
|
|
||||||
# If the object has an "all" method, assume it's a relationship
|
# If the object has an "all" method, assume it's a relationship
|
||||||
if is_simple_callable(getattr(obj, 'all', None)):
|
if is_simple_callable(getattr(obj, 'all', None)):
|
||||||
|
@ -364,7 +394,6 @@ class ModelSerializer(Serializer):
|
||||||
field = self.get_field(model_field)
|
field = self.get_field(model_field)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
field.initialize(parent=self, field_name=model_field.name)
|
|
||||||
ret[model_field.name] = field
|
ret[model_field.name] = field
|
||||||
|
|
||||||
for field_name in self.opts.read_only_fields:
|
for field_name in self.opts.read_only_fields:
|
||||||
|
@ -396,10 +425,14 @@ class ModelSerializer(Serializer):
|
||||||
"""
|
"""
|
||||||
# TODO: filter queryset using:
|
# TODO: filter queryset using:
|
||||||
# .using(db).complex_filter(self.rel.limit_choices_to)
|
# .using(db).complex_filter(self.rel.limit_choices_to)
|
||||||
queryset = model_field.rel.to._default_manager
|
kwargs = {
|
||||||
|
'null': model_field.null,
|
||||||
|
'queryset': model_field.rel.to._default_manager
|
||||||
|
}
|
||||||
|
|
||||||
if to_many:
|
if to_many:
|
||||||
return ManyPrimaryKeyRelatedField(queryset=queryset)
|
return ManyPrimaryKeyRelatedField(**kwargs)
|
||||||
return PrimaryKeyRelatedField(queryset=queryset)
|
return PrimaryKeyRelatedField(**kwargs)
|
||||||
|
|
||||||
def get_field(self, model_field):
|
def get_field(self, model_field):
|
||||||
"""
|
"""
|
||||||
|
@ -424,10 +457,6 @@ class ModelSerializer(Serializer):
|
||||||
kwargs['choices'] = model_field.flatchoices
|
kwargs['choices'] = model_field.flatchoices
|
||||||
return ChoiceField(**kwargs)
|
return ChoiceField(**kwargs)
|
||||||
|
|
||||||
max_length = getattr(model_field, 'max_length', None)
|
|
||||||
if max_length:
|
|
||||||
kwargs['max_length'] = max_length
|
|
||||||
|
|
||||||
if model_field.verbose_name is not None:
|
if model_field.verbose_name is not None:
|
||||||
kwargs['label'] = model_field.verbose_name
|
kwargs['label'] = model_field.verbose_name
|
||||||
|
|
||||||
|
@ -457,6 +486,18 @@ class ModelSerializer(Serializer):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return ModelField(model_field=model_field, **kwargs)
|
return ModelField(model_field=model_field, **kwargs)
|
||||||
|
|
||||||
|
def get_validation_exclusions(self):
|
||||||
|
"""
|
||||||
|
Return a list of field names to exclude from model validation.
|
||||||
|
"""
|
||||||
|
cls = self.opts.model
|
||||||
|
opts = get_concrete_model(cls)._meta
|
||||||
|
exclusions = [field.name for field in opts.fields + opts.many_to_many]
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
if field_name in exclusions and not field.read_only:
|
||||||
|
exclusions.remove(field_name)
|
||||||
|
return exclusions
|
||||||
|
|
||||||
def restore_object(self, attrs, instance=None):
|
def restore_object(self, attrs, instance=None):
|
||||||
"""
|
"""
|
||||||
Restore the model instance.
|
Restore the model instance.
|
||||||
|
@ -478,7 +519,14 @@ class ModelSerializer(Serializer):
|
||||||
for field in self.opts.model._meta.many_to_many:
|
for field in self.opts.model._meta.many_to_many:
|
||||||
if field.name in attrs:
|
if field.name in attrs:
|
||||||
self.m2m_data[field.name] = attrs.pop(field.name)
|
self.m2m_data[field.name] = attrs.pop(field.name)
|
||||||
return self.opts.model(**attrs)
|
|
||||||
|
instance = self.opts.model(**attrs)
|
||||||
|
try:
|
||||||
|
instance.full_clean(exclude=self.get_validation_exclusions())
|
||||||
|
except ValidationError, err:
|
||||||
|
self._errors = err.message_dict
|
||||||
|
return None
|
||||||
|
return instance
|
||||||
|
|
||||||
def save(self, save_m2m=True):
|
def save(self, save_m2m=True):
|
||||||
"""
|
"""
|
||||||
|
@ -537,9 +585,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
# TODO: filter queryset using:
|
# TODO: filter queryset using:
|
||||||
# .using(db).complex_filter(self.rel.limit_choices_to)
|
# .using(db).complex_filter(self.rel.limit_choices_to)
|
||||||
rel = model_field.rel.to
|
rel = model_field.rel.to
|
||||||
queryset = rel._default_manager
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'queryset': queryset,
|
'null': model_field.null,
|
||||||
|
'queryset': rel._default_manager,
|
||||||
'view_name': self._get_default_view_name(rel)
|
'view_name': self._get_default_view_name(rel)
|
||||||
}
|
}
|
||||||
if to_many:
|
if to_many:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.conf.urls.defaults import patterns, include
|
from django.conf.urls.defaults import patterns
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,)
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockView.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
(r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
|
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ class TokenAuthTests(TestCase):
|
||||||
def test_token_login_json(self):
|
def test_token_login_json(self):
|
||||||
"""Ensure token login view using JSON POST works."""
|
"""Ensure token login view using JSON POST works."""
|
||||||
client = Client(enforce_csrf_checks=True)
|
client = Client(enforce_csrf_checks=True)
|
||||||
response = client.post('/auth-token/login/',
|
response = client.post('/auth-token/',
|
||||||
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
|
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(json.loads(response.content)['token'], self.key)
|
self.assertEqual(json.loads(response.content)['token'], self.key)
|
||||||
|
@ -165,21 +165,21 @@ class TokenAuthTests(TestCase):
|
||||||
def test_token_login_json_bad_creds(self):
|
def test_token_login_json_bad_creds(self):
|
||||||
"""Ensure token login view using JSON POST fails if bad credentials are used."""
|
"""Ensure token login view using JSON POST fails if bad credentials are used."""
|
||||||
client = Client(enforce_csrf_checks=True)
|
client = Client(enforce_csrf_checks=True)
|
||||||
response = client.post('/auth-token/login/',
|
response = client.post('/auth-token/',
|
||||||
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
|
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_token_login_json_missing_fields(self):
|
def test_token_login_json_missing_fields(self):
|
||||||
"""Ensure token login view using JSON POST fails if missing fields."""
|
"""Ensure token login view using JSON POST fails if missing fields."""
|
||||||
client = Client(enforce_csrf_checks=True)
|
client = Client(enforce_csrf_checks=True)
|
||||||
response = client.post('/auth-token/login/',
|
response = client.post('/auth-token/',
|
||||||
json.dumps({'username': self.username}), 'application/json')
|
json.dumps({'username': self.username}), 'application/json')
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_token_login_form(self):
|
def test_token_login_form(self):
|
||||||
"""Ensure token login view using form POST works."""
|
"""Ensure token login view using form POST works."""
|
||||||
client = Client(enforce_csrf_checks=True)
|
client = Client(enforce_csrf_checks=True)
|
||||||
response = client.post('/auth-token/login/',
|
response = client.post('/auth-token/',
|
||||||
{'username': self.username, 'password': self.password})
|
{'username': self.username, 'password': self.password})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(json.loads(response.content)['token'], self.key)
|
self.assertEqual(json.loads(response.content)['token'], self.key)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
from django.utils import simplejson as json
|
||||||
from rest_framework import generics, status, serializers
|
from rest_framework import generics, status, serializers
|
||||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
|
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
|
||||||
|
|
||||||
|
@ -54,10 +55,12 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
||||||
model = BlogPostComment
|
model = BlogPostComment
|
||||||
serializer_class = BlogPostCommentSerializer
|
serializer_class = BlogPostCommentSerializer
|
||||||
|
|
||||||
|
|
||||||
class BlogPostCommentDetail(generics.RetrieveAPIView):
|
class BlogPostCommentDetail(generics.RetrieveAPIView):
|
||||||
model = BlogPostComment
|
model = BlogPostComment
|
||||||
serializer_class = BlogPostCommentSerializer
|
serializer_class = BlogPostCommentSerializer
|
||||||
|
|
||||||
|
|
||||||
class BlogPostDetail(generics.RetrieveAPIView):
|
class BlogPostDetail(generics.RetrieveAPIView):
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
|
|
||||||
|
@ -71,7 +74,7 @@ class AlbumDetail(generics.RetrieveAPIView):
|
||||||
model = Album
|
model = Album
|
||||||
|
|
||||||
|
|
||||||
class OptionalRelationDetail(generics.RetrieveAPIView):
|
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = OptionalRelationModel
|
model = OptionalRelationModel
|
||||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||||
|
|
||||||
|
@ -162,7 +165,7 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
GET requests to ListCreateAPIView should return list of objects.
|
GET requests to ListCreateAPIView should return list of objects.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/manytomany/')
|
request = factory.get('/manytomany/')
|
||||||
response = self.list_view(request).render()
|
response = self.list_view(request)
|
||||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEquals(response.data, self.data)
|
self.assertEquals(response.data, self.data)
|
||||||
|
|
||||||
|
@ -171,7 +174,7 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
GET requests to ListCreateAPIView should return list of objects.
|
GET requests to ListCreateAPIView should return list of objects.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/manytomany/1/')
|
request = factory.get('/manytomany/1/')
|
||||||
response = self.detail_view(request, pk=1).render()
|
response = self.detail_view(request, pk=1)
|
||||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEquals(response.data, self.data[0])
|
self.assertEquals(response.data, self.data[0])
|
||||||
|
|
||||||
|
@ -194,7 +197,7 @@ class TestCreateWithForeignKeys(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
request = factory.post('/comments/', data=data)
|
request = factory.post('/comments/', data=data)
|
||||||
response = self.create_view(request).render()
|
response = self.create_view(request)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
|
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
|
||||||
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
||||||
|
@ -219,7 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
request = factory.post('/photos/', data=data)
|
request = factory.post('/photos/', data=data)
|
||||||
response = self.list_create_view(request).render()
|
response = self.list_create_view(request)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
|
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
|
||||||
self.assertEqual(self.post.photo_set.count(), 1)
|
self.assertEqual(self.post.photo_set.count(), 1)
|
||||||
|
@ -244,6 +247,16 @@ class TestOptionalRelationHyperlinkedView(TestCase):
|
||||||
for non existing relations.
|
for non existing relations.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/optionalrelationmodel-detail/1')
|
request = factory.get('/optionalrelationmodel-detail/1')
|
||||||
response = self.detail_view(request, pk=1).render()
|
response = self.detail_view(request, pk=1)
|
||||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEquals(response.data, self.data)
|
self.assertEquals(response.data, self.data)
|
||||||
|
|
||||||
|
def test_put_detail_view(self):
|
||||||
|
"""
|
||||||
|
PUT requests to RetrieveUpdateDestroyAPIView with optional relations
|
||||||
|
should accept None for non existing relations.
|
||||||
|
"""
|
||||||
|
response = self.client.put('/optionalrelation/1/',
|
||||||
|
data=json.dumps(self.data),
|
||||||
|
content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
|
@ -125,8 +125,21 @@ class ActionItem(RESTFrameworkModel):
|
||||||
|
|
||||||
|
|
||||||
# Models for reverse relations
|
# Models for reverse relations
|
||||||
|
class Person(RESTFrameworkModel):
|
||||||
|
name = models.CharField(max_length=10)
|
||||||
|
age = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'age': self.age,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BlogPost(RESTFrameworkModel):
|
class BlogPost(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
|
writer = models.ForeignKey(Person, null=True, blank=True)
|
||||||
|
|
||||||
def get_first_comment(self):
|
def get_first_comment(self):
|
||||||
return self.blogpostcomment_set.all()[0]
|
return self.blogpostcomment_set.all()[0]
|
||||||
|
@ -146,21 +159,9 @@ class Photo(RESTFrameworkModel):
|
||||||
album = models.ForeignKey(Album)
|
album = models.ForeignKey(Album)
|
||||||
|
|
||||||
|
|
||||||
class Person(RESTFrameworkModel):
|
|
||||||
name = models.CharField(max_length=10)
|
|
||||||
age = models.IntegerField(null=True, blank=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self):
|
|
||||||
return {
|
|
||||||
'name': self.name,
|
|
||||||
'age': self.age,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #324
|
# Model for issue #324
|
||||||
class BlankFieldModel(RESTFrameworkModel):
|
class BlankFieldModel(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100, blank=True)
|
title = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #380
|
# Model for issue #380
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.core.paginator import Paginator
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework import generics, status, pagination, filters
|
from rest_framework import generics, status, pagination, filters, serializers
|
||||||
from rest_framework.compat import django_filters
|
from rest_framework.compat import django_filters
|
||||||
from rest_framework.tests.models import BasicModel, FilterableItem
|
from rest_framework.tests.models import BasicModel, FilterableItem
|
||||||
|
|
||||||
|
@ -148,6 +148,11 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
self.assertEquals(response.data['previous'], None)
|
self.assertEquals(response.data['previous'], None)
|
||||||
|
|
||||||
|
|
||||||
|
class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
|
||||||
|
class Meta:
|
||||||
|
object_serializer_class = serializers.Serializer
|
||||||
|
|
||||||
|
|
||||||
class UnitTestPagination(TestCase):
|
class UnitTestPagination(TestCase):
|
||||||
"""
|
"""
|
||||||
Unit tests for pagination of primitive objects.
|
Unit tests for pagination of primitive objects.
|
||||||
|
@ -172,6 +177,15 @@ class UnitTestPagination(TestCase):
|
||||||
self.assertEquals(serializer.data['previous'], '?page=2')
|
self.assertEquals(serializer.data['previous'], '?page=2')
|
||||||
self.assertEquals(serializer.data['results'], self.objects[20:])
|
self.assertEquals(serializer.data['results'], self.objects[20:])
|
||||||
|
|
||||||
|
def test_context_available_in_result(self):
|
||||||
|
"""
|
||||||
|
Ensure context gets passed through to the object serializer.
|
||||||
|
"""
|
||||||
|
serializer = PassOnContextPaginationSerializer(self.first_page)
|
||||||
|
serializer.data
|
||||||
|
results = serializer.fields[serializer.results_field]
|
||||||
|
self.assertTrue(serializer.context is results.context)
|
||||||
|
|
||||||
|
|
||||||
class TestUnpaginated(TestCase):
|
class TestUnpaginated(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -236,3 +250,32 @@ class TestCustomPaginateByParam(TestCase):
|
||||||
response = self.view(request).render()
|
response = self.view(request).render()
|
||||||
self.assertEquals(response.data['count'], 13)
|
self.assertEquals(response.data['count'], 13)
|
||||||
self.assertEquals(response.data['results'], self.data[:5])
|
self.assertEquals(response.data['results'], self.data[:5])
|
||||||
|
|
||||||
|
|
||||||
|
class CustomField(serializers.Field):
|
||||||
|
def to_native(self, value):
|
||||||
|
if not 'view' in self.context:
|
||||||
|
raise RuntimeError("context isn't getting passed into custom field")
|
||||||
|
return "value"
|
||||||
|
|
||||||
|
|
||||||
|
class BasicModelSerializer(serializers.Serializer):
|
||||||
|
text = CustomField()
|
||||||
|
|
||||||
|
|
||||||
|
class TestContextPassedToCustomField(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
BasicModel.objects.create(text='ala ma kota')
|
||||||
|
|
||||||
|
def test_with_pagination(self):
|
||||||
|
class ListView(generics.ListCreateAPIView):
|
||||||
|
model = BasicModel
|
||||||
|
serializer_class = BasicModelSerializer
|
||||||
|
paginate_by = 1
|
||||||
|
|
||||||
|
self.view = ListView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = self.view(request).render()
|
||||||
|
|
||||||
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
|
@ -49,9 +49,22 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||||
model = ForeignKeySource
|
model = ForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
model = NullableForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||||
|
|
||||||
class PrimaryKeyManyToManyTests(TestCase):
|
class PKManyToManyTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for idx in range(1, 4):
|
for idx in range(1, 4):
|
||||||
target = ManyToManyTarget(name='target-%d' % idx)
|
target = ManyToManyTarget(name='target-%d' % idx)
|
||||||
|
@ -117,6 +130,25 @@ class PrimaryKeyManyToManyTests(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_many_to_many_create(self):
|
||||||
|
data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]}
|
||||||
|
serializer = ManyToManySourceSerializer(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 added, and everything else is as expected
|
||||||
|
queryset = ManyToManySource.objects.all()
|
||||||
|
serializer = ManyToManySourceSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'id': 1, 'name': u'source-1', 'targets': [1]},
|
||||||
|
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
|
||||||
|
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]},
|
||||||
|
{'id': 4, 'name': u'source-4', 'targets': [1, 3]},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
def test_reverse_many_to_many_create(self):
|
def test_reverse_many_to_many_create(self):
|
||||||
data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
|
data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
|
||||||
serializer = ManyToManyTargetSerializer(data=data)
|
serializer = ManyToManyTargetSerializer(data=data)
|
||||||
|
@ -137,7 +169,7 @@ class PrimaryKeyManyToManyTests(TestCase):
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
class PrimaryKeyForeignKeyTests(TestCase):
|
class PKForeignKeyTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
target = ForeignKeyTarget(name='target-1')
|
target = ForeignKeyTarget(name='target-1')
|
||||||
target.save()
|
target.save()
|
||||||
|
@ -174,7 +206,7 @@ class PrimaryKeyForeignKeyTests(TestCase):
|
||||||
self.assertEquals(serializer.data, data)
|
self.assertEquals(serializer.data, data)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
# # Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = ForeignKeySource.objects.all()
|
queryset = ForeignKeySource.objects.all()
|
||||||
serializer = ForeignKeySourceSerializer(queryset)
|
serializer = ForeignKeySourceSerializer(queryset)
|
||||||
expected = [
|
expected = [
|
||||||
|
@ -184,6 +216,106 @@ class PrimaryKeyForeignKeyTests(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
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 PKNullableForeignKeyTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
target = ForeignKeyTarget(name='target-1')
|
||||||
|
target.save()
|
||||||
|
for idx in range(1, 4):
|
||||||
|
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
|
||||||
|
source.save()
|
||||||
|
|
||||||
|
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': 1},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': 1},
|
||||||
|
{'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': 1},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': 1},
|
||||||
|
{'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': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||||
|
]
|
||||||
|
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': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
# reverse foreign keys MUST be read_only
|
# reverse foreign keys MUST be read_only
|
||||||
# In the general case they do not provide .remove() or .clear()
|
# In the general case they do not provide .remove() or .clear()
|
||||||
# and cannot be arbitrarily set.
|
# and cannot be arbitrarily set.
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import pickle
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers, fields
|
from rest_framework import serializers, fields
|
||||||
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
|
from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel,
|
||||||
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
|
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||||
ManyToManyModel, Person, ReadOnlyManyToManyModel)
|
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
||||||
|
|
||||||
|
|
||||||
class SubComment(object):
|
class SubComment(object):
|
||||||
|
@ -62,6 +63,13 @@ class PersonSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('age',)
|
read_only_fields = ('age',)
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumsSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Album
|
||||||
|
fields = ['title'] # lists are also valid options
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(TestCase):
|
class BasicTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.comment = Comment(
|
self.comment = Comment(
|
||||||
|
@ -169,7 +177,7 @@ class ValidationTests(TestCase):
|
||||||
'content': 'x' * 1001,
|
'content': 'x' * 1001,
|
||||||
'created': datetime.datetime(2012, 1, 1)
|
'created': datetime.datetime(2012, 1, 1)
|
||||||
}
|
}
|
||||||
self.actionitem = ActionItem('Some to do item',
|
self.actionitem = ActionItem(title='Some to do item',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
|
@ -277,6 +285,19 @@ class ValidationTests(TestCase):
|
||||||
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
|
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
|
||||||
|
|
||||||
|
|
||||||
|
class ModelValidationTests(TestCase):
|
||||||
|
def test_validate_unique(self):
|
||||||
|
"""
|
||||||
|
Just check if serializers.ModelSerializer handles unique checks via .full_clean()
|
||||||
|
"""
|
||||||
|
serializer = AlbumsSerializer(data={'title': 'a'})
|
||||||
|
serializer.is_valid()
|
||||||
|
serializer.save()
|
||||||
|
second_serializer = AlbumsSerializer(data={'title': 'a'})
|
||||||
|
self.assertFalse(second_serializer.is_valid())
|
||||||
|
self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
|
||||||
|
|
||||||
|
|
||||||
class RegexValidationTest(TestCase):
|
class RegexValidationTest(TestCase):
|
||||||
def test_create_failed(self):
|
def test_create_failed(self):
|
||||||
serializer = BookSerializer(data={'isbn': '1234567890'})
|
serializer = BookSerializer(data={'isbn': '1234567890'})
|
||||||
|
@ -560,6 +581,47 @@ class ManyRelatedTests(TestCase):
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedTraversalTest(TestCase):
|
||||||
|
def test_nested_traversal(self):
|
||||||
|
user = Person.objects.create(name="django")
|
||||||
|
post = BlogPost.objects.create(title="Test blog post", writer=user)
|
||||||
|
post.blogpostcomment_set.create(text="I love this blog post")
|
||||||
|
|
||||||
|
from rest_framework.tests.models import BlogPostComment
|
||||||
|
|
||||||
|
class PersonSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
fields = ("name", "age")
|
||||||
|
|
||||||
|
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BlogPostComment
|
||||||
|
fields = ("text", "post_owner")
|
||||||
|
|
||||||
|
text = serializers.CharField()
|
||||||
|
post_owner = PersonSerializer(source='blog_post.writer')
|
||||||
|
|
||||||
|
class BlogPostSerializer(serializers.Serializer):
|
||||||
|
title = serializers.CharField()
|
||||||
|
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
||||||
|
|
||||||
|
serializer = BlogPostSerializer(instance=post)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'title': u'Test blog post',
|
||||||
|
'comments': [{
|
||||||
|
'text': u'I love this blog post',
|
||||||
|
'post_owner': {
|
||||||
|
"name": u"django",
|
||||||
|
"age": None
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
class SerializerMethodFieldTests(TestCase):
|
class SerializerMethodFieldTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
|
@ -643,6 +705,118 @@ class BlankFieldTests(TestCase):
|
||||||
self.assertEquals(serializer.is_valid(), False)
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
|
|
||||||
|
|
||||||
|
#test for issue #460
|
||||||
|
class SerializerPickleTests(TestCase):
|
||||||
|
"""
|
||||||
|
Test pickleability of the output of Serializers
|
||||||
|
"""
|
||||||
|
def test_pickle_simple_model_serializer_data(self):
|
||||||
|
"""
|
||||||
|
Test simple serializer
|
||||||
|
"""
|
||||||
|
pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
|
||||||
|
|
||||||
|
def test_pickle_inner_serializer(self):
|
||||||
|
"""
|
||||||
|
Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
|
||||||
|
have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
|
||||||
|
See DictWithMetadata.__getstate__
|
||||||
|
"""
|
||||||
|
class InnerPersonSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
fields = ('name', 'age')
|
||||||
|
pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data)
|
||||||
|
|
||||||
|
|
||||||
|
class DepthTest(TestCase):
|
||||||
|
def test_implicit_nesting(self):
|
||||||
|
writer = Person.objects.create(name="django", age=1)
|
||||||
|
post = BlogPost.objects.create(title="Test blog post", writer=writer)
|
||||||
|
|
||||||
|
class BlogPostSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BlogPost
|
||||||
|
depth = 1
|
||||||
|
|
||||||
|
serializer = BlogPostSerializer(instance=post)
|
||||||
|
expected = {'id': 1, 'title': u'Test blog post',
|
||||||
|
'writer': {'id': 1, 'name': u'django', 'age': 1}}
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_explicit_nesting(self):
|
||||||
|
writer = Person.objects.create(name="django", age=1)
|
||||||
|
post = BlogPost.objects.create(title="Test blog post", writer=writer)
|
||||||
|
|
||||||
|
class PersonSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
|
||||||
|
class BlogPostSerializer(serializers.ModelSerializer):
|
||||||
|
writer = PersonSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BlogPost
|
||||||
|
|
||||||
|
serializer = BlogPostSerializer(instance=post)
|
||||||
|
expected = {'id': 1, 'title': u'Test blog post',
|
||||||
|
'writer': {'id': 1, 'name': u'django', 'age': 1}}
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSerializerContextTests(TestCase):
|
||||||
|
|
||||||
|
def test_nested_serializer_context(self):
|
||||||
|
"""
|
||||||
|
Regression for #497
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/issues/497
|
||||||
|
"""
|
||||||
|
class PhotoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Photo
|
||||||
|
fields = ("description", "callable")
|
||||||
|
|
||||||
|
callable = serializers.SerializerMethodField('_callable')
|
||||||
|
|
||||||
|
def _callable(self, instance):
|
||||||
|
if not 'context_item' in self.context:
|
||||||
|
raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
|
||||||
|
return "success"
|
||||||
|
|
||||||
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Album
|
||||||
|
fields = ("photo_set", "callable")
|
||||||
|
|
||||||
|
photo_set = PhotoSerializer(source="photo_set")
|
||||||
|
callable = serializers.SerializerMethodField("_callable")
|
||||||
|
|
||||||
|
def _callable(self, instance):
|
||||||
|
if not 'context_item' in self.context:
|
||||||
|
raise RuntimeError("context isn't getting passed into 1st level nested serializer")
|
||||||
|
return "success"
|
||||||
|
|
||||||
|
class AlbumCollection(object):
|
||||||
|
albums = None
|
||||||
|
|
||||||
|
class AlbumCollectionSerializer(serializers.Serializer):
|
||||||
|
albums = AlbumSerializer(source="albums")
|
||||||
|
|
||||||
|
album1 = Album.objects.create(title="album 1")
|
||||||
|
album2 = Album.objects.create(title="album 2")
|
||||||
|
Photo.objects.create(description="Bigfoot", album=album1)
|
||||||
|
Photo.objects.create(description="Unicorn", album=album1)
|
||||||
|
Photo.objects.create(description="Yeti", album=album2)
|
||||||
|
Photo.objects.create(description="Sasquatch", album=album2)
|
||||||
|
album_collection = AlbumCollection()
|
||||||
|
album_collection.albums = [album1, album2]
|
||||||
|
|
||||||
|
# This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
|
||||||
|
AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
|
||||||
|
|
||||||
# Test for issue #467
|
# Test for issue #467
|
||||||
class FieldLabelTest(TestCase):
|
class FieldLabelTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user