Merge remote-tracking branch 'reference/py3k' into p3k

This commit is contained in:
Xavier Ordoquy 2013-01-02 16:09:21 +01:00
commit 737349d238
56 changed files with 2570 additions and 780 deletions

5
.gitignore vendored
View File

@ -10,5 +10,10 @@ dist/
*.egg-info/
MANIFEST
bin/
include/
lib/
local/
!.gitignore
!.travis.yml

View File

@ -6,9 +6,10 @@ python:
- "3.2"
env:
- DJANGO=https://www.djangoproject.com/download/1.5b1/tarball/
- DJANGO=django==1.4.2 --use-mirrors
- DJANGO=django==1.3.3 --use-mirrors
- DJANGO=https://github.com/django/django/zipball/master
- DJANGO=https://www.djangoproject.com/download/1.5b2/tarball/
- DJANGO=django==1.4.3 --use-mirrors
- DJANGO=django==1.3.5 --use-mirrors
install:
- pip install $DJANGO

View File

@ -2,7 +2,9 @@
**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]
@ -37,14 +39,35 @@ There is also a sandbox API you can use for testing purposes, [available here][s
# Installation
Install using `pip`...
Install using `pip`, including any optional packages you want...
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.
git clone git@github.com:tomchristie/django-rest-framework.git
cd django-rest-framework
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
@ -58,6 +81,73 @@ To run the tests.
# Changelog
### 2.1.14
**Date**: 31st Dec 2012
* 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.
**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`.
This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`.
### 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
**Date**: 11th Dec 2012
* Bugfix: Fix broken nested serialization.
* Bugfix: Fix `Meta.fields` only working as tuple not as list.
* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field.
## 2.1.8
**Date**: 8th Dec 2012
* Fix for creating nullable Foreign Keys with `''` as well as `None`.
* Added `null=<bool>` related field option.
## 2.1.7
**Date**: 7th Dec 2012
* Serializers now properly support nullable Foreign Keys.
* Serializer validation now includes model field validation, such as uniqueness constraints.
* Support 'true' and 'false' string values for BooleanField.
* Added pickle support for serialized data.
* Support `source='dotted.notation'` style for nested serializers.
* Make `Request.user` settable.
* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`
## 2.1.6
**Date**: 23rd Nov 2012
@ -171,6 +261,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
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html

View File

@ -2,11 +2,11 @@
# Serializer fields
> Flat is better than nested.
> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it -- normalizing it to a consistent format.
>
> &mdash; [The Zen of Python][cite]
> &mdash; [Django documentation][cite]
Serializer fields handle converting between primative values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects.
Serializer fields handle converting between primitive values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects.
---
@ -28,7 +28,7 @@ Defaults to the name of the field.
### `read_only`
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance dureing deserialization.
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
Defaults to `False`
@ -41,7 +41,7 @@ Defaults to `True`.
### `default`
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behaviour is to not populate the attribute at all.
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
### `validators`
@ -96,9 +96,9 @@ Would produce output similar to:
'expired': True
}
By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary.
By default, the `Field` class will perform a basic translation of the source value into primitive datatypes, falling back to unicode representations of complex datatypes when necessary.
You can customize this behaviour by overriding the `.to_native(self, value)` method.
You can customize this behavior by overriding the `.to_native(self, value)` method.
## WritableField
@ -110,6 +110,24 @@ A generic field that can be tied to any arbitrary model field. The `ModelField`
**Signature:** `ModelField(model_field=<Django ModelField class>)`
## SerializerMethodField
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
from rest_framework import serializers
from django.contrib.auth.models import User
from django.utils.timezone import now
class UserSerializer(serializers.ModelSerializer):
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
class Meta:
model = User
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
---
# Typed Fields
@ -211,148 +229,8 @@ Signature and validation is the same as with `FileField`.
---
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads.
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
---
# Relational Fields
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
## RelatedField
This field can be applied to any of the following:
* A `ForeignKey` field.
* A `OneToOneField` field.
* A reverse OneToOne relationship
* Any other "to-one" relationship.
By default `RelatedField` will represent the target of the field using it's `__unicode__` method.
You can customise this behaviour by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method.
## ManyRelatedField
This field can be applied to any of the following:
* A `ManyToManyField` field.
* A reverse ManyToMany relationship.
* A reverse ForeignKey relationship
* Any other "to-many" relationship.
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method.
For example, given the following models:
class TaggedItem(models.Model):
"""
Tags arbitrary model instances using a generic relation.
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
return self.tag
class Bookmark(models.Model):
"""
A bookmark consists of a URL, and 0 or more descriptive tags.
"""
url = models.URLField()
tags = GenericRelation(TaggedItem)
And a model serializer defined like this:
class BookmarkSerializer(serializers.ModelSerializer):
tags = serializers.ManyRelatedField(source='tags')
class Meta:
model = Bookmark
exclude = ('id',)
Then an example output format for a Bookmark instance would be:
{
'tags': [u'django', u'python'],
'url': u'https://www.djangoproject.com/'
}
## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
By default these fields are read-write, although you can change this behaviour using the `read_only` flag.
**Arguments**:
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
## SlugRelatedField / ManySlugRelatedField
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
By default these fields read-write, although you can change this behaviour using the `read_only` flag.
**Arguments**:
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
By default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
This field is always read-only.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
# Other Fields
## SerializerMethodField
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
from rest_framework import serializers
from django.contrib.auth.models import User
from django.utils.timezone import now
class UserSerializer(serializers.ModelSerializer):
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
class Meta:
model = User
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
[cite]: http://www.python.org/dev/peps/pep-0020/
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS

View File

@ -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`.
* `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'`.
---

View File

@ -53,7 +53,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
Or, if you're using the `@api_view` decorator with function based views.
@api_view('GET')
@permission_classes(IsAuthenticated)
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
content = {
'status': 'request was permitted'

139
docs/api-guide/relations.md Normal file
View File

@ -0,0 +1,139 @@
<a class="github" href="relations.py"></a>
# Serializer relations
> Bad programmers worry about the code.
> Good programmers worry about data structures and their relationships.
>
> &mdash; [Linus Torvalds][cite]
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
---
**Note:** The relational fields are declared in `relations.py`, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
---
## RelatedField
This field can be applied to any of the following:
* A `ForeignKey` field.
* A `OneToOneField` field.
* A reverse OneToOne relationship
* Any other "to-one" relationship.
By default `RelatedField` will represent the target of the field using it's `__unicode__` method.
You can customize this behavior by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method.
## ManyRelatedField
This field can be applied to any of the following:
* A `ManyToManyField` field.
* A reverse ManyToMany relationship.
* A reverse ForeignKey relationship
* Any other "to-many" relationship.
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method.
For example, given the following models:
class TaggedItem(models.Model):
"""
Tags arbitrary model instances using a generic relation.
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
return self.tag
class Bookmark(models.Model):
"""
A bookmark consists of a URL, and 0 or more descriptive tags.
"""
url = models.URLField()
tags = GenericRelation(TaggedItem)
And a model serializer defined like this:
class BookmarkSerializer(serializers.ModelSerializer):
tags = serializers.ManyRelatedField(source='tags')
class Meta:
model = Bookmark
exclude = ('id',)
Then an example output format for a Bookmark instance would be:
{
'tags': [u'django', u'python'],
'url': u'https://www.djangoproject.com/'
}
## PrimaryKeyRelatedField
## ManyPrimaryKeyRelatedField
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
By default these fields are read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## SlugRelatedField
## ManySlugRelatedField
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
By default these fields read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## HyperlinkedRelatedField
## ManyHyperlinkedRelatedField
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
By default, `HyperlinkedRelatedField` is read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
This field is always read-only.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
[cite]: http://lwn.net/Articles/193245/

View File

@ -4,8 +4,7 @@
> Expanding the usefulness of the serializers is something that we would
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
area would be gratefully accepted.
will take some serious design work.
>
> &mdash; 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
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

View File

@ -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.
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].
@ -52,21 +52,21 @@ Install using `pip`, including any optional packages you want...
pip install -r requirements.txt
pip install -r optionals.txt
Add `rest_framework` to your `INSTALLED_APPS`.
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
'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('',
...
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
@ -94,6 +94,7 @@ The API guide is your complete reference manual to all the functionality provide
* [Renderers][renderers]
* [Serializers][serializers]
* [Serializer fields][fields]
* [Serializer relations][relations]
* [Authentication][authentication]
* [Permissions][permissions]
* [Throttling][throttling]
@ -185,6 +186,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[renderers]: api-guide/renderers.md
[serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md
[relations]: api-guide/relations.md
[authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md

View File

@ -72,6 +72,7 @@
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
<li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li>
<li><a href="{{ base_url }}/api-guide/relations{{ suffix }}">Serializer relations</a></li>
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>

View File

@ -69,6 +69,21 @@ The following people have helped make REST framework great.
* Olivier Aubert - [oaubert]
* Yuri Prezument - [yprez]
* Fabian Buechler - [fabianbuechler]
* Mark Hughes - [mhsparks]
* Michael van de Waeter - [mvdwaeter]
* Reinout van Rees - [reinout]
* Michael Richards - [justanotherbody]
* Ben Roberts - [roberts81]
* Venkata Subramanian Mahalingam - [annacoder]
* George Kappel - [gkappel]
* Colin Murtaugh - [cmurtaugh]
* Simon Pantzare - [pilt]
* Szymon Teżewski - [sunscrapers]
* 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.
@ -88,10 +103,9 @@ Development of REST framework 2.0 was sponsored by [DabApps].
## Contact
To contact the author directly:
For usage questions please see the [REST framework discussion group][group].
* twitter: [@_tomchristie][twitter]
* email: [tom@tomchristie.com][email]
You can also contact [@_tomchristie][twitter] directly on twitter.
[email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie
@ -105,6 +119,7 @@ To contact the author directly:
[dabapps]: http://lab.dabapps.com
[sandbox]: http://restframework.herokuapp.com/
[heroku]: http://www.heroku.com/
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[tomchristie]: https://github.com/tomchristie
[markotibold]: https://github.com/markotibold
@ -173,3 +188,18 @@ To contact the author directly:
[oaubert]: https://github.com/oaubert
[yprez]: https://github.com/yprez
[fabianbuechler]: https://github.com/fabianbuechler
[mhsparks]: https://github.com/mhsparks
[mvdwaeter]: https://github.com/mvdwaeter
[reinout]: https://github.com/reinout
[justanotherbody]: https://github.com/justanotherbody
[roberts81]: https://github.com/roberts81
[annacoder]: https://github.com/annacoder
[gkappel]: https://github.com/gkappel
[cmurtaugh]: https://github.com/cmurtaugh
[pilt]: https://github.com/pilt
[sunscrapers]: https://github.com/sunscrapers
[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

View File

@ -4,19 +4,103 @@
>
> &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite].
## 2.1.6
## 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
### Master
* Relation changes are no longer persisted in `.restore_object`
### 2.1.14
**Date**: 31st Dec 2012
* 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.
**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`.
This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`.
### 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
**Date**: 11th Dec 2012
* Bugfix: Fix broken nested serialization.
* Bugfix: Fix `Meta.fields` only working as tuple not as list.
* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field.
### 2.1.8
**Date**: 8th Dec 2012
* Fix for creating nullable Foreign Keys with `''` as well as `None`.
* Added `null=<bool>` related field option.
### 2.1.7
**Date**: 7th Dec 2012
* Serializers now properly support nullable Foreign Keys.
* Serializer validation now includes model field validation, such as uniqueness constraints.
* Support 'true' and 'false' string values for BooleanField.
* Added pickle support for serialized data.
* Support `source='dotted.notation'` style for nested serializers.
* Make `Request.user` settable.
* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`.
### 2.1.6
**Date**: 23rd Nov 2012
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
## 2.1.5
### 2.1.5
**Date**: 23rd Nov 2012
* Bugfix: Fix DjangoModelPermissions.
## 2.1.4
### 2.1.4
**Date**: 22nd Nov 2012
@ -27,7 +111,7 @@
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
## 2.1.3
### 2.1.3
**Date**: 16th Nov 2012
@ -38,29 +122,27 @@
* 201 Responses now return a 'Location' header.
* Bugfix: Serializer fields now respect `max_length`.
## 2.1.2
### 2.1.2
**Date**: 9th Nov 2012
* **Filtering support.**
* Bugfix: Support creation of objects with reverse M2M relations.
## 2.1.1
### 2.1.1
**Date**: 7th Nov 2012
* Support use of HTML exception templates. Eg. `403.html`
* 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.
* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
## 2.1.0
### 2.1.0
**Date**: 5th Nov 2012
**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.**
* `queryset` argument is now optional on writable model fields.
* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
@ -69,13 +151,19 @@
* Bugfix: Support choice field in Browseable API.
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
## 2.0.2
**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.2
**Date**: 2nd Nov 2012
* Fix issues with pk related fields in the browsable API.
## 2.0.1
### 2.0.1
**Date**: 1st Nov 2012
@ -83,7 +171,7 @@
* Added SlugRelatedField and ManySlugRelatedField.
* If PUT creates an instance return '201 Created', instead of '200 OK'.
## 2.0.0
### 2.0.0
**Date**: 30th Oct 2012
@ -92,34 +180,40 @@
---
## 0.4.0
## 0.4.x series
### 0.4.0
* Supports Django 1.5.
* Fixes issues with 'HEAD' method.
* Allow views to specify template used by TemplateRenderer
* More consistent error responses
* Some serializer fixes
* Fix internet explorer ajax behaviour
* Fix internet explorer ajax behavior
* 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
## 0.3.3
---
## 0.3.x series
### 0.3.3
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* 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.
- Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit.
* Saner template variable autoescaping.
* Tider setup.py
- Too magical. Explicit is better than implicit.
* Saner template variable auto-escaping.
* Tidier setup.py
* Updated for URLObject 2.0
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
## 0.3.2
### 0.3.2
* Bugfixes:
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
@ -131,37 +225,41 @@
* get_name, get_description become methods on the view - makes them overridable.
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
## 0.3.1
### 0.3.1
* [not documented]
## 0.3.0
### 0.3.0
* JSONP Support
* Bugfixes, including support for latest markdown release
## 0.2.4
---
## 0.2.x series
### 0.2.4
* Fix broken IsAdminUser permission.
* OPTIONS support.
* XMLParser.
* Drop mentions of Blog, BitBucket.
## 0.2.3
### 0.2.3
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
## 0.2.2
### 0.2.2
* Throttling support complete.
## 0.2.1
### 0.2.1
* Couple of simple bugfixes over 0.2.0
## 0.2.0
### 0.2.0
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
@ -185,14 +283,20 @@
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
You can reuse these mixin classes individually without using the ``View`` class.
## 0.1.1
---
## 0.1.x series
### 0.1.1
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
## 0.1.0
### 0.1.0
* Initial release.
[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
[announcement]: rest-framework-2-announcement.md

View File

@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components
## What REST framework doesn't provide.
What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
[cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
@ -44,6 +44,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
[maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html
[hal]: http://stateless.co/hal_specification.html
[collection]: http://www.amundsen.com/media-types/collection/
[microformats]: http://microformats.org/wiki/Main_Page
[serialization]: ../api-guide/serializers.md

View File

@ -14,7 +14,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
## Setting up a new environment
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on.
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
:::bash
mkdir ~/env
@ -39,7 +39,6 @@ To get started, let's create a new project to work with.
cd tutorial
Once that's done we can create an app that we'll use to create a simple Web API.
We're going to create a project that
python manage.py startapp snippets
@ -64,7 +63,7 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
'snippets'
)
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views.
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
urlpatterns = patterns('',
url(r'^', include('snippets.urls')),
@ -105,7 +104,7 @@ Don't forget to sync the database for the first time.
## Creating a Serializer class
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
from django.forms import widgets
from rest_framework import serializers
@ -146,7 +145,7 @@ We can actually also save ourselves some time by using the `ModelSerializer` cla
## Working with Serializers
Before we go any further we'll familiarise ourselves with using our new Serializer class. Let's drop into the Django shell.
Before we go any further we'll familiarize ourselves with using our new Serializer class. Let's drop into the Django shell.
python manage.py shell
@ -166,7 +165,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial
serializer.data
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
At this point we've translated the model instance into python native datatypes. To finalize the serialization process we render the data into `json`.
content = JSONRenderer().render(serializer.data)
content
@ -292,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
)
It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
## Testing our first attempt at a Web API
@ -304,7 +303,7 @@ It's worth noting that there's a couple of edge cases we're not dealing with pro
We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views.
Our API views don't do anything particularly special at the moment, beyond serve `json` responses, and there's some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
Our API views don't do anything particularly special at the moment, beyond serving `json` responses, and there are some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].

View File

@ -66,6 +66,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
Here is the view for an individual snippet.
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
@ -92,7 +94,7 @@ Our instance view is an improvement over the previous example. It's a little mo
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
This should all feel very familiar - there's not a lot different to working with regular Django views.
This should all feel very familiar - it is not a lot different from working with regular Django views.
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
@ -128,7 +130,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
**TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]."
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
### Browsability

View File

@ -102,14 +102,14 @@ Let's take a look at how we can compose our views by using the mixin classes.
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
We'll take a moment to examine exactly what's happening here. We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
class SnippetDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.SingleObjectBaseView):
generics.SingleObjectAPIView):
model = Snippet
serializer_class = SnippetSerializer
@ -122,7 +122,7 @@ The base class provides the core functionality, and the mixin classes provide th
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
Pretty similar. This time we're using the `SingleObjectBaseView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
Pretty similar. This time we're using the `SingleObjectAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
## Using generic class based views
@ -142,7 +142,7 @@ Using the mixin classes we've rewritten the views to use slightly less code than
model = Snippet
serializer_class = SnippetSerializer
Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django.
Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django.
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API.

View File

@ -61,7 +61,7 @@ Now that we've got some users to work with, we'd better add representations of t
model = User
fields = ('id', 'username', 'snippets')
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it.
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
@ -92,9 +92,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin
## Updating our serializer
Now that snippets are associated with the user that created them, let's update our SnippetSerializer to reflect that.
Add the following field to the serializer definition:
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
owner = serializers.Field(source='owner.username')
@ -108,7 +106,7 @@ The field we've added is the untyped `Field` class, in contrast to the other typ
## Adding required permissions to views
Now that code snippets are associated with users we want to make sure that only authenticated users are able to create, update and delete code snippets.
Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update and delete code snippets.
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.

View File

@ -25,7 +25,7 @@ Notice that we're using REST framework's `reverse` function in order to return f
The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
@ -151,7 +151,7 @@ We could also customize the pagination style if we needed too, but in this case
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the hightlighted code HTML representations.
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations.
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
@ -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:
* 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.
* Follow the author [on Twitter][twitter] and say hi.
* [Follow the author on Twitter][twitter] and say hi.
**Now go build awesome things.**

View File

@ -137,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
'PAGINATE_BY': 10
}
Okay, that's us done.
Okay, we're done.
---

View File

@ -1,3 +1,3 @@
__version__ = '2.1.6'
__version__ = '2.1.14'
VERSION = __version__ # synonym

View File

@ -1,6 +1,7 @@
from django.contrib.auth import authenticate
from rest_framework import serializers
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
@ -21,4 +22,3 @@ class AuthTokenSerializer(serializers.Serializer):
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')

View File

@ -6,6 +6,7 @@ from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.serializers import AuthTokenSerializer
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
@ -18,7 +19,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view()

View File

@ -8,6 +8,12 @@ import six
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
try:
import django_filters
@ -24,6 +30,16 @@ except ImportError:
from six import BytesIO
# Try to import PIL in either of the two ways it can end up installed.
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
Image = None
def get_concrete_model(model_cls):
try:
return model_cls._meta.concrete_model

View File

@ -9,19 +9,16 @@ import re
import warnings
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.core.exceptions import ValidationError
from django.conf import settings
from django import forms
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type
try:
from django.utils.encoding import smart_text
except ImportError:
from django.utils.encoding import smart_unicode as smart_text
from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse
from rest_framework.compat import parse_date, parse_datetime
from rest_framework.compat import timezone
from rest_framework.compat import BytesIO
@ -42,6 +39,7 @@ def is_simple_callable(obj):
class Field(object):
read_only = True
creation_counter = 0
empty = ''
type_name = None
@ -143,7 +141,7 @@ class WritableField(Field):
if required is None:
self.required = not(read_only)
else:
assert not read_only, "Cannot set required=True and read_only=True"
assert not (read_only and required), "Cannot set required=True and read_only=True"
self.required = required
messages = {}
@ -261,430 +259,6 @@ class ModelField(WritableField):
"type": self.model_field.get_internal_type()
}
##### Relational fields #####
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
If not overridden, this represents a to-one relationship, using the unicode
representation of the target.
"""
widget = widgets.Select
cache_choices = False
empty_label = None
default_read_only = True # TODO: Remove this
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
super(RelatedField, self).__init__(*args, **kwargs)
self.read_only = kwargs.pop('read_only', self.default_read_only)
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
try:
manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all()
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
except:
raise
msg = ('Serializer related fields must include a `queryset`' +
' argument or set `read_only=True')
raise Exception(msg)
### We need this stuff to make form choices work...
# def __deepcopy__(self, memo):
# result = super(RelatedField, self).__deepcopy__(memo)
# result.queryset = result.queryset
# return result
def prepare_value(self, obj):
return self.to_native(obj)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return ModelChoiceIterator(self)
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
### Regular serializer stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
value = data.get(field_name)
into[(self.source or field_name)] = self.from_native(value)
class ManyRelatedMixin(object):
"""
Mixin to convert a related field to a many related field.
"""
widget = widgets.SelectMultiple
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return [self.to_native(item) for item in value.all()]
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
try:
# Form data
value = data.getlist(self.source or field_name)
except:
# Non-form data
value = data.get(self.source or field_name)
else:
if value == ['']:
value = []
into[field_name] = [self.from_native(item) for item in value]
class ManyRelatedField(ManyRelatedMixin, RelatedField):
"""
Base class for related model managers.
If not overridden, this represents a to-many relationship, using the unicode
representations of the target, and is read-only.
"""
pass
### PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
Represents a to-one relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.ChoiceField
# TODO: Remove these field hacks...
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
# TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return pk
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
try:
# Prefer obj.serializable_value for performance reasons
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
obj = getattr(obj, self.source or field_name)
return self.to_native(obj.pk)
# Forward relationship
return self.to_native(pk)
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
Represents a to-many relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.MultipleChoiceField
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def to_native(self, pk):
return pk
def field_to_native(self, obj, field_name):
try:
# Prefer obj.serializable_value for performance reasons
queryset = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedManager (reverse relationship)
queryset = getattr(obj, self.source or field_name)
return [self.to_native(item.pk) for item in queryset.all()]
# Forward relationship
return [self.to_native(item.pk) for item in queryset.all()]
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
raise ValidationError(msg)
### Slug relationships
class SlugRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
super(SlugRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
return getattr(obj, self.slug_field)
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
raise ValidationError('Object with %s=%s does not exist.' %
(self.slug_field, unicode(data)))
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
form_field_class = forms.MultipleChoiceField
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a to-one relationship, using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def get_slug_field(self):
"""
Get the name of a slug field to be used to look up by slug.
"""
return self.slug_field
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
pk = getattr(obj, 'pk', None)
if pk is None:
return
kwargs = {self.pk_url_kwarg: pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
slug = getattr(obj, self.slug_field, None)
if not slug:
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
if value.startswith('http:') or value.startswith('https:'):
# If needed convert absolute URLs to relative path
value = urlparse(value).path
prefix = get_script_prefix()
if value.startswith(prefix):
value = '/' + value[len(prefix):]
try:
match = resolve(value)
except:
raise ValidationError('Invalid hyperlink - No URL match')
if match.url_name != self.view_name:
raise ValidationError('Invalid hyperlink - Incorrect URL match')
pk = match.kwargs.get(self.pk_url_kwarg, None)
slug = match.kwargs.get(self.slug_url_kwarg, None)
# Try explicit primary key.
if pk is not None:
queryset = self.queryset.filter(pk=pk)
# Next, try looking up by slug.
elif slug is not None:
slug_field = self.get_slug_field()
queryset = self.queryset.filter(**{slug_field: slug})
# If none of those are defined, it's an error.
else:
raise ValidationError('Invalid hyperlink')
try:
obj = queryset.get()
except ObjectDoesNotExist:
raise ValidationError('Invalid hyperlink - object does not exist.')
return obj
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
form_field_class = forms.MultipleChoiceField
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the
# HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
self.format = kwargs.pop('format', None)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
kwargs = {self.pk_url_kwarg: obj.pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
slug = getattr(obj, self.slug_field, None)
if not slug:
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
##### Typed Fields #####
@ -703,9 +277,9 @@ class BooleanField(WritableField):
default = False
def from_native(self, value):
if value in ('t', 'True', '1'):
if value in ('true', 't', 'True', '1'):
return True
if value in ('f', 'False', '0'):
if value in ('false', 'f', 'False', '0'):
return False
return bool(value)
@ -797,7 +371,7 @@ class ChoiceField(WritableField):
if value == smart_text(k2):
return True
else:
if value == smart_text(k):
if value == smart_text(k) or value == k:
return True
return False
@ -827,6 +401,7 @@ class EmailField(CharField):
class RegexField(CharField):
type_name = 'RegexField'
form_field_class = forms.RegexField
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
@ -1058,11 +633,8 @@ class ImageField(FileField):
if f is None:
return None
# Try to import PIL in either of the two ways it can end up installed.
try:
from PIL import Image
except ImportError:
import Image
from compat import Image
assert Image is not None, 'PIL must be installed for ImageField support'
# We need to get a file object for PIL. We might have a path or we might
# have to read the data into memory.

View File

@ -115,6 +115,10 @@ class UpdateModelMixin(object):
slug_field = self.get_slug_field()
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):
"""
@ -122,6 +126,6 @@ class DestroyModelMixin(object):
Should be mixed in with `SingleObjectBaseView`.
"""
def destroy(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
obj = self.get_object()
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

452
rest_framework/relations.py Normal file
View File

@ -0,0 +1,452 @@
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django import forms
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
try:
from django.utils.encoding import smart_text
except ImportError:
from django.utils.encoding import smart_unicode as smart_text
from rest_framework.fields import Field, WritableField
from rest_framework.reverse import reverse
from urlparse import urlparse
##### Relational fields #####
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
If not overridden, this represents a to-one relationship, using the unicode
representation of the target.
"""
widget = widgets.Select
cache_choices = False
empty_label = None
default_read_only = True # TODO: Remove this
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
self.null = kwargs.pop('null', False)
super(RelatedField, self).__init__(*args, **kwargs)
self.read_only = kwargs.pop('read_only', self.default_read_only)
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
try:
manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all()
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
except:
raise
msg = ('Serializer related fields must include a `queryset`' +
' argument or set `read_only=True')
raise Exception(msg)
### We need this stuff to make form choices work...
# def __deepcopy__(self, memo):
# result = super(RelatedField, self).__deepcopy__(memo)
# result.queryset = result.queryset
# return result
def prepare_value(self, obj):
return self.to_native(obj)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return ModelChoiceIterator(self)
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
### Regular serializer stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
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:
raise ValidationError('Value may not be null')
elif value in (None, '') and self.null:
into[(self.source or field_name)] = None
else:
into[(self.source or field_name)] = self.from_native(value)
class ManyRelatedMixin(object):
"""
Mixin to convert a related field to a many related field.
"""
widget = widgets.SelectMultiple
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return [self.to_native(item) for item in value.all()]
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
try:
# Form data
value = data.getlist(self.source or field_name)
except:
# Non-form data
value = data.get(self.source or field_name)
else:
if value == ['']:
value = []
into[field_name] = [self.from_native(item) for item in value]
class ManyRelatedField(ManyRelatedMixin, RelatedField):
"""
Base class for related model managers.
If not overridden, this represents a to-many relationship, using the unicode
representations of the target, and is read-only.
"""
pass
### PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
Represents a to-one relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.ChoiceField
# TODO: Remove these field hacks...
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
# TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return pk
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
try:
# Prefer obj.serializable_value for performance reasons
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
obj = getattr(obj, self.source or field_name)
return self.to_native(obj.pk)
# Forward relationship
return self.to_native(pk)
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
Represents a to-many relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.MultipleChoiceField
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def to_native(self, pk):
return pk
def field_to_native(self, obj, field_name):
try:
# Prefer obj.serializable_value for performance reasons
queryset = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedManager (reverse relationship)
queryset = getattr(obj, self.source or field_name)
return [self.to_native(item.pk) for item in queryset.all()]
# Forward relationship
return [self.to_native(item.pk) for item in queryset.all()]
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
raise ValidationError(msg)
### Slug relationships
class SlugRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
super(SlugRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
return getattr(obj, self.slug_field)
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
raise ValidationError('Object with %s=%s does not exist.' %
(self.slug_field, unicode(data)))
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
form_field_class = forms.MultipleChoiceField
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a to-one relationship, using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def get_slug_field(self):
"""
Get the name of a slug field to be used to look up by slug.
"""
return self.slug_field
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
pk = getattr(obj, 'pk', None)
if pk is None:
return
kwargs = {self.pk_url_kwarg: pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
slug = getattr(obj, self.slug_field, None)
if not slug:
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
if value.startswith('http:') or value.startswith('https:'):
# If needed convert absolute URLs to relative path
value = urlparse(value).path
prefix = get_script_prefix()
if value.startswith(prefix):
value = '/' + value[len(prefix):]
try:
match = resolve(value)
except:
raise ValidationError('Invalid hyperlink - No URL match')
if match.url_name != self.view_name:
raise ValidationError('Invalid hyperlink - Incorrect URL match')
pk = match.kwargs.get(self.pk_url_kwarg, None)
slug = match.kwargs.get(self.slug_url_kwarg, None)
# Try explicit primary key.
if pk is not None:
queryset = self.queryset.filter(pk=pk)
# Next, try looking up by slug.
elif slug is not None:
slug_field = self.get_slug_field()
queryset = self.queryset.filter(**{slug_field: slug})
# If none of those are defined, it's an error.
else:
raise ValidationError('Invalid hyperlink')
try:
obj = queryset.get()
except ObjectDoesNotExist:
raise ValidationError('Invalid hyperlink - object does not exist.')
return obj
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
form_field_class = forms.MultipleChoiceField
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the
# HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
self.format = kwargs.pop('format', None)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
kwargs = {self.pk_url_kwarg: obj.pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
slug = getattr(obj, self.slug_field, None)
if not slug:
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)

View File

@ -22,7 +22,7 @@ from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import VERSION, status
from rest_framework import serializers, parsers
from rest_framework import parsers
class BaseRenderer(object):
@ -322,6 +322,9 @@ class BrowsableAPIRenderer(BaseRenderer):
if getattr(v, 'choices', None) is not None:
kwargs['choices'] = v.choices
if getattr(v, 'regex', None) is not None:
kwargs['regex'] = v.regex
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
kwargs['widget'] = widget

View File

@ -170,6 +170,15 @@ class Request(object):
self._user, self._auth = self._authenticate()
return self._user
@user.setter
def user(self, value):
"""
Sets the user on the current request. This is necessary to maintain
compatilbility with django.contrib.auth where the user proprety is
set in the login and logout functions.
"""
self._user = value
@property
def auth(self):
"""
@ -180,6 +189,14 @@ class Request(object):
self._user, self._auth = self._authenticate()
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):
"""
Parses the request content into self.DATA and self.FILES.

View File

@ -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
import os
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'
from coverage import coverage
@ -55,6 +58,12 @@ def main():
if 'compat.py' in files:
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.report(cov_files)

View File

@ -5,6 +5,9 @@
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
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'
from django.conf import settings

View File

@ -1,7 +1,7 @@
"""
Blank URLConf just to keep runtests.py happy.
"""
from django.conf.urls.defaults import *
from rest_framework.compat import patterns
urlpatterns = patterns('',
)

View File

@ -16,7 +16,7 @@ from rest_framework.compat import get_concrete_model
# This helps keep the seperation between model fields, form fields, and
# serializer fields more explicit.
from rest_framework.relations import *
from rest_framework.fields import *
@ -24,7 +24,16 @@ class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
pass
def __getstate__(self):
"""
Used by pickle (e.g., caching).
Overriden to remove metadata from the dict, since it shouldn't be pickled
and may in some instances be unpickleable.
"""
# return an instance of the first dict in MRO that isn't a DictWithMetadata
for base in self.__class__.__mro__:
if not isinstance(base, DictWithMetadata) and isinstance(base, dict):
return base(self)
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
@ -93,7 +102,8 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None
@ -133,8 +143,6 @@ class BaseSerializer(Field):
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
# Set up the field
field.initialize(parent=self, field_name=key)
# Add in the default fields
default_fields = self.get_default_fields()
@ -154,6 +162,9 @@ class BaseSerializer(Field):
for key in self.opts.exclude:
ret.pop(key, None)
for key, field in ret.items():
field.initialize(parent=self, field_name=key)
return ret
#####
@ -186,6 +197,7 @@ class BaseSerializer(Field):
ret.fields = {}
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
@ -199,6 +211,7 @@ class BaseSerializer(Field):
"""
reverted_data = {}
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
@ -219,10 +232,18 @@ class BaseSerializer(Field):
except ValidationError as err:
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
try:
attrs = self.validate(attrs)
except ValidationError as err:
self._errors['non_field_errors'] = err.messages
# If there are already errors, we don't run .validate() because
# field-validation failed and thus `attrs` may not be complete.
# which in turn can cause inconsistent validation errors.
if not self._errors:
try:
attrs = self.validate(attrs)
except ValidationError as err:
if hasattr(err, 'message_dict'):
for field_name, error_messages in err.message_dict.items():
self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
elif hasattr(err, 'messages'):
self._errors['non_field_errors'] = err.messages
return attrs
@ -274,15 +295,23 @@ class BaseSerializer(Field):
Override default so that we can apply ModelSerializer as a nested
field to relationships.
"""
obj = getattr(obj, self.source or field_name)
if is_simple_callable(obj):
obj = obj()
if self.source:
for component in self.source.split('.'):
obj = getattr(obj, component)
if is_simple_callable(obj):
obj = obj()
else:
obj = getattr(obj, field_name)
if is_simple_callable(obj):
obj = value()
# If the object has an "all" method, assume it's a relationship
if is_simple_callable(getattr(obj, 'all', None)):
return [self.to_native(item) for item in obj.all()]
if obj is None:
return None
return self.to_native(obj)
@property
@ -366,7 +395,6 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
field.initialize(parent=self, field_name=model_field.name)
ret[model_field.name] = field
for field_name in self.opts.read_only_fields:
@ -398,10 +426,14 @@ class ModelSerializer(Serializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
queryset = model_field.rel.to._default_manager
kwargs = {
'null': model_field.null,
'queryset': model_field.rel.to._default_manager
}
if to_many:
return ManyPrimaryKeyRelatedField(queryset=queryset)
return PrimaryKeyRelatedField(queryset=queryset)
return ManyPrimaryKeyRelatedField(**kwargs)
return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
@ -411,7 +443,7 @@ class ModelSerializer(Serializer):
kwargs['blank'] = model_field.blank
if model_field.null:
if model_field.null or model_field.blank:
kwargs['required'] = False
if model_field.has_default():
@ -426,10 +458,6 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
max_length = getattr(model_field, 'max_length', None)
if max_length:
kwargs['max_length'] = max_length
field_mapping = {
models.FloatField: FloatField,
models.IntegerField: IntegerField,
@ -453,28 +481,56 @@ class ModelSerializer(Serializer):
except KeyError:
return ModelField(model_field=model_field, **kwargs)
def get_validation_exclusions(self):
"""
Return a list of field names to exclude from model validation.
"""
cls = self.opts.model
opts = get_concrete_model(cls)._meta
exclusions = [field.name for field in opts.fields + opts.many_to_many]
for field_name, field in self.fields.items():
if field_name in exclusions and not field.read_only:
exclusions.remove(field_name)
return exclusions
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
"""
self.m2m_data = {}
self.related_data = {}
if instance is not None:
for key, val in attrs.items():
setattr(instance, key, val)
return instance
# 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 relations
# Reverse m2m relations
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.m2m_data[field_name] = attrs.pop(field_name)
# Forward relations
# Forward m2m relations
for field in self.opts.model._meta.many_to_many:
if field.name in attrs:
self.m2m_data[field.name] = attrs.pop(field.name)
return self.opts.model(**attrs)
if instance is not None:
for key, val in attrs.items():
setattr(instance, key, val)
else:
instance = self.opts.model(**attrs)
try:
instance.full_clean(exclude=self.get_validation_exclusions())
except ValidationError, err:
self._errors = err.message_dict
return None
return instance
def save(self, save_m2m=True):
"""
@ -487,6 +543,11 @@ class ModelSerializer(Serializer):
setattr(self.object, accessor_name, object_list)
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
@ -533,9 +594,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
queryset = rel._default_manager
kwargs = {
'queryset': queryset,
'null': model_field.null,
'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
if to_many:

View File

@ -1,6 +1,5 @@
{% load url from future %}
{% load rest_framework %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
@ -14,10 +13,10 @@
<title>{% block title %}Django REST framework{% endblock %}</title>
{% 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="{% get_static_prefix %}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='{% get_static_prefix %}rest_framework/css/default.css'/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
{% endblock %}
@ -195,10 +194,10 @@
{% endblock %}
{% block script %}
<script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script>
<script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script>
<script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script>
<script src="{% get_static_prefix %}rest_framework/js/default.js"></script>
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script>
{% endblock %}
</body>
</html>

View File

@ -1,11 +1,11 @@
{% load url from future %}
{% load static %}
{% load rest_framework %}
<html>
<head>
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}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='{% get_static_prefix %}rest_framework/css/default.css'/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
</head>
<body class="container">

View File

@ -20,6 +20,89 @@ import string
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):
"""
Given a URL and a key/val pair, set or replace an item in the query

View File

@ -1,15 +1,13 @@
from django.conf.urls.defaults import patterns, include
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.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.authtoken.models import Token
from rest_framework.authentication import TokenAuthentication
from rest_framework.compat import patterns
from rest_framework.views import APIView
import base64
@ -27,7 +25,7 @@ MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
(r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
)
@ -157,7 +155,7 @@ class TokenAuthTests(TestCase):
def test_token_login_json(self):
"""Ensure token login view using JSON POST works."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
@ -165,21 +163,21 @@ class TokenAuthTests(TestCase):
def test_token_login_json_bad_creds(self):
"""Ensure token login view using JSON POST fails if bad credentials are used."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
self.assertEqual(response.status_code, 401)
self.assertEqual(response.status_code, 400)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
response = client.post('/auth-token/',
json.dumps({'username': self.username}), 'application/json')
self.assertEqual(response.status_code, 401)
self.assertEqual(response.status_code, 400)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
response = client.post('/auth-token/',
{'username': self.username, 'password': self.password})
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)

View File

@ -1,5 +1,5 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from rest_framework.compat import patterns, url
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView

View File

@ -1,7 +1,7 @@
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import status
from rest_framework.response import Response
from django.test.client import RequestFactory
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rest_framework.authentication import BasicAuthentication

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import six
from django.db import models
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json
@ -178,7 +179,7 @@ class TestInstanceView(TestCase):
content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
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.data, {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
@ -305,3 +306,36 @@ class TestCreateModelWithAutoNowAddField(TestCase):
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
created = self.objects.get(id=1)
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)

View File

@ -1,11 +1,11 @@
import six
from django.core.exceptions import PermissionDenied
from django.conf.urls.defaults import patterns, url
from django.http import Http404
from django.test import TestCase
from django.template import TemplateDoesNotExist, Template
import django.template.loader
from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response

View File

@ -1,7 +1,8 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json
from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
factory = RequestFactory()
@ -54,10 +55,12 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost
@ -71,7 +74,7 @@ class AlbumDetail(generics.RetrieveAPIView):
model = Album
class OptionalRelationDetail(generics.RetrieveAPIView):
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model = OptionalRelationModel
model_serializer_class = serializers.HyperlinkedModelSerializer
@ -162,7 +165,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/')
response = self.list_view(request).render()
response = self.list_view(request)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data)
@ -171,7 +174,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/1/')
response = self.detail_view(request, pk=1).render()
response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data[0])
@ -194,7 +197,7 @@ class TestCreateWithForeignKeys(TestCase):
}
request = factory.post('/comments/', data=data)
response = self.create_view(request).render()
response = self.create_view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
@ -219,7 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
}
request = factory.post('/photos/', data=data)
response = self.list_create_view(request).render()
response = self.list_create_view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
self.assertEqual(self.post.photo_set.count(), 1)
@ -244,6 +247,16 @@ class TestOptionalRelationHyperlinkedView(TestCase):
for non existing relations.
"""
request = factory.get('/optionalrelationmodel-detail/1')
response = self.detail_view(request, pk=1).render()
response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data)
def test_put_detail_view(self):
"""
PUT requests to RetrieveUpdateDestroyAPIView with optional relations
should accept None for non existing relations.
"""
response = self.client.put('/optionalrelation/1/',
data=json.dumps(self.data),
content_type='application/json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -51,6 +51,11 @@ class RESTFrameworkModel(models.Model):
abstract = True
class HasPositiveIntegerAsChoice(RESTFrameworkModel):
some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
some_integer = models.PositiveIntegerField(choices=some_choices)
class Anchor(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor')
@ -124,8 +129,21 @@ class ActionItem(RESTFrameworkModel):
# Models for reverse relations
class Person(RESTFrameworkModel):
name = models.CharField(max_length=10)
age = models.IntegerField(null=True, blank=True)
@property
def info(self):
return {
'name': self.name,
'age': self.age,
}
class BlogPost(RESTFrameworkModel):
title = models.CharField(max_length=100)
writer = models.ForeignKey(Person, null=True, blank=True)
def get_first_comment(self):
return self.blogpostcomment_set.all()[0]
@ -145,21 +163,9 @@ class Photo(RESTFrameworkModel):
album = models.ForeignKey(Album)
class Person(RESTFrameworkModel):
name = models.CharField(max_length=10)
age = models.IntegerField(null=True, blank=True)
@property
def info(self):
return {
'name': self.name,
'age': self.age,
}
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
title = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=100, blank=True, null=False)
# Model for issue #380

View File

@ -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.contrib.auth.models import Group, User
# from rest_framework.resources import ModelResource

View File

@ -4,7 +4,7 @@ from django.core.paginator import Paginator
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from rest_framework import generics, status, pagination, filters
from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters
from rest_framework.tests.models import BasicModel, FilterableItem
@ -148,6 +148,11 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEquals(response.data['previous'], None)
class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
class Meta:
object_serializer_class = serializers.Serializer
class UnitTestPagination(TestCase):
"""
Unit tests for pagination of primitive objects.
@ -172,6 +177,15 @@ class UnitTestPagination(TestCase):
self.assertEquals(serializer.data['previous'], '?page=2')
self.assertEquals(serializer.data['results'], self.objects[20:])
def test_context_available_in_result(self):
"""
Ensure context gets passed through to the object serializer.
"""
serializer = PassOnContextPaginationSerializer(self.first_page)
serializer.data
results = serializer.fields[serializer.results_field]
self.assertTrue(serializer.context is results.context)
class TestUnpaginated(TestCase):
"""
@ -236,3 +250,32 @@ class TestCustomPaginateByParam(TestCase):
response = self.view(request).render()
self.assertEquals(response.data['count'], 13)
self.assertEquals(response.data['results'], self.data[:5])
class CustomField(serializers.Field):
def to_native(self, value):
if not 'view' in self.context:
raise RuntimeError("context isn't getting passed into custom field")
return "value"
class BasicModelSerializer(serializers.Serializer):
text = CustomField()
class TestContextPassedToCustomField(TestCase):
def setUp(self):
BasicModel.objects.create(text='ala ma kota')
def test_with_pagination(self):
class ListView(generics.ListCreateAPIView):
model = BasicModel
serializer_class = BasicModelSerializer
paginate_by = 1
self.view = ListView.as_view()
request = factory.get('/')
response = self.view(request).render()
self.assertEquals(response.status_code, status.HTTP_200_OK)

View File

@ -0,0 +1,434 @@
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())
serializer.save()
self.assertEquals(serializer.data, data)
# 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())
serializer.save()
self.assertEquals(serializer.data, data)
# 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())
# We shouldn't have saved anything to the db yet since save
# hasn't been called.
queryset = ForeignKeyTarget.objects.all()
new_serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
]
self.assertEquals(new_serializer.data, expected)
serializer.save()
self.assertEquals(serializer.data, data)
# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'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)

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

View File

@ -0,0 +1,414 @@
from django.db import models
from django.test import TestCase
from rest_framework import serializers
# ManyToMany
class ManyToManyTarget(models.Model):
name = models.CharField(max_length=100)
class ManyToManySource(models.Model):
name = models.CharField(max_length=100)
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
class ManyToManyTargetSerializer(serializers.ModelSerializer):
sources = serializers.ManyPrimaryKeyRelatedField()
class Meta:
model = ManyToManyTarget
class ManyToManySourceSerializer(serializers.ModelSerializer):
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.ModelSerializer):
sources = serializers.ManyPrimaryKeyRelatedField()
class Meta:
model = ForeignKeyTarget
class ForeignKeySourceSerializer(serializers.ModelSerializer):
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.ModelSerializer):
class Meta:
model = NullableForeignKeySource
# TODO: Add test that .data cannot be accessed prior to .is_valid
class PKManyToManyTests(TestCase):
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 = [
{'id': 1, 'name': u'source-1', 'targets': [1]},
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
{'id': 3, 'name': u'target-3', 'sources': [3]}
]
self.assertEquals(serializer.data, expected)
def test_many_to_many_update(self):
data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}
instance = ManyToManySource.objects.get(pk=1)
serializer = ManyToManySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
serializer.save()
self.assertEquals(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]},
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_update(self):
data = {'id': 1, 'name': u'target-1', 'sources': [1]}
instance = ManyToManyTarget.objects.get(pk=1)
serializer = ManyToManyTargetSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
serializer.save()
self.assertEquals(serializer.data, data)
# Ensure target 1 is updated, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [1]},
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
{'id': 3, 'name': u'target-3', 'sources': [3]}
]
self.assertEquals(serializer.data, expected)
def test_many_to_many_create(self):
data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]}
serializer = ManyToManySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'targets': [1]},
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]},
{'id': 4, 'name': u'source-4', 'targets': [1, 3]},
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': u'target-4', 'sources': [1, 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 = [
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
{'id': 3, 'name': u'target-3', 'sources': [3]},
{'id': 4, 'name': u'target-4', 'sources': [1, 3]}
]
self.assertEquals(serializer.data, expected)
class PKForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
new_target = ForeignKeyTarget(name='target-2')
new_target.save()
for idx in range(1, 4):
source = ForeignKeySource(name='source-%d' % idx, target=target)
source.save()
def test_foreign_key_retrieve(self):
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', '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': [1, 2, 3]},
{'id': 2, 'name': u'target-2', 'sources': []},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update(self):
data = {'id': 1, 'name': u'source-1', 'target': 2}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 2},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1}
]
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())
# We shouldn't have saved anything to the db yet since save
# hasn't been called.
queryset = ForeignKeyTarget.objects.all()
new_serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': u'target-2', 'sources': []},
]
self.assertEquals(new_serializer.data, expected)
serializer.save()
self.assertEquals(serializer.data, data)
# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [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 4 is added, 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 3 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):
data = {'id': 1, 'name': u'source-1', 'target': None}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
class PKNullableForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
for idx in range(1, 4):
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': 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):
data = {'id': 4, 'name': u'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': None},
{'id': 4, 'name': u'source-4', 'target': None}
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create_with_valid_emptystring(self):
"""
The emptystring should be interpreted as null in the context
of relationships.
"""
data = {'id': 4, 'name': u'source-4', 'target': ''}
expected_data = {'id': 4, 'name': u'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, expected_data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': None},
{'id': 4, 'name': u'source-4', 'target': None}
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_valid_null(self):
data = {'id': 1, 'name': u'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': None},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': None}
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_valid_emptystring(self):
"""
The emptystring should be interpreted as null in the context
of relationships.
"""
data = {'id': 1, 'name': u'source-1', 'target': ''}
expected_data = {'id': 1, 'name': u'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, expected_data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': None},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 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)

View File

@ -2,13 +2,12 @@ import pickle
import re
import six
from django.conf.urls.defaults import patterns, url, include
from django.core.cache import cache
from django.test import TestCase
from django.test.client import RequestFactory
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.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
@ -448,19 +447,19 @@ class CacheRenderTest(TestCase):
return
if state == None:
return
if isinstance(state,tuple):
if not isinstance(state[0],dict):
state=state[1]
if isinstance(state, tuple):
if not isinstance(state[0], dict):
state = state[1]
else:
state=state[0].update(state[1])
state = state[0].update(state[1])
result = {}
for i in state:
try:
pickle.dumps(state[i],protocol=2)
pickle.dumps(state[i], protocol=2)
except pickle.PicklingError:
if not state[i] in seen:
seen.append(state[i])
result[i] = cls._get_pickling_errors(state[i],seen)
result[i] = cls._get_pickling_errors(state[i], seen)
return result
def http_resp(self, http_method, url):

View File

@ -3,14 +3,15 @@ Tests for content parsing, and form-overloaded content parsing.
"""
import six
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.utils import simplejson as json
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from django.test.client import RequestFactory
from rest_framework.compat import patterns
from rest_framework.parsers import (
BaseParser,
FormParser,
@ -278,3 +279,37 @@ class TestContentParsingWithAuthentication(TestCase):
# response = self.csrf_client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
class TestUserSetter(TestCase):
def setUp(self):
# Pass request object through session middleware so session is
# available to login and logout functions
self.request = Request(factory.get('/'))
SessionMiddleware().process_request(self.request)
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
self.user = authenticate(username='ringo', password='yellow')
def test_user_can_be_set(self):
self.request.user = self.user
self.assertEqual(self.request.user, self.user)
def test_user_can_login(self):
login(self.request, self.user)
self.assertEqual(self.request.user, self.user)
def test_user_can_logout(self):
self.request.user = self.user
self.assertFalse(self.request.user.is_anonymous())
logout(self.request)
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')

View File

@ -1,9 +1,8 @@
import unittest
import six
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from rest_framework.compat import patterns, url, include
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status

View File

@ -1,6 +1,6 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse
factory = RequestFactory()

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals
import datetime
import pickle
from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
ManyToManyModel, Person, ReadOnlyManyToManyModel)
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
class SubComment(object):
@ -64,6 +65,18 @@ class PersonSerializer(serializers.ModelSerializer):
read_only_fields = ('age',)
class AlbumsSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ['title'] # lists are also valid options
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = HasPositiveIntegerAsChoice
fields = ['some_integer']
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@ -171,7 +184,7 @@ class ValidationTests(TestCase):
'content': 'x' * 1001,
'created': datetime.datetime(2012, 1, 1)
}
self.actionitem = ActionItem('Some to do item',
self.actionitem = ActionItem(title='Some to do item',
)
def test_create(self):
@ -279,6 +292,57 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': ['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):
def test_validate_unique(self):
"""
Just check if serializers.ModelSerializer handles unique checks via .full_clean()
"""
serializer = AlbumsSerializer(data={'title': 'a'})
serializer.is_valid()
serializer.save()
second_serializer = AlbumsSerializer(data={'title': 'a'})
self.assertFalse(second_serializer.is_valid())
self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
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):
def test_create_failed(self):
serializer = BookSerializer(data={'isbn': '1234567890'})
@ -562,6 +626,47 @@ class ManyRelatedTests(TestCase):
self.assertEqual(serializer.data, expected)
class RelatedTraversalTest(TestCase):
def test_nested_traversal(self):
user = Person.objects.create(name="django")
post = BlogPost.objects.create(title="Test blog post", writer=user)
post.blogpostcomment_set.create(text="I love this blog post")
from rest_framework.tests.models import BlogPostComment
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ("name", "age")
class BlogPostCommentSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPostComment
fields = ("text", "post_owner")
text = serializers.CharField()
post_owner = PersonSerializer(source='blog_post.writer')
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField()
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
serializer = BlogPostSerializer(instance=post)
expected = {
'title': u'Test blog post',
'comments': [{
'text': u'I love this blog post',
'post_owner': {
"name": u"django",
"age": None
}
}]
}
self.assertEqual(serializer.data, expected)
class SerializerMethodFieldTests(TestCase):
def setUp(self):
@ -628,6 +733,10 @@ class BlankFieldTests(TestCase):
serializer = self.model_serializer_class(data=self.data)
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):
"""
Test to ensure blank data in a field not marked as blank=True
@ -643,3 +752,120 @@ class BlankFieldTests(TestCase):
"""
serializer = self.not_blank_model_serializer_class(data=self.data)
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
class SerializerPickleTests(TestCase):
"""
Test pickleability of the output of Serializers
"""
def test_pickle_simple_model_serializer_data(self):
"""
Test simple serializer
"""
pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
def test_pickle_inner_serializer(self):
"""
Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
See DictWithMetadata.__getstate__
"""
class InnerPersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('name', 'age')
pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data)
class DepthTest(TestCase):
def test_implicit_nesting(self):
writer = Person.objects.create(name="django", age=1)
post = BlogPost.objects.create(title="Test blog post", writer=writer)
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
depth = 1
serializer = BlogPostSerializer(instance=post)
expected = {'id': 1, 'title': u'Test blog post',
'writer': {'id': 1, 'name': u'django', 'age': 1}}
self.assertEqual(serializer.data, expected)
def test_explicit_nesting(self):
writer = Person.objects.create(name="django", age=1)
post = BlogPost.objects.create(title="Test blog post", writer=writer)
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
class BlogPostSerializer(serializers.ModelSerializer):
writer = PersonSerializer()
class Meta:
model = BlogPost
serializer = BlogPostSerializer(instance=post)
expected = {'id': 1, 'title': u'Test blog post',
'writer': {'id': 1, 'name': u'django', 'age': 1}}
self.assertEqual(serializer.data, expected)
class NestedSerializerContextTests(TestCase):
def test_nested_serializer_context(self):
"""
Regression for #497
https://github.com/tomchristie/django-rest-framework/issues/497
"""
class PhotoSerializer(serializers.ModelSerializer):
class Meta:
model = Photo
fields = ("description", "callable")
callable = serializers.SerializerMethodField('_callable')
def _callable(self, instance):
if not 'context_item' in self.context:
raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
return "success"
class AlbumSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ("photo_set", "callable")
photo_set = PhotoSerializer(source="photo_set")
callable = serializers.SerializerMethodField("_callable")
def _callable(self, instance):
if not 'context_item' in self.context:
raise RuntimeError("context isn't getting passed into 1st level nested serializer")
return "success"
class AlbumCollection(object):
albums = None
class AlbumCollectionSerializer(serializers.Serializer):
albums = AlbumSerializer(source="albums")
album1 = Album.objects.create(title="album 1")
album2 = Album.objects.create(title="album 2")
Photo.objects.create(description="Bigfoot", album=album1)
Photo.objects.create(description="Unicorn", album=album1)
Photo.objects.create(description="Yeti", album=album2)
Photo.objects.create(description="Sasquatch", album=album2)
album_collection = AlbumCollection()
album_collection.albums = [album1, album2]
# This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data

