mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +03:00
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into patch-support
This commit is contained in:
commit
c6f212238c
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,5 +10,10 @@ dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
|
bin/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
local/
|
||||||
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!.travis.yml
|
!.travis.yml
|
||||||
|
|
56
README.md
56
README.md
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
**A toolkit for building well-connected, self-describing web APIs.**
|
**A toolkit for building well-connected, self-describing web APIs.**
|
||||||
|
|
||||||
**Author:** Tom Christie. [Follow me on Twitter][twitter]
|
**Author:** Tom Christie. [Follow me on Twitter][twitter].
|
||||||
|
|
||||||
|
**Support:** [REST framework discussion group][group].
|
||||||
|
|
||||||
[![build-status-image]][travis]
|
[![build-status-image]][travis]
|
||||||
|
|
||||||
|
@ -37,14 +39,35 @@ There is also a sandbox API you can use for testing purposes, [available here][s
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Install using `pip`...
|
Install using `pip`, including any optional packages you want...
|
||||||
|
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
|
pip install markdown # Markdown support for the browseable API.
|
||||||
|
pip install pyyaml # YAML content-type support.
|
||||||
|
pip install django-filter # Filtering support
|
||||||
|
|
||||||
...or clone the project from github.
|
...or clone the project from github.
|
||||||
|
|
||||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||||
|
cd django-rest-framework
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install -r optionals.txt
|
||||||
|
|
||||||
|
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
...
|
||||||
|
'rest_framework',
|
||||||
|
)
|
||||||
|
|
||||||
|
If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
...
|
||||||
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
@ -58,6 +81,34 @@ To run the tests.
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### 2.1.13
|
||||||
|
|
||||||
|
**Date**: 28th Dec 2012
|
||||||
|
|
||||||
|
* Support configurable `STATICFILES_STORAGE` storage.
|
||||||
|
* Bugfix: Related fields now respect the required flag, and may be required=False.
|
||||||
|
|
||||||
|
### 2.1.12
|
||||||
|
|
||||||
|
**Date**: 21st Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix bug that could occur using ChoiceField.
|
||||||
|
* 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
|
||||||
|
|
||||||
|
**Date**: 17th Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix issue with M2M fields in browseable API.
|
||||||
|
|
||||||
|
## 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
|
**Date**: 11th Dec 2012
|
||||||
|
@ -198,6 +249,7 @@ 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=restframework2
|
||||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||||
[twitter]: https://twitter.com/_tomchristie
|
[twitter]: https://twitter.com/_tomchristie
|
||||||
|
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
||||||
|
|
|
@ -150,7 +150,7 @@ Provides a base view for acting on a single object, by combining REST framework'
|
||||||
|
|
||||||
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
|
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
|
||||||
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
|
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
|
||||||
* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
|
* `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
|
||||||
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
|
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
|
|
||||||
> Expanding the usefulness of the serializers is something that we would
|
> Expanding the usefulness of the serializers is something that we would
|
||||||
like to address. However, it's not a trivial problem, and it
|
like to address. However, it's not a trivial problem, and it
|
||||||
will take some serious design work. Any offers to help out in this
|
will take some serious design work.
|
||||||
area would be gratefully accepted.
|
|
||||||
>
|
>
|
||||||
> — Russell Keith-Magee, [Django users group][cite]
|
> — Russell Keith-Magee, [Django users group][cite]
|
||||||
|
|
||||||
|
@ -110,7 +109,22 @@ Your `validate_<fieldname>` methods should either just return the `attrs` dictio
|
||||||
|
|
||||||
### Object-level validation
|
### Object-level validation
|
||||||
|
|
||||||
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`.
|
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example:
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class EventSerializer(serializers.Serializer):
|
||||||
|
description = serializers.CahrField(max_length=100)
|
||||||
|
start = serializers.DateTimeField()
|
||||||
|
finish = serializers.DateTimeField()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""
|
||||||
|
Check that the start is before the stop.
|
||||||
|
"""
|
||||||
|
if attrs['start'] < attrs['finish']:
|
||||||
|
raise serializers.ValidationError("finish must occur after start")
|
||||||
|
return attrs
|
||||||
|
|
||||||
## Saving object state
|
## Saving object state
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ Django REST framework is a lightweight library that makes it easy to build Web A
|
||||||
|
|
||||||
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
||||||
|
|
||||||
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
|
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
|
||||||
|
|
||||||
There is also a sandbox API you can use for testing purposes, [available here][sandbox].
|
There is also a sandbox API you can use for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
|
@ -52,21 +52,21 @@ Install using `pip`, including any optional packages you want...
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r optionals.txt
|
pip install -r optionals.txt
|
||||||
|
|
||||||
Add `rest_framework` to your `INSTALLED_APPS`.
|
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
)
|
)
|
||||||
|
|
||||||
If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
...
|
...
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
|
||||||
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
|
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,11 @@ The following people have helped make REST framework great.
|
||||||
* Colin Murtaugh - [cmurtaugh]
|
* Colin Murtaugh - [cmurtaugh]
|
||||||
* Simon Pantzare - [pilt]
|
* Simon Pantzare - [pilt]
|
||||||
* Szymon Teżewski - [sunscrapers]
|
* Szymon Teżewski - [sunscrapers]
|
||||||
|
* Joel Marcotte - [joual]
|
||||||
|
* Trey Hunner - [treyhunner]
|
||||||
|
* Roman Akinfold - [akinfold]
|
||||||
|
* Toran Billups - [toranb]
|
||||||
|
* Sébastien Béal - [sebastibe]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -98,10 +103,9 @@ Development of REST framework 2.0 was sponsored by [DabApps].
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
To contact the author directly:
|
For usage questions please see the [REST framework discussion group][group].
|
||||||
|
|
||||||
* twitter: [@_tomchristie][twitter]
|
You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
* email: [tom@tomchristie.com][email]
|
|
||||||
|
|
||||||
[email]: mailto:tom@tomchristie.com
|
[email]: mailto:tom@tomchristie.com
|
||||||
[twitter]: http://twitter.com/_tomchristie
|
[twitter]: http://twitter.com/_tomchristie
|
||||||
|
@ -115,6 +119,7 @@ To contact the author directly:
|
||||||
[dabapps]: http://lab.dabapps.com
|
[dabapps]: http://lab.dabapps.com
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
[heroku]: http://www.heroku.com/
|
[heroku]: http://www.heroku.com/
|
||||||
|
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
|
|
||||||
[tomchristie]: https://github.com/tomchristie
|
[tomchristie]: https://github.com/tomchristie
|
||||||
[markotibold]: https://github.com/markotibold
|
[markotibold]: https://github.com/markotibold
|
||||||
|
@ -193,3 +198,8 @@ To contact the author directly:
|
||||||
[cmurtaugh]: https://github.com/cmurtaugh
|
[cmurtaugh]: https://github.com/cmurtaugh
|
||||||
[pilt]: https://github.com/pilt
|
[pilt]: https://github.com/pilt
|
||||||
[sunscrapers]: https://github.com/sunscrapers
|
[sunscrapers]: https://github.com/sunscrapers
|
||||||
|
[joual]: https://github.com/joual
|
||||||
|
[treyhunner]: https://github.com/treyhunner
|
||||||
|
[akinfold]: https://github.com/akinfold
|
||||||
|
[toranb]: https://github.com/toranb
|
||||||
|
[sebastibe]: https://github.com/sebastibe
|
||||||
|
|
|
@ -4,10 +4,49 @@
|
||||||
>
|
>
|
||||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
|
||||||
|
|
||||||
|
Medium version numbers (0.x.0) may include minor API changes. You should read the release notes carefully before upgrading between medium point releases.
|
||||||
|
|
||||||
|
Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2.1.x series
|
## 2.1.x series
|
||||||
|
|
||||||
### Master
|
### Master
|
||||||
|
|
||||||
|
* Bugfix: ModelSerializers now include reverse FK fields on creation.
|
||||||
|
* Bugfix: Model fields with `blank=True` are now `required=False` by default.
|
||||||
|
* Bugfix: Nested serializers now support nullable relationships.
|
||||||
|
|
||||||
|
### 2.1.13
|
||||||
|
|
||||||
|
**Date**: 28th Dec 2012
|
||||||
|
|
||||||
|
* Support configurable `STATICFILES_STORAGE` storage.
|
||||||
|
* Bugfix: Related fields now respect the required flag, and may be required=False.
|
||||||
|
|
||||||
|
### 2.1.12
|
||||||
|
|
||||||
|
**Date**: 21st Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix bug that could occur using ChoiceField.
|
||||||
|
* 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
|
||||||
|
|
||||||
|
**Date**: 17th Dec 2012
|
||||||
|
|
||||||
|
* Bugfix: Fix issue with M2M fields in browseable API.
|
||||||
|
|
||||||
|
### 2.1.10
|
||||||
|
|
||||||
|
**Date**: 17th Dec 2012
|
||||||
|
|
||||||
* Bugfix: Ensure read-only fields don't have model validation applied.
|
* Bugfix: Ensure read-only fields don't have model validation applied.
|
||||||
* Bugfix: Fix hyperlinked fields in paginated results.
|
* Bugfix: Fix hyperlinked fields in paginated results.
|
||||||
|
|
||||||
|
@ -85,7 +124,7 @@
|
||||||
|
|
||||||
* Support use of HTML exception templates. Eg. `403.html`
|
* Support use of HTML exception templates. Eg. `403.html`
|
||||||
* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
|
* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
|
||||||
* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs.
|
* Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs.
|
||||||
* 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.
|
||||||
|
|
||||||
|
@ -93,8 +132,6 @@
|
||||||
|
|
||||||
**Date**: 5th Nov 2012
|
**Date**: 5th Nov 2012
|
||||||
|
|
||||||
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
|
|
||||||
|
|
||||||
* **Serializer `instance` and `data` keyword args have their position swapped.**
|
* **Serializer `instance` and `data` keyword args have their position swapped.**
|
||||||
* `queryset` argument is now optional on writable model fields.
|
* `queryset` argument is now optional on writable model fields.
|
||||||
* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
|
* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
|
||||||
|
@ -103,6 +140,8 @@
|
||||||
* 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.
|
||||||
|
|
||||||
|
**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.0.x series
|
## 2.0.x series
|
||||||
|
@ -139,9 +178,9 @@
|
||||||
* Allow views to specify template used by TemplateRenderer
|
* Allow views to specify template used by TemplateRenderer
|
||||||
* More consistent error responses
|
* More consistent error responses
|
||||||
* Some serializer fixes
|
* Some serializer fixes
|
||||||
* Fix internet explorer ajax behaviour
|
* Fix internet explorer ajax behavior
|
||||||
* Minor xml and yaml fixes
|
* Minor xml and yaml fixes
|
||||||
* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
|
* Improve setup (e.g. 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -152,13 +191,13 @@
|
||||||
|
|
||||||
* 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.
|
||||||
- Easier to override. Won't conflict with customised admin styles (eg grappelli)
|
- Easier to override. Won't conflict with customized admin styles (e.g. grappelli)
|
||||||
* Templates are now nicely namespaced.
|
* Templates are now nicely namespaced.
|
||||||
- Allows easier overriding.
|
- Allows easier overriding.
|
||||||
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
|
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
|
||||||
- Too magical. Explict is better than implicit.
|
- Too magical. Explicit is better than implicit.
|
||||||
* Saner template variable autoescaping.
|
* Saner template variable auto-escaping.
|
||||||
* Tider setup.py
|
* Tidier setup.py
|
||||||
* Updated for URLObject 2.0
|
* Updated for URLObject 2.0
|
||||||
* Bugfixes:
|
* Bugfixes:
|
||||||
- Bug with PerUserThrottling when user contains unicode chars.
|
- Bug with PerUserThrottling when user contains unicode chars.
|
||||||
|
@ -246,5 +285,7 @@
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|
||||||
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
|
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
|
||||||
|
[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag
|
||||||
|
[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
|
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||||
[announcement]: rest-framework-2-announcement.md
|
[announcement]: rest-framework-2-announcement.md
|
||||||
|
|
|
@ -163,9 +163,9 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
|
||||||
|
|
||||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
||||||
|
|
||||||
* Contribute on [GitHub][github] by reviewing and subitting issues, and making pull requests.
|
* 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.
|
* Join the [REST framework discussion group][group], and help build the community.
|
||||||
* Follow the author [on Twitter][twitter] and say hi.
|
* [Follow the author on Twitter][twitter] and say hi.
|
||||||
|
|
||||||
**Now go build awesome things.**
|
**Now go build awesome things.**
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '2.1.9'
|
__version__ = '2.1.13'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -5,6 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
import django
|
import django
|
||||||
|
|
||||||
|
# location of patterns, url, include changes in 1.4 onwards
|
||||||
|
try:
|
||||||
|
from django.conf.urls import patterns, url, include
|
||||||
|
except:
|
||||||
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
|
|
||||||
# django-filter is optional
|
# django-filter is optional
|
||||||
try:
|
try:
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
|
@ -351,7 +351,12 @@ class RelatedField(WritableField):
|
||||||
if self.read_only:
|
if self.read_only:
|
||||||
return
|
return
|
||||||
|
|
||||||
value = data.get(field_name)
|
try:
|
||||||
|
value = data[field_name]
|
||||||
|
except KeyError:
|
||||||
|
if self.required:
|
||||||
|
raise ValidationError(self.error_messages['required'])
|
||||||
|
return
|
||||||
|
|
||||||
if value in (None, '') and not self.null:
|
if value in (None, '') and not self.null:
|
||||||
raise ValidationError('Value may not be null')
|
raise ValidationError('Value may not be null')
|
||||||
|
@ -384,6 +389,7 @@ class ManyRelatedMixin(object):
|
||||||
else:
|
else:
|
||||||
if value == ['']:
|
if value == ['']:
|
||||||
value = []
|
value = []
|
||||||
|
|
||||||
into[field_name] = [self.from_native(item) for item in value]
|
into[field_name] = [self.from_native(item) for item in value]
|
||||||
|
|
||||||
|
|
||||||
|
@ -795,7 +801,7 @@ class ChoiceField(WritableField):
|
||||||
if value == smart_unicode(k2):
|
if value == smart_unicode(k2):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
if value == smart_unicode(k):
|
if value == smart_unicode(k) or value == k:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,10 @@ class UpdateModelMixin(object):
|
||||||
slug_field = self.get_slug_field()
|
slug_field = self.get_slug_field()
|
||||||
setattr(obj, slug_field, slug)
|
setattr(obj, slug_field, slug)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
class DestroyModelMixin(object):
|
class DestroyModelMixin(object):
|
||||||
"""
|
"""
|
||||||
|
@ -120,6 +124,6 @@ class DestroyModelMixin(object):
|
||||||
Should be mixed in with `SingleObjectBaseView`.
|
Should be mixed in with `SingleObjectBaseView`.
|
||||||
"""
|
"""
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
obj = self.get_object()
|
||||||
self.object.delete()
|
obj.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
@ -20,7 +20,7 @@ from rest_framework.utils import dict2xml
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from rest_framework import VERSION, status
|
from rest_framework import VERSION, status
|
||||||
from rest_framework import serializers, parsers
|
from rest_framework import parsers
|
||||||
|
|
||||||
|
|
||||||
class BaseRenderer(object):
|
class BaseRenderer(object):
|
||||||
|
|
|
@ -188,6 +188,14 @@ class Request(object):
|
||||||
self._user, self._auth = self._authenticate()
|
self._user, self._auth = self._authenticate()
|
||||||
return self._auth
|
return self._auth
|
||||||
|
|
||||||
|
@auth.setter
|
||||||
|
def auth(self, value):
|
||||||
|
"""
|
||||||
|
Sets any non-user authentication information associated with the
|
||||||
|
request, such as an authentication token.
|
||||||
|
"""
|
||||||
|
self._auth = value
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
def _load_data_and_files(self):
|
||||||
"""
|
"""
|
||||||
Parses the request content into self.DATA and self.FILES.
|
Parses the request content into self.DATA and self.FILES.
|
||||||
|
|
|
@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep
|
||||||
# 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
|
||||||
|
|
||||||
|
# fix sys path so we don't need to setup PYTHONPATH
|
||||||
|
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 coverage import coverage
|
from coverage import coverage
|
||||||
|
@ -55,6 +58,12 @@ def main():
|
||||||
if 'compat.py' in files:
|
if 'compat.py' in files:
|
||||||
files.remove('compat.py')
|
files.remove('compat.py')
|
||||||
|
|
||||||
|
# Same applies to template tags module.
|
||||||
|
# This module has to include branching on Django versions,
|
||||||
|
# so it's never possible for it to have full coverage.
|
||||||
|
if 'rest_framework.py' in files:
|
||||||
|
files.remove('rest_framework.py')
|
||||||
|
|
||||||
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
||||||
|
|
||||||
cov.report(cov_files)
|
cov.report(cov_files)
|
||||||
|
|
|
@ -5,10 +5,8 @@
|
||||||
# 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
|
# fix sys path so we don't need to setup PYTHONPATH
|
||||||
python ./rest_framework/runtests/runtests.py
|
|
||||||
"""
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
|
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'
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Blank URLConf just to keep runtests.py happy.
|
Blank URLConf just to keep runtests.py happy.
|
||||||
"""
|
"""
|
||||||
from django.conf.urls.defaults import *
|
from rest_framework.compat import patterns
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
)
|
)
|
||||||
|
|
|
@ -160,6 +160,9 @@ class BaseSerializer(Field):
|
||||||
for key in self.opts.exclude:
|
for key in self.opts.exclude:
|
||||||
ret.pop(key, None)
|
ret.pop(key, None)
|
||||||
|
|
||||||
|
for key, field in ret.items():
|
||||||
|
field.initialize(parent=self, field_name=key)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
#####
|
#####
|
||||||
|
@ -174,13 +177,6 @@ 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.
|
||||||
|
|
||||||
|
@ -311,6 +307,9 @@ class BaseSerializer(Field):
|
||||||
if is_simple_callable(getattr(obj, 'all', None)):
|
if is_simple_callable(getattr(obj, 'all', None)):
|
||||||
return [self.to_native(item) for item in obj.all()]
|
return [self.to_native(item) for item in obj.all()]
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
return self.to_native(obj)
|
return self.to_native(obj)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -442,7 +441,7 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
kwargs['blank'] = model_field.blank
|
kwargs['blank'] = model_field.blank
|
||||||
|
|
||||||
if model_field.null:
|
if model_field.null or model_field.blank:
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
|
|
||||||
if model_field.has_default():
|
if model_field.has_default():
|
||||||
|
@ -497,29 +496,38 @@ class ModelSerializer(Serializer):
|
||||||
Restore the model instance.
|
Restore the model instance.
|
||||||
"""
|
"""
|
||||||
self.m2m_data = {}
|
self.m2m_data = {}
|
||||||
|
self.related_data = {}
|
||||||
|
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
for key, val in attrs.items():
|
for key, val in attrs.items():
|
||||||
setattr(instance, key, val)
|
setattr(instance, key, val)
|
||||||
return instance
|
|
||||||
|
|
||||||
# Reverse relations
|
else:
|
||||||
|
# Reverse fk relations
|
||||||
|
for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
|
||||||
|
field_name = obj.field.related_query_name()
|
||||||
|
if field_name in attrs:
|
||||||
|
self.related_data[field_name] = attrs.pop(field_name)
|
||||||
|
|
||||||
|
# Reverse m2m relations
|
||||||
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
|
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
|
||||||
field_name = obj.field.related_query_name()
|
field_name = obj.field.related_query_name()
|
||||||
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)
|
||||||
|
|
||||||
# Forward relations
|
# Forward m2m relations
|
||||||
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)
|
||||||
|
|
||||||
instance = self.opts.model(**attrs)
|
instance = self.opts.model(**attrs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
instance.full_clean(exclude=self.get_validation_exclusions())
|
instance.full_clean(exclude=self.get_validation_exclusions())
|
||||||
except ValidationError, err:
|
except ValidationError, err:
|
||||||
self._errors = err.message_dict
|
self._errors = err.message_dict
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def save(self, save_m2m=True):
|
def save(self, save_m2m=True):
|
||||||
|
@ -533,6 +541,11 @@ class ModelSerializer(Serializer):
|
||||||
setattr(self.object, accessor_name, object_list)
|
setattr(self.object, accessor_name, object_list)
|
||||||
self.m2m_data = {}
|
self.m2m_data = {}
|
||||||
|
|
||||||
|
if getattr(self, 'related_data', None):
|
||||||
|
for accessor_name, object_list in self.related_data.items():
|
||||||
|
setattr(self.object, accessor_name, object_list)
|
||||||
|
self.related_data = {}
|
||||||
|
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% load url from future %}
|
{% load url from future %}
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -14,10 +13,10 @@
|
||||||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/prettify.css'/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -195,10 +194,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script>
|
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
|
||||||
<script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script>
|
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
|
||||||
<script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script>
|
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
|
||||||
<script src="{% get_static_prefix %}rest_framework/js/default.js"></script>
|
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{% load url from future %}
|
{% load url from future %}
|
||||||
{% load static %}
|
{% load rest_framework %}
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="container">
|
<body class="container">
|
||||||
|
|
|
@ -11,6 +11,89 @@ import string
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
# Note we don't use 'load staticfiles', because we need a 1.3 compatible
|
||||||
|
# version, so instead we include the `static` template tag ourselves.
|
||||||
|
|
||||||
|
# When 1.3 becomes unsupported by REST framework, we can instead start to
|
||||||
|
# use the {% load staticfiles %} tag, remove the following code,
|
||||||
|
# and add a dependancy that `django.contrib.staticfiles` must be installed.
|
||||||
|
|
||||||
|
# Note: We can't put this into the `compat` module because the compat import
|
||||||
|
# from rest_framework.compat import ...
|
||||||
|
# conflicts with this rest_framework template tag module.
|
||||||
|
|
||||||
|
try: # Django 1.5+
|
||||||
|
from django.contrib.staticfiles.templatetags import StaticFilesNode
|
||||||
|
|
||||||
|
@register.tag('static')
|
||||||
|
def do_static(parser, token):
|
||||||
|
return StaticFilesNode.handle_token(parser, token)
|
||||||
|
|
||||||
|
except:
|
||||||
|
try: # Django 1.4
|
||||||
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def static(path):
|
||||||
|
"""
|
||||||
|
A template tag that returns the URL to a file
|
||||||
|
using staticfiles' storage backend
|
||||||
|
"""
|
||||||
|
return staticfiles_storage.url(path)
|
||||||
|
|
||||||
|
except: # Django 1.3
|
||||||
|
from urlparse import urljoin
|
||||||
|
from django import template
|
||||||
|
from django.templatetags.static import PrefixNode
|
||||||
|
|
||||||
|
class StaticNode(template.Node):
|
||||||
|
def __init__(self, varname=None, path=None):
|
||||||
|
if path is None:
|
||||||
|
raise template.TemplateSyntaxError(
|
||||||
|
"Static template nodes must be given a path to return.")
|
||||||
|
self.path = path
|
||||||
|
self.varname = varname
|
||||||
|
|
||||||
|
def url(self, context):
|
||||||
|
path = self.path.resolve(context)
|
||||||
|
return self.handle_simple(path)
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
url = self.url(context)
|
||||||
|
if self.varname is None:
|
||||||
|
return url
|
||||||
|
context[self.varname] = url
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_simple(cls, path):
|
||||||
|
return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_token(cls, parser, token):
|
||||||
|
"""
|
||||||
|
Class method to parse prefix node and return a Node.
|
||||||
|
"""
|
||||||
|
bits = token.split_contents()
|
||||||
|
|
||||||
|
if len(bits) < 2:
|
||||||
|
raise template.TemplateSyntaxError(
|
||||||
|
"'%s' takes at least one argument (path to file)" % bits[0])
|
||||||
|
|
||||||
|
path = parser.compile_filter(bits[1])
|
||||||
|
|
||||||
|
if len(bits) >= 2 and bits[-2] == 'as':
|
||||||
|
varname = bits[3]
|
||||||
|
else:
|
||||||
|
varname = None
|
||||||
|
|
||||||
|
return cls(varname, path)
|
||||||
|
|
||||||
|
@register.tag('static')
|
||||||
|
def do_static_13(parser, token):
|
||||||
|
return StaticNode.handle_token(parser, token)
|
||||||
|
|
||||||
|
|
||||||
def replace_query_param(url, key, val):
|
def replace_query_param(url, key, val):
|
||||||
"""
|
"""
|
||||||
Given a URL and a key/val pair, set or replace an item in the query
|
Given a URL and a key/val pair, set or replace an item in the query
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
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.utils import simplejson as json
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
from rest_framework.compat import patterns
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
from rest_framework import generics, serializers, status
|
from rest_framework import generics, serializers, status
|
||||||
|
@ -174,7 +175,7 @@ class TestInstanceView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.put('/1', json.dumps(content),
|
request = factory.put('/1', json.dumps(content),
|
||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
response = self.view(request, pk=1).render()
|
response = self.view(request, pk='1').render()
|
||||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
|
@ -315,3 +316,36 @@ class TestCreateModelWithAutoNowAddField(TestCase):
|
||||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||||
created = self.objects.get(id=1)
|
created = self.objects.get(id=1)
|
||||||
self.assertEquals(created.content, 'foobar')
|
self.assertEquals(created.content, 'foobar')
|
||||||
|
|
||||||
|
|
||||||
|
# Test for particularly ugly reression with m2m in browseable API
|
||||||
|
class ClassB(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassA(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
childs = models.ManyToManyField(ClassB, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassASerializer(serializers.ModelSerializer):
|
||||||
|
childs = serializers.ManyPrimaryKeyRelatedField(source='childs')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ClassA
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleView(generics.ListCreateAPIView):
|
||||||
|
serializer_class = ClassASerializer
|
||||||
|
model = ClassA
|
||||||
|
|
||||||
|
|
||||||
|
class TestM2MBrowseableAPI(TestCase):
|
||||||
|
def test_m2m_in_browseable_api(self):
|
||||||
|
"""
|
||||||
|
Test for particularly ugly reression with m2m in browseable API
|
||||||
|
"""
|
||||||
|
request = factory.get('/', HTTP_ACCEPT='text/html')
|
||||||
|
view = ExampleView().as_view()
|
||||||
|
response = view(request).render()
|
||||||
|
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.conf.urls.defaults import patterns, url
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.template import TemplateDoesNotExist, Template
|
from django.template import TemplateDoesNotExist, Template
|
||||||
import django.template.loader
|
import django.template.loader
|
||||||
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
from rest_framework.renderers import TemplateHTMLRenderer
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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 django.utils import simplejson as json
|
||||||
from rest_framework import generics, status, serializers
|
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
|
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
|
@ -51,6 +51,11 @@ class RESTFrameworkModel(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class HasPositiveIntegerAsChoice(RESTFrameworkModel):
|
||||||
|
some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
|
||||||
|
some_integer = models.PositiveIntegerField(choices=some_choices)
|
||||||
|
|
||||||
|
|
||||||
class Anchor(RESTFrameworkModel):
|
class Anchor(RESTFrameworkModel):
|
||||||
text = models.CharField(max_length=100, default='anchor')
|
text = models.CharField(max_length=100, default='anchor')
|
||||||
|
|
||||||
|
@ -160,7 +165,7 @@ class Photo(RESTFrameworkModel):
|
||||||
|
|
||||||
# Model for issue #324
|
# Model for issue #324
|
||||||
class BlankFieldModel(RESTFrameworkModel):
|
class BlankFieldModel(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100, blank=True, null=True)
|
title = models.CharField(max_length=100, blank=True, null=False)
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #380
|
# Model for issue #380
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# from django.conf.urls.defaults import patterns, url
|
# from rest_framework.compat import patterns, url
|
||||||
# from django.forms import ModelForm
|
# from django.forms import ModelForm
|
||||||
# from django.contrib.auth.models import Group, User
|
# from django.contrib.auth.models import Group, User
|
||||||
# from rest_framework.resources import ModelResource
|
# from rest_framework.resources import ModelResource
|
||||||
|
|
424
rest_framework/tests/relations_hyperlink.py
Normal file
424
rest_framework/tests/relations_hyperlink.py
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.compat import patterns, url
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_view(request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
||||||
|
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ManyToManyTarget
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
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')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ForeignKeyTarget
|
||||||
|
|
||||||
|
|
||||||
|
class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
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.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = NullableForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||||
|
|
||||||
|
class HyperlinkedManyToManyTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.relations_hyperlink'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
for idx in range(1, 4):
|
||||||
|
target = ManyToManyTarget(name='target-%d' % idx)
|
||||||
|
target.save()
|
||||||
|
source = ManyToManySource(name='source-%d' % idx)
|
||||||
|
source.save()
|
||||||
|
for target in ManyToManyTarget.objects.all():
|
||||||
|
source.targets.add(target)
|
||||||
|
|
||||||
|
def test_many_to_many_retrieve(self):
|
||||||
|
queryset = ManyToManySource.objects.all()
|
||||||
|
serializer = ManyToManySourceSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
|
||||||
|
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
|
||||||
|
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_many_to_many_retrieve(self):
|
||||||
|
queryset = ManyToManyTarget.objects.all()
|
||||||
|
serializer = ManyToManyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_many_to_many_update(self):
|
||||||
|
data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
|
||||||
|
instance = ManyToManySource.objects.get(pk=1)
|
||||||
|
serializer = ManyToManySourceSerializer(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 = ManyToManySource.objects.all()
|
||||||
|
serializer = ManyToManySourceSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
|
||||||
|
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
|
||||||
|
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_many_to_many_update(self):
|
||||||
|
data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}
|
||||||
|
instance = ManyToManyTarget.objects.get(pk=1)
|
||||||
|
serializer = ManyToManyTargetSerializer(instance, data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Ensure target 1 is updated, and everything else is as expected
|
||||||
|
queryset = ManyToManyTarget.objects.all()
|
||||||
|
serializer = ManyToManyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']},
|
||||||
|
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
|
||||||
|
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_many_to_many_create(self):
|
||||||
|
data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/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 = [
|
||||||
|
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
|
||||||
|
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
|
||||||
|
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
|
||||||
|
{'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_many_to_many_create(self):
|
||||||
|
data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
|
||||||
|
serializer = ManyToManyTargetSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
self.assertEqual(obj.name, u'target-4')
|
||||||
|
|
||||||
|
# Ensure target 4 is added, and everything else is as expected
|
||||||
|
queryset = ManyToManyTarget.objects.all()
|
||||||
|
serializer = ManyToManyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']},
|
||||||
|
{'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class HyperlinkedForeignKeyTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.relations_hyperlink'
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_foreign_key_retrieve(self):
|
||||||
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
serializer = ForeignKeyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
|
||||||
|
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_update(self):
|
||||||
|
data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/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 = [
|
||||||
|
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'},
|
||||||
|
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_foreign_key_update(self):
|
||||||
|
data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
|
||||||
|
instance = ForeignKeyTarget.objects.get(pk=2)
|
||||||
|
serializer = ForeignKeyTargetSerializer(instance, data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Ensure target 2 is update, and everything else is as expected
|
||||||
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
serializer = ForeignKeyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
|
||||||
|
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_create(self):
|
||||||
|
data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'}
|
||||||
|
serializer = ForeignKeySourceSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
self.assertEqual(obj.name, u'source-4')
|
||||||
|
|
||||||
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
|
queryset = ForeignKeySource.objects.all()
|
||||||
|
serializer = ForeignKeySourceSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_foreign_key_create(self):
|
||||||
|
data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/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 4 is added, and everything else is as expected
|
||||||
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
serializer = ForeignKeyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
|
||||||
|
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
|
||||||
|
{'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_update_with_invalid_null(self):
|
||||||
|
data = {'url': '/foreignkeysource/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 HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.relations_hyperlink'
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_create_with_valid_null(self):
|
||||||
|
data = {'url': '/nullableforeignkeysource/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 = [
|
||||||
|
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
|
||||||
|
{'url': '/nullableforeignkeysource/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 = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''}
|
||||||
|
expected_data = {'url': '/nullableforeignkeysource/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 = [
|
||||||
|
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
|
||||||
|
{'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_update_with_valid_null(self):
|
||||||
|
data = {'url': '/nullableforeignkeysource/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 = [
|
||||||
|
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
|
||||||
|
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/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 = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''}
|
||||||
|
expected_data = {'url': '/nullableforeignkeysource/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 = [
|
||||||
|
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
|
||||||
|
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
|
||||||
|
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
# reverse foreign keys MUST be read_only
|
||||||
|
# In the general case they do not provide .remove() or .clear()
|
||||||
|
# and cannot be arbitrarily set.
|
||||||
|
|
||||||
|
# def test_reverse_foreign_key_update(self):
|
||||||
|
# data = {'id': 1, 'name': u'target-1', 'sources': [1]}
|
||||||
|
# instance = ForeignKeyTarget.objects.get(pk=1)
|
||||||
|
# serializer = ForeignKeyTargetSerializer(instance, data=data)
|
||||||
|
# self.assertTrue(serializer.is_valid())
|
||||||
|
# self.assertEquals(serializer.data, data)
|
||||||
|
# serializer.save()
|
||||||
|
|
||||||
|
# # Ensure target 1 is updated, and everything else is as expected
|
||||||
|
# queryset = ForeignKeyTarget.objects.all()
|
||||||
|
# serializer = ForeignKeyTargetSerializer(queryset)
|
||||||
|
# expected = [
|
||||||
|
# {'id': 1, 'name': u'target-1', 'sources': [1]},
|
||||||
|
# {'id': 2, 'name': u'target-2', 'sources': []},
|
||||||
|
# ]
|
||||||
|
# self.assertEquals(serializer.data, expected)
|
102
rest_framework/tests/relations_nested.py
Normal file
102
rest_framework/tests/relations_nested.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
depth = 1
|
||||||
|
model = ForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
class FlatForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||||
|
sources = FlatForeignKeySourceSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
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 ReverseForeignKeyTests(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': {'id': 1, 'name': u'target-1'}},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'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': [
|
||||||
|
{'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': 2, 'name': u'target-2', 'sources': [
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedNullableForeignKeyTests(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': {'id': 1, 'name': u'target-1'}},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': None},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
|
@ -38,7 +38,7 @@ class ForeignKeySource(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||||
sources = serializers.ManyPrimaryKeyRelatedField(read_only=True)
|
sources = serializers.ManyPrimaryKeyRelatedField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ForeignKeyTarget
|
model = ForeignKeyTarget
|
||||||
|
@ -216,6 +216,60 @@ class PKForeignKeyTests(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_foreign_key_update(self):
|
||||||
|
data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]}
|
||||||
|
instance = ForeignKeyTarget.objects.get(pk=2)
|
||||||
|
serializer = ForeignKeyTargetSerializer(instance, data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# 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': [2]},
|
||||||
|
{'id': 2, 'name': u'target-2', 'sources': [1, 3]},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_foreign_key_create(self):
|
||||||
|
data = {'id': 4, 'name': u'source-4', 'target': 2}
|
||||||
|
serializer = ForeignKeySourceSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEquals(serializer.data, data)
|
||||||
|
self.assertEqual(obj.name, u'source-4')
|
||||||
|
|
||||||
|
# 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': 1},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': 1},
|
||||||
|
{'id': 4, 'name': u'source-4', 'target': 2},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_reverse_foreign_key_create(self):
|
||||||
|
data = {'id': 3, 'name': u'target-3', 'sources': [1, 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 4 is added, and everything else is as expected
|
||||||
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
serializer = ForeignKeyTargetSerializer(queryset)
|
||||||
|
expected = [
|
||||||
|
{'id': 1, 'name': u'target-1', 'sources': [2]},
|
||||||
|
{'id': 2, 'name': u'target-2', 'sources': []},
|
||||||
|
{'id': 3, 'name': u'target-3', 'sources': [1, 3]},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
def test_foreign_key_update_with_invalid_null(self):
|
def test_foreign_key_update_with_invalid_null(self):
|
||||||
data = {'id': 1, 'name': u'source-1', 'target': None}
|
data = {'id': 1, 'name': u'source-1', 'target': None}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
|
@ -229,9 +283,21 @@ class PKNullableForeignKeyTests(TestCase):
|
||||||
target = ForeignKeyTarget(name='target-1')
|
target = ForeignKeyTarget(name='target-1')
|
||||||
target.save()
|
target.save()
|
||||||
for idx in range(1, 4):
|
for idx in range(1, 4):
|
||||||
|
if idx == 3:
|
||||||
|
target = None
|
||||||
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
|
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
|
||||||
source.save()
|
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': 1},
|
||||||
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
|
{'id': 3, 'name': u'source-3', 'target': None},
|
||||||
|
]
|
||||||
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
def test_foreign_key_create_with_valid_null(self):
|
def test_foreign_key_create_with_valid_null(self):
|
||||||
data = {'id': 4, 'name': u'source-4', 'target': None}
|
data = {'id': 4, 'name': u'source-4', 'target': None}
|
||||||
serializer = NullableForeignKeySourceSerializer(data=data)
|
serializer = NullableForeignKeySourceSerializer(data=data)
|
||||||
|
@ -246,7 +312,7 @@ class PKNullableForeignKeyTests(TestCase):
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': u'source-1', 'target': 1},
|
{'id': 1, 'name': u'source-1', 'target': 1},
|
||||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
{'id': 3, 'name': u'source-3', 'target': 1},
|
{'id': 3, 'name': u'source-3', 'target': None},
|
||||||
{'id': 4, 'name': u'source-4', 'target': None}
|
{'id': 4, 'name': u'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
@ -270,7 +336,7 @@ class PKNullableForeignKeyTests(TestCase):
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': u'source-1', 'target': 1},
|
{'id': 1, 'name': u'source-1', 'target': 1},
|
||||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
{'id': 3, 'name': u'source-3', 'target': 1},
|
{'id': 3, 'name': u'source-3', 'target': None},
|
||||||
{'id': 4, 'name': u'source-4', 'target': None}
|
{'id': 4, 'name': u'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
@ -289,7 +355,7 @@ class PKNullableForeignKeyTests(TestCase):
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': u'source-1', 'target': None},
|
{'id': 1, 'name': u'source-1', 'target': None},
|
||||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
{'id': 3, 'name': u'source-3', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
||||||
|
@ -312,7 +378,7 @@ class PKNullableForeignKeyTests(TestCase):
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': u'source-1', 'target': None},
|
{'id': 1, 'name': u'source-1', 'target': None},
|
||||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
{'id': 3, 'name': u'source-3', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEquals(serializer.data, expected)
|
self.assertEquals(serializer.data, expected)
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from rest_framework import status, permissions
|
from rest_framework import status, permissions
|
||||||
from rest_framework.compat import yaml
|
from rest_framework.compat import yaml, patterns, url, include
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
"""
|
"""
|
||||||
Tests for content parsing, and form-overloaded content parsing.
|
Tests for content parsing, and form-overloaded content parsing.
|
||||||
"""
|
"""
|
||||||
from django.conf.urls.defaults import patterns
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
from django.test.client import RequestFactory
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
from django.test.client import RequestFactory
|
from rest_framework.compat import patterns
|
||||||
from rest_framework.parsers import (
|
from rest_framework.parsers import (
|
||||||
BaseParser,
|
BaseParser,
|
||||||
FormParser,
|
FormParser,
|
||||||
|
@ -304,3 +303,11 @@ class TestUserSetter(TestCase):
|
||||||
self.assertFalse(self.request.user.is_anonymous())
|
self.assertFalse(self.request.user.is_anonymous())
|
||||||
logout(self.request)
|
logout(self.request)
|
||||||
self.assertTrue(self.request.user.is_anonymous())
|
self.assertTrue(self.request.user.is_anonymous())
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthSetter(TestCase):
|
||||||
|
|
||||||
|
def test_auth_can_be_set(self):
|
||||||
|
request = Request(factory.get('/'))
|
||||||
|
request.auth = 'DUMMY'
|
||||||
|
self.assertEqual(request.auth, 'DUMMY')
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import unittest
|
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from rest_framework.compat import patterns, url, include
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 rest_framework.compat import patterns, url
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
import pickle
|
import pickle
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel,
|
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||||
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
|
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
||||||
|
|
||||||
|
@ -69,6 +69,11 @@ class AlbumsSerializer(serializers.ModelSerializer):
|
||||||
model = Album
|
model = Album
|
||||||
fields = ['title'] # lists are also valid options
|
fields = ['title'] # lists are also valid options
|
||||||
|
|
||||||
|
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = HasPositiveIntegerAsChoice
|
||||||
|
fields = ['some_integer']
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(TestCase):
|
class BasicTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -285,6 +290,12 @@ 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 PositiveIntegerAsChoiceTests(TestCase):
|
||||||
|
def test_positive_integer_in_json_is_correctly_parsed(self):
|
||||||
|
data = {'some_integer':1}
|
||||||
|
serializer = PositiveIntegerAsChoiceSerializer(data=data)
|
||||||
|
self.assertEquals(serializer.is_valid(), True)
|
||||||
|
|
||||||
class ModelValidationTests(TestCase):
|
class ModelValidationTests(TestCase):
|
||||||
def test_validate_unique(self):
|
def test_validate_unique(self):
|
||||||
"""
|
"""
|
||||||
|
@ -297,6 +308,38 @@ class ModelValidationTests(TestCase):
|
||||||
self.assertFalse(second_serializer.is_valid())
|
self.assertFalse(second_serializer.is_valid())
|
||||||
self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
|
self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
|
||||||
|
|
||||||
|
def test_foreign_key_with_partial(self):
|
||||||
|
"""
|
||||||
|
Test ModelSerializer validation with partial=True
|
||||||
|
|
||||||
|
Specifically test foreign key validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
album = Album(title='test')
|
||||||
|
album.save()
|
||||||
|
|
||||||
|
class PhotoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Photo
|
||||||
|
|
||||||
|
photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
|
||||||
|
self.assertTrue(photo_serializer.is_valid())
|
||||||
|
photo = photo_serializer.save()
|
||||||
|
|
||||||
|
# Updating only the album (foreign key)
|
||||||
|
photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
|
||||||
|
self.assertTrue(photo_serializer.is_valid())
|
||||||
|
self.assertTrue(photo_serializer.save())
|
||||||
|
|
||||||
|
# Updating only the description
|
||||||
|
photo_serializer = PhotoSerializer(instance=photo,
|
||||||
|
data={'description': 'new'},
|
||||||
|
partial=True)
|
||||||
|
|
||||||
|
self.assertTrue(photo_serializer.is_valid())
|
||||||
|
self.assertTrue(photo_serializer.save())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RegexValidationTest(TestCase):
|
class RegexValidationTest(TestCase):
|
||||||
def test_create_failed(self):
|
def test_create_failed(self):
|
||||||
|
@ -688,6 +731,10 @@ class BlankFieldTests(TestCase):
|
||||||
serializer = self.model_serializer_class(data=self.data)
|
serializer = self.model_serializer_class(data=self.data)
|
||||||
self.assertEquals(serializer.is_valid(), True)
|
self.assertEquals(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
def test_create_model_null_field(self):
|
||||||
|
serializer = self.model_serializer_class(data={'title': None})
|
||||||
|
self.assertEquals(serializer.is_valid(), True)
|
||||||
|
|
||||||
def test_create_not_blank_field(self):
|
def test_create_not_blank_field(self):
|
||||||
"""
|
"""
|
||||||
Test to ensure blank data in a field not marked as blank=True
|
Test to ensure blank data in a field not marked as blank=True
|
||||||
|
@ -704,6 +751,10 @@ class BlankFieldTests(TestCase):
|
||||||
serializer = self.not_blank_model_serializer_class(data=self.data)
|
serializer = self.not_blank_model_serializer_class(data=self.data)
|
||||||
self.assertEquals(serializer.is_valid(), False)
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
|
|
||||||
|
def test_create_model_null_field(self):
|
||||||
|
serializer = self.model_serializer_class(data={})
|
||||||
|
self.assertEquals(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
|
||||||
#test for issue #460
|
#test for issue #460
|
||||||
class SerializerPickleTests(TestCase):
|
class SerializerPickleTests(TestCase):
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
NO_SETTING = ('!', None)
|
NO_SETTING = ('!', None)
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsManager(object):
|
class TestSettingsManager(object):
|
||||||
"""
|
"""
|
||||||
A class which can modify some Django settings temporarily for a
|
A class which can modify some Django settings temporarily for a
|
||||||
|
@ -57,6 +58,7 @@ class SettingsTestCase(TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.settings_manager.revert()
|
self.settings_manager.revert()
|
||||||
|
|
||||||
|
|
||||||
class TestModelsTestCase(SettingsTestCase):
|
class TestModelsTestCase(SettingsTestCase):
|
||||||
def setUp(self, *args, **kwargs):
|
def setUp(self, *args, **kwargs):
|
||||||
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
|
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.conf.urls.defaults import url
|
from rest_framework.compat import url
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`.
|
||||||
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
from django.conf.urls.defaults import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
|
|
||||||
|
|
||||||
template_name = {'template_name': 'rest_framework/login.html'}
|
template_name = {'template_name': 'rest_framework/login.html'}
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -12,12 +12,12 @@ deps = https://github.com/django/django/zipball/master
|
||||||
|
|
||||||
[testenv:py2.7-django1.4]
|
[testenv:py2.7-django1.4]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.4.1
|
deps = django==1.4.3
|
||||||
django-filter==0.5.4
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.7-django1.3]
|
[testenv:py2.7-django1.3]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.3.3
|
deps = django==1.3.5
|
||||||
django-filter==0.5.4
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.6-django1.5]
|
[testenv:py2.6-django1.5]
|
||||||
|
@ -27,10 +27,10 @@ deps = https://github.com/django/django/zipball/master
|
||||||
|
|
||||||
[testenv:py2.6-django1.4]
|
[testenv:py2.6-django1.4]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.4.1
|
deps = django==1.4.3
|
||||||
django-filter==0.5.4
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.6-django1.3]
|
[testenv:py2.6-django1.3]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.3.3
|
deps = django==1.3.5
|
||||||
django-filter==0.5.4
|
django-filter==0.5.4
|
||||||
|
|
Loading…
Reference in New Issue
Block a user