View File

@ -6,6 +6,7 @@ from django.test import TestCase
NO_SETTING = ('!', None)
class TestSettingsManager(object):
"""
A class which can modify some Django settings temporarily for a
@ -19,7 +20,7 @@ class TestSettingsManager(object):
self._original_settings = {}
def set(self, **kwargs):
for k,v in kwargs.iteritems():
for k, v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
NO_SETTING))
setattr(settings, k, v)
@ -31,7 +32,7 @@ class TestSettingsManager(object):
call_command('syncdb', verbosity=0)
def revert(self):
for k,v in self._original_settings.iteritems():
for k, v in self._original_settings.iteritems():
if v == NO_SETTING:
delattr(settings, k)
else:
@ -57,6 +58,7 @@ class SettingsTestCase(TestCase):
def tearDown(self):
self.settings_manager.revert()
class TestModelsTestCase(SettingsTestCase):
def setUp(self, *args, **kwargs):
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)

View File

@ -1,4 +1,4 @@
from django.conf.urls.defaults import url
from rest_framework.compat import url
from rest_framework.settings import api_settings

View File

@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`.
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'}

View File

@ -12,12 +12,12 @@ deps = https://github.com/django/django/zipball/master
[testenv:py2.7-django1.4]
basepython = python2.7
deps = django==1.4.1
deps = django==1.4.3
django-filter==0.5.4
[testenv:py2.7-django1.3]
basepython = python2.7
deps = django==1.3.3
deps = django==1.3.5
django-filter==0.5.4
[testenv:py2.6-django1.5]
@ -27,10 +27,10 @@ deps = https://github.com/django/django/zipball/master
[testenv:py2.6-django1.4]
basepython = python2.6
deps = django==1.4.1
deps = django==1.4.3
django-filter==0.5.4
[testenv:py2.6-django1.3]
basepython = python2.6
deps = django==1.3.3
deps = django==1.3.5
django-filter==0.5.4