mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Merged to latest master
This commit is contained in:
commit
36fa722ebb
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,5 +10,10 @@ dist/
|
|||
*.egg-info/
|
||||
MANIFEST
|
||||
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
local/
|
||||
|
||||
!.gitignore
|
||||
!.travis.yml
|
||||
|
|
|
@ -6,13 +6,12 @@ python:
|
|||
|
||||
env:
|
||||
- DJANGO=https://github.com/django/django/zipball/master
|
||||
- DJANGO=django==1.4.1 --use-mirrors
|
||||
- DJANGO=django==1.3.3 --use-mirrors
|
||||
- DJANGO=django==1.4.3 --use-mirrors
|
||||
- DJANGO=django==1.3.5 --use-mirrors
|
||||
|
||||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install -r requirements.txt --use-mirrors
|
||||
- pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
- pip install django-filter==0.5.4 --use-mirrors
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
|
149
README.md
149
README.md
|
@ -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]
|
||||
|
||||
|
@ -33,17 +35,39 @@ There is also a sandbox API you can use for testing purposes, [available here][s
|
|||
|
||||
* [Markdown] - Markdown support for the self describing API.
|
||||
* [PyYAML] - YAML content type support.
|
||||
* [django-filter] - Filtering support.
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -57,6 +81,119 @@ To run the tests.
|
|||
|
||||
# Changelog
|
||||
|
||||
### 2.1.15
|
||||
|
||||
**Date**: 3rd Jan 2013
|
||||
|
||||
* Added `PATCH` support.
|
||||
* Added `RetrieveUpdateAPIView`.
|
||||
* Relation changes are now persisted in `.save` instead of in `.restore_object`.
|
||||
* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`.
|
||||
* Tweak behavior of hyperlinked fields with an explicit format suffix.
|
||||
* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None.
|
||||
* Bugfix: Partial updates should not set default values if field is not included.
|
||||
|
||||
### 2.1.14
|
||||
|
||||
**Date**: 31st Dec 2012
|
||||
|
||||
* 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
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Fix DjangoModelPermissions.
|
||||
|
||||
## 2.1.4
|
||||
|
||||
**Date**: 22nd Nov 2012
|
||||
|
||||
* Support for partial updates with serializers.
|
||||
* Added `RegexField`.
|
||||
* Added `SerializerMethodField`.
|
||||
* Serializer performance improvements.
|
||||
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
|
||||
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
|
||||
|
||||
## 2.1.3
|
||||
|
||||
**Date**: 16th Nov 2012
|
||||
|
||||
* Added `FileField` and `ImageField`. For use with `MultiPartParser`.
|
||||
* Added `URLField` and `SlugField`.
|
||||
* Support for `read_only_fields` on `ModelSerializer` classes.
|
||||
* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
|
||||
* 201 Responses now return a 'Location' header.
|
||||
* Bugfix: Serializer fields now respect `max_length`.
|
||||
|
||||
## 2.1.2
|
||||
|
||||
**Date**: 9th Nov 2012
|
||||
|
@ -110,7 +247,7 @@ To run the tests.
|
|||
|
||||
# License
|
||||
|
||||
Copyright (c) 2011, Tom Christie
|
||||
Copyright (c) 2011-2013, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -133,16 +270,18 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
||||
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
||||
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||
|
||||
[docs]: http://django-rest-framework.org/
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
|
||||
|
|
|
@ -126,19 +126,36 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
|||
|
||||
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
|
||||
|
||||
<<<<<<< HEAD
|
||||
<!--## OAuth2Authentication
|
||||
=======
|
||||
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
||||
|
||||
This authentication scheme uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf.
|
||||
@receiver(post_save, sender=User)
|
||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
Token.objects.create(user=instance)
|
||||
|
||||
If successfully authenticated, `OAuth2Authentication` provides the following credentials.
|
||||
If you've already created some users, you can generate tokens for all existing users like this:
|
||||
|
||||
* `request.user` will be a Django `User` instance.
|
||||
* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
**TODO**: Note type of response (401 vs 403)
|
||||
for user in User.objects.all():
|
||||
Token.objects.get_or_create(user=user)
|
||||
|
||||
**TODO**: Implement OAuth2Authentication, using django-oauth2-provider.
|
||||
-->
|
||||
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password.
|
||||
REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
|
||||
|
||||
urlpatterns += patterns('',
|
||||
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
|
||||
)
|
||||
|
||||
Note that the URL part of the pattern can be whatever you want to use.
|
||||
|
||||
The `obtain_auth_token` view will return a JSON response when valid `username` and `password` fields are POSTed to the view using form data or JSON:
|
||||
|
||||
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }
|
||||
|
||||
## SessionAuthentication
|
||||
|
||||
|
|
|
@ -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.
|
||||
>
|
||||
> — [The Zen of Python][cite]
|
||||
> — [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
|
||||
|
@ -131,6 +149,18 @@ or `django.db.models.fields.TextField`.
|
|||
|
||||
**Signature:** `CharField(max_length=None, min_length=None)`
|
||||
|
||||
## URLField
|
||||
|
||||
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
|
||||
|
||||
**Signature:** `CharField(max_length=200, min_length=None)`
|
||||
|
||||
## SlugField
|
||||
|
||||
Corresponds to `django.db.models.fields.SlugField`.
|
||||
|
||||
**Signature:** `CharField(max_length=50, min_length=None)`
|
||||
|
||||
## ChoiceField
|
||||
|
||||
A field that can accept a value out of a limited set of choices.
|
||||
|
@ -141,6 +171,16 @@ A text representation, validates the text to be a valid e-mail address.
|
|||
|
||||
Corresponds to `django.db.models.fields.EmailField`
|
||||
|
||||
## RegexField
|
||||
|
||||
A text representation, that validates the given value matches against a certain regular expression.
|
||||
|
||||
Uses Django's `django.core.validators.RegexValidator` for validation.
|
||||
|
||||
Corresponds to `django.forms.fields.RegexField`
|
||||
|
||||
**Signature:** `RegexField(regex, max_length=None, min_length=None)`
|
||||
|
||||
## DateField
|
||||
|
||||
A date representation.
|
||||
|
@ -165,124 +205,32 @@ A floating point representation.
|
|||
|
||||
Corresponds to `django.db.models.fields.FloatField`.
|
||||
|
||||
## FileField
|
||||
|
||||
A file representation. Performs Django's standard FileField validation.
|
||||
|
||||
Corresponds to `django.forms.fields.FileField`.
|
||||
|
||||
**Signature:** `FileField(max_length=None, allow_empty_file=False)`
|
||||
|
||||
- `max_length` designates the maximum length for the file name.
|
||||
|
||||
- `allow_empty_file` designates if empty files are allowed.
|
||||
|
||||
## ImageField
|
||||
|
||||
An image representation.
|
||||
|
||||
Corresponds to `django.forms.fields.ImageField`.
|
||||
|
||||
Requires the `PIL` package.
|
||||
|
||||
Signature and validation is the same as with `FileField`.
|
||||
|
||||
---
|
||||
|
||||
# Relational Fields
|
||||
**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 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`.
|
||||
|
||||
[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
|
||||
|
|
|
@ -71,7 +71,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
|
|||
by filtering against a `username` query parameter in the URL.
|
||||
"""
|
||||
queryset = Purchase.objects.all()
|
||||
username = self.request.QUERY_PARAMS.get('username', None):
|
||||
username = self.request.QUERY_PARAMS.get('username', None)
|
||||
if username is not None:
|
||||
queryset = queryset.filter(purchaser__username=username)
|
||||
return queryset
|
||||
|
@ -84,9 +84,9 @@ As well as being able to override the default queryset, REST framework also incl
|
|||
|
||||
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
|
||||
|
||||
To use REST framework's default filtering backend, first install `django-filter`.
|
||||
To use REST framework's filtering backend, first install `django-filter`.
|
||||
|
||||
pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
|
||||
pip install django-filter
|
||||
|
||||
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
||||
|
||||
|
@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
|||
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
|
||||
}
|
||||
|
||||
**Note**: The currently supported version of `django-filter` is the `master` branch. A PyPI release is expected to be coming soon.
|
||||
|
||||
## Specifying filter fields
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
>
|
||||
> — [Django Documentation][cite]
|
||||
|
||||
One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
|
||||
One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
|
||||
|
||||
The generic views provided by REST framework allow you to quickly build API views that map closely to your database models.
|
||||
|
||||
If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views.
|
||||
If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -29,7 +29,7 @@ For more complex cases you might also want to override various methods on the vi
|
|||
model = User
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
"""
|
||||
Use smaller pagination for HTML representations.
|
||||
|
@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin]
|
|||
|
||||
Used for **update-only** endpoints for a **single model instance**.
|
||||
|
||||
Provides a `put` method handler.
|
||||
Provides `put` and `patch` method handlers.
|
||||
|
||||
Extends: [SingleObjectAPIView], [UpdateModelMixin]
|
||||
|
||||
|
@ -97,6 +97,14 @@ Provides `get` and `post` method handlers.
|
|||
|
||||
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin]
|
||||
|
||||
## RetrieveUpdateAPIView
|
||||
|
||||
Used for **read or update** endpoints to represent a **single model instance**.
|
||||
|
||||
Provides `get`, `put` and `patch` method handlers.
|
||||
|
||||
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin]
|
||||
|
||||
## RetrieveDestroyAPIView
|
||||
|
||||
Used for **read or delete** endpoints to represent a **single model instance**.
|
||||
|
@ -109,7 +117,7 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]
|
|||
|
||||
Used for **read-write-delete** endpoints to represent a **single model instance**.
|
||||
|
||||
Provides `get`, `put` and `delete` method handlers.
|
||||
Provides `get`, `put`, `patch` and `delete` method handlers.
|
||||
|
||||
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
|
||||
|
||||
|
@ -123,52 +131,90 @@ Each of the generic views provided is built by combining one of the base views b
|
|||
|
||||
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
|
||||
|
||||
**Attributes**:
|
||||
|
||||
* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required.
|
||||
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
|
||||
|
||||
## MultipleObjectAPIView
|
||||
|
||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
|
||||
|
||||
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
|
||||
|
||||
**Attributes**:
|
||||
|
||||
* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`.
|
||||
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
|
||||
* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
|
||||
|
||||
## SingleObjectAPIView
|
||||
|
||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
|
||||
|
||||
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
|
||||
|
||||
**Attributes**:
|
||||
|
||||
* `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_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'`.
|
||||
|
||||
---
|
||||
|
||||
# Mixins
|
||||
|
||||
The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour.
|
||||
The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour.
|
||||
|
||||
## ListModelMixin
|
||||
|
||||
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
||||
|
||||
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
|
||||
|
||||
If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
|
||||
|
||||
Should be mixed in with [MultipleObjectAPIView].
|
||||
|
||||
## CreateModelMixin
|
||||
|
||||
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
||||
|
||||
If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
|
||||
|
||||
If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||
|
||||
Should be mixed in with any [GenericAPIView].
|
||||
|
||||
## RetrieveModelMixin
|
||||
|
||||
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
||||
|
||||
If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
||||
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
## UpdateModelMixin
|
||||
|
||||
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
||||
|
||||
If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
|
||||
|
||||
If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
|
||||
|
||||
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||
|
||||
A boolean `partial` keyword argument may be supplied to the `.update()` method. If `partial` is set to `True`, all fields for the update will be optional. This allows support for HTTP `PATCH` requests.
|
||||
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
## DestroyModelMixin
|
||||
|
||||
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
||||
|
||||
If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
|
||||
|
||||
Should be mixed in with [SingleObjectAPIView].
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||
|
@ -184,4 +230,4 @@ Should be mixed in with [SingleObjectAPIView].
|
|||
[CreateModelMixin]: #createmodelmixin
|
||||
[RetrieveModelMixin]: #retrievemodelmixin
|
||||
[UpdateModelMixin]: #updatemodelmixin
|
||||
[DestroyModelMixin]: #destroymodelmixin
|
||||
[DestroyModelMixin]: #destroymodelmixin
|
||||
|
|
|
@ -70,33 +70,32 @@ We could now use our pagination serializer in a view like this.
|
|||
# If page is not an integer, deliver first page.
|
||||
users = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||
# If page is out of range (e.g. 9999),
|
||||
# deliver last page of results.
|
||||
users = paginator.page(paginator.num_pages)
|
||||
|
||||
serializer_context = {'request': request}
|
||||
serializer = PaginatedUserSerializer(instance=users,
|
||||
serializer = PaginatedUserSerializer(users,
|
||||
context=serializer_context)
|
||||
return Response(serializer.data)
|
||||
|
||||
## Pagination in the generic views
|
||||
|
||||
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, or by turning pagination off completely.
|
||||
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
|
||||
|
||||
The default pagination style may be set globally, using the `PAGINATION_SERIALIZER` and `PAGINATE_BY` settings. For example.
|
||||
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATION_SERIALIZER': (
|
||||
'example_app.pagination.CustomPaginationSerializer',
|
||||
),
|
||||
'PAGINATE_BY': 10
|
||||
'PAGINATE_BY': 10,
|
||||
'PAGINATE_BY_PARAM': 'page_size'
|
||||
}
|
||||
|
||||
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
|
||||
|
||||
class PaginatedListView(ListAPIView):
|
||||
model = ExampleModel
|
||||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
paginate_by_param = 'page_size'
|
||||
|
||||
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
||||
|
||||
|
@ -122,4 +121,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
|
|||
|
||||
results_field = 'objects'
|
||||
|
||||
## Using your custom pagination serializer
|
||||
|
||||
To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
|
||||
'example_app.pagination.CustomPaginationSerializer',
|
||||
}
|
||||
|
||||
Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
|
||||
|
||||
class PaginatedListView(ListAPIView):
|
||||
model = ExampleModel
|
||||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||
|
|
|
@ -159,4 +159,17 @@ For example:
|
|||
files = {name: uploaded}
|
||||
return DataAndFiles(data, files)
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## MessagePack
|
||||
|
||||
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the `djangorestframework-msgpack` package which provides MessagePack renderer and parser support for REST framework. Documentation is [available here][djangorestframework-msgpack].
|
||||
|
||||
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
|
||||
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
|
@ -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
139
docs/api-guide/relations.md
Normal 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.
|
||||
>
|
||||
> — [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/
|
|
@ -271,6 +271,15 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
|
|||
|
||||
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## MessagePack
|
||||
|
||||
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the `djangorestframework-msgpack` package which provides MessagePack renderer and parser support for REST framework. Documentation is [available here][djangorestframework-msgpack].
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||
[conneg]: content-negotiation.md
|
||||
|
@ -280,4 +289,7 @@ Templates will render with a `RequestContext` which includes the `status_code` a
|
|||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
||||
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
|
||||
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
|
||||
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
|
@ -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.
|
||||
>
|
||||
> — Russell Keith-Magee, [Django users group][cite]
|
||||
|
||||
|
@ -34,7 +33,7 @@ Declaring a serializer looks very similar to declaring a form:
|
|||
created = serializers.DateTimeField()
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
if instance:
|
||||
if instance is not None:
|
||||
instance.title = attrs['title']
|
||||
instance.content = attrs['content']
|
||||
instance.created = attrs['created']
|
||||
|
@ -77,6 +76,10 @@ When deserializing data, we can either create a new instance, or update an exist
|
|||
serializer = CommentSerializer(data=data) # Create new instance
|
||||
serializer = CommentSerializer(comment, data=data) # Update `instance`
|
||||
|
||||
By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
|
||||
|
||||
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
|
||||
|
||||
## Validation
|
||||
|
||||
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages.
|
||||
|
@ -106,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
|
||||
|
||||
|
@ -248,6 +266,15 @@ The default `ModelSerializer` uses primary keys for relationships, but you can a
|
|||
|
||||
The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
|
||||
|
||||
## Specifying which fields should be read-only
|
||||
|
||||
You may wish to specify multiple fields as read-only. Instead of adding each field explicitely with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
read_only_fields = ('created', 'modified')
|
||||
|
||||
## Customising the default fields
|
||||
|
||||
You can create customized subclasses of `ModelSerializer` that use a different set of default fields for the representation, by overriding various `get_<field_type>_field` methods.
|
||||
|
|
|
@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
|
|||
|
||||
Default: `rest_framework.pagination.PaginationSerializer`
|
||||
|
||||
## FORMAT_SUFFIX_KWARG
|
||||
## FILTER_BACKEND
|
||||
|
||||
**TODO**
|
||||
The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled.
|
||||
|
||||
Default: `'format'`
|
||||
## PAGINATE_BY
|
||||
|
||||
The default page size to use for pagination. If set to `None`, pagination is disabled by default.
|
||||
|
||||
Default: `None`
|
||||
|
||||
## PAGINATE_BY_PARAM
|
||||
|
||||
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
|
||||
|
||||
Default: `None`
|
||||
|
||||
## UNAUTHENTICATED_USER
|
||||
|
||||
|
@ -150,4 +160,10 @@ Default: `'accept'`
|
|||
|
||||
Default: `'format'`
|
||||
|
||||
## FORMAT_SUFFIX_KWARG
|
||||
|
||||
**TODO**
|
||||
|
||||
Default: `'format'`
|
||||
|
||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||
|
|
|
@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas
|
|||
|
||||
For example:
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import authentication, permissions
|
||||
|
||||
class ListUsers(APIView):
|
||||
"""
|
||||
View to list all users in the system.
|
||||
|
|
|
@ -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].
|
||||
|
||||
|
@ -32,17 +32,18 @@ REST framework requires the following:
|
|||
|
||||
The following packages are optional:
|
||||
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the self describing API.
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
|
||||
* [PyYAML][yaml] (3.10+) - YAML content-type support.
|
||||
* [django-filter][django-filter] (master) - Filtering support.
|
||||
* [django-filter][django-filter] (0.5.4+) - Filtering support.
|
||||
|
||||
## Installation
|
||||
|
||||
Install using `pip`, including any optional packages you want...
|
||||
|
||||
pip install djangorestframework
|
||||
pip install markdown # Recommended if using the browseable API.
|
||||
pip install pyyaml # Required for yaml content-type support.
|
||||
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.
|
||||
|
||||
|
@ -51,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
|
||||
|
||||
|
@ -93,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]
|
||||
|
@ -136,7 +138,7 @@ Paid support is also available from [DabApps], and can include work on REST fram
|
|||
|
||||
## License
|
||||
|
||||
Copyright (c) 2011-2012, Tom Christie
|
||||
Copyright (c) 2011-2013, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -160,7 +162,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[yaml]: http://pypi.python.org/pypi/PyYAML
|
||||
|
@ -184,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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The following people have helped make REST framework great.
|
||||
|
||||
* Tom Christie - [tomchristie]
|
||||
* Tom Christie - [tomchristie]
|
||||
* Marko Tibold - [markotibold]
|
||||
* Paul Bagwell - [pbgwl]
|
||||
* Sébastien Piquemal - [sebpiq]
|
||||
|
@ -58,6 +58,36 @@ The following people have helped make REST framework great.
|
|||
* Michael Shepanski - [mjs7231]
|
||||
* Toni Michel - [tonimichel]
|
||||
* Ben Konrath - [benkonrath]
|
||||
* Marc Aymerich - [glic3rinu]
|
||||
* Ludwig Kraatz - [ludwigkraatz]
|
||||
* Rob Romano - [robromano]
|
||||
* Eugene Mechanism - [mechanism]
|
||||
* Jonas Liljestrand - [jonlil]
|
||||
* Justin Davis - [irrelative]
|
||||
* Dustin Bachrach - [dbachrach]
|
||||
* Mark Shirley - [maspwr]
|
||||
* 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]
|
||||
* Andrew Hankinson - [ahankinson]
|
||||
* Juan Riaza - [juanriaza]
|
||||
* Michael Mior - [michaelmior]
|
||||
* Marc Tamlyn - [mjtamlyn]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -69,7 +99,7 @@ Project hosting is with [GitHub].
|
|||
|
||||
Continuous integration testing is managed with [Travis CI][travis-ci].
|
||||
|
||||
The [live sandbox][sandbox] is hosted on [Heroku].
|
||||
The [live sandbox][sandbox] is hosted on [Heroku].
|
||||
|
||||
Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects.
|
||||
|
||||
|
@ -77,11 +107,10 @@ 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].
|
||||
|
||||
You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||
|
||||
* twitter: [@_tomchristie][twitter]
|
||||
* email: [tom@tomchristie.com][email]
|
||||
|
||||
[email]: mailto:tom@tomchristie.com
|
||||
[twitter]: http://twitter.com/_tomchristie
|
||||
[bootstrap]: http://twitter.github.com/bootstrap/
|
||||
|
@ -94,6 +123,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
|
||||
|
@ -151,3 +181,33 @@ To contact the author directly:
|
|||
[mjs7231]: https://github.com/mjs7231
|
||||
[tonimichel]: https://github.com/tonimichel
|
||||
[benkonrath]: https://github.com/benkonrath
|
||||
[glic3rinu]: https://github.com/glic3rinu
|
||||
[ludwigkraatz]: https://github.com/ludwigkraatz
|
||||
[robromano]: https://github.com/robromano
|
||||
[mechanism]: https://github.com/mechanism
|
||||
[jonlil]: https://github.com/jonlil
|
||||
[irrelative]: https://github.com/irrelative
|
||||
[dbachrach]: https://github.com/dbachrach
|
||||
[maspwr]: https://github.com/maspwr
|
||||
[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
|
||||
[ahankinson]: https://github.com/ahankinson
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[michaelmior]: https://github.com/michaelmior
|
||||
[mjtamlyn]: https://github.com/mjtamlyn
|
||||
|
|
|
@ -4,29 +4,159 @@
|
|||
>
|
||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||
|
||||
## 2.1.2
|
||||
## 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
|
||||
|
||||
* Deprecate django.utils.simplejson in favor of Python 2.6's built-in json module.
|
||||
* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types.
|
||||
* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
|
||||
|
||||
### 2.1.15
|
||||
|
||||
**Date**: 3rd Jan 2013
|
||||
|
||||
* Added `PATCH` support.
|
||||
* Added `RetrieveUpdateAPIView`.
|
||||
* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`.
|
||||
* Tweak behavior of hyperlinked fields with an explicit format suffix.
|
||||
* Relation changes are now persisted in `.save()` instead of in `.restore_object()`.
|
||||
* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None.
|
||||
* Bugfix: Partial updates should not set default values if field is not included.
|
||||
|
||||
### 2.1.14
|
||||
|
||||
**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 separate them from regular data type fields, such as `CharField` and `IntegerField`.
|
||||
|
||||
This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and referring 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
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Fix DjangoModelPermissions.
|
||||
|
||||
### 2.1.4
|
||||
|
||||
**Date**: 22nd Nov 2012
|
||||
|
||||
* Support for partial updates with serializers.
|
||||
* Added `RegexField`.
|
||||
* Added `SerializerMethodField`.
|
||||
* Serializer performance improvements.
|
||||
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
|
||||
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
|
||||
|
||||
### 2.1.3
|
||||
|
||||
**Date**: 16th Nov 2012
|
||||
|
||||
* Added `FileField` and `ImageField`. For use with `MultiPartParser`.
|
||||
* Added `URLField` and `SlugField`.
|
||||
* Support for `read_only_fields` on `ModelSerializer` classes.
|
||||
* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
|
||||
* 201 Responses now return a 'Location' header.
|
||||
* Bugfix: Serializer fields now respect `max_length`.
|
||||
|
||||
### 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.
|
||||
|
@ -35,13 +165,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
|
||||
|
||||
|
@ -49,7 +185,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
|
||||
|
||||
|
@ -58,34 +194,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)
|
||||
|
@ -97,37 +239,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.
|
||||
|
@ -151,14 +297,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
|
@ -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.
|
||||
|
||||
|
@ -113,7 +115,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
|
|||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
urlpatterns = patterns('snippet.views',
|
||||
urlpatterns = patterns('snippets.views',
|
||||
url(r'^snippets/$', 'snippet_list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ We can also write our API views using class based views, rather than function ba
|
|||
|
||||
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
|
||||
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from django.http import Http404
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
@ -66,7 +66,7 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
|
|||
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
from snippetpost import views
|
||||
from snippets import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^snippets/$', views.SnippetList.as_view()),
|
||||
|
@ -85,8 +85,8 @@ The create/retrieve/update/delete operations that we've been using so far are go
|
|||
|
||||
Let's take a look at how we can compose our views by using the mixin classes.
|
||||
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from rest_framework import mixins
|
||||
from rest_framework import generics
|
||||
|
||||
|
@ -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,14 +122,14 @@ 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
|
||||
|
||||
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
|
||||
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.**
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`. Once you'
|
|||
|
||||
First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
13
mkdocs.py
13
mkdocs.py
|
@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs')
|
|||
html_dir = os.path.join(root_dir, 'html')
|
||||
|
||||
local = not '--deploy' in sys.argv
|
||||
preview = '-p' in sys.argv
|
||||
|
||||
if local:
|
||||
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
|
||||
|
@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
|||
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
|
||||
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
|
||||
open(output_path, 'w').write(output.encode('utf-8'))
|
||||
|
||||
if preview:
|
||||
import subprocess
|
||||
|
||||
url = 'html/index.html'
|
||||
|
||||
try:
|
||||
subprocess.Popen(["open", url]) # Mac
|
||||
except OSError:
|
||||
subprocess.Popen(["xdg-open", url]) # Linux
|
||||
except:
|
||||
os.startfile(url) # Windows
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
django-filter>=0.5.4
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '2.1.2'
|
||||
__version__ = '2.1.15'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -5,13 +5,21 @@ from south.v2 import SchemaMigration
|
|||
from django.db import models
|
||||
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
except ImportError: # django < 1.5
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Token'
|
||||
db.create_table('authtoken_token', (
|
||||
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['auth.User'])),
|
||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('authtoken', ['Token'])
|
||||
|
@ -36,7 +44,7 @@ class Migration(SchemaMigration):
|
|||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
|
@ -56,7 +64,7 @@ class Migration(SchemaMigration):
|
|||
'Meta': {'object_name': 'Token'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import uuid
|
||||
import hmac
|
||||
from hashlib import sha1
|
||||
from rest_framework.compat import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
@ -9,7 +10,7 @@ class Token(models.Model):
|
|||
The default authorization token model.
|
||||
"""
|
||||
key = models.CharField(max_length=40, primary_key=True)
|
||||
user = models.OneToOneField('auth.User', related_name='auth_token')
|
||||
user = models.OneToOneField(User, related_name='auth_token')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
24
rest_framework/authtoken/serializers.py
Normal file
24
rest_framework/authtoken/serializers.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get('username')
|
||||
password = attrs.get('password')
|
||||
|
||||
if username and password:
|
||||
user = authenticate(username=username, password=password)
|
||||
|
||||
if user:
|
||||
if not user.is_active:
|
||||
raise serializers.ValidationError('User account is disabled.')
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
else:
|
||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||
else:
|
||||
raise serializers.ValidationError('Must include "username" and "password"')
|
|
@ -0,0 +1,25 @@
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from rest_framework import parsers
|
||||
from rest_framework import renderers
|
||||
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 = ()
|
||||
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
||||
renderer_classes = (renderers.JSONRenderer,)
|
||||
model = Token
|
||||
|
||||
def post(self, request):
|
||||
serializer = AuthTokenSerializer(data=request.DATA)
|
||||
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_400_BAD_REQUEST)
|
||||
|
||||
|
||||
obtain_auth_token = ObtainAuthToken.as_view()
|
|
@ -1,10 +1,16 @@
|
|||
"""
|
||||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of django/python, and compatbility wrappers around optional packages.
|
||||
versions of django/python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
# flake8: noqa
|
||||
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
|
||||
|
@ -19,6 +25,16 @@ except ImportError:
|
|||
import StringIO
|
||||
|
||||
|
||||
# Try to import PIL in either of the two ways it can end up installed.
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
try:
|
||||
import Image
|
||||
except ImportError:
|
||||
Image = None
|
||||
|
||||
|
||||
def get_concrete_model(model_cls):
|
||||
try:
|
||||
return model_cls._meta.concrete_model
|
||||
|
@ -27,6 +43,20 @@ def get_concrete_model(model_cls):
|
|||
return model_cls
|
||||
|
||||
|
||||
# Django 1.5 add support for custom auth user model
|
||||
if django.VERSION >= (1, 5):
|
||||
from django.conf import settings
|
||||
if hasattr(settings, 'AUTH_USER_MODEL'):
|
||||
User = settings.AUTH_USER_MODEL
|
||||
else:
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
try:
|
||||
from django.contrib.auth.models import User
|
||||
except ImportError:
|
||||
raise ImportError(u"User model is not to be found.")
|
||||
|
||||
|
||||
# First implementation of Django class-based views did not include head method
|
||||
# in base View class - https://code.djangoproject.com/ticket/15668
|
||||
if django.VERSION >= (1, 4):
|
||||
|
@ -66,6 +96,12 @@ else:
|
|||
update_wrapper(view, cls.dispatch, assigned=())
|
||||
return view
|
||||
|
||||
# Taken from @markotibold's attempt at supporting PATCH.
|
||||
# https://github.com/markotibold/django-rest-framework/tree/patch
|
||||
http_method_names = set(View.http_method_names)
|
||||
http_method_names.add('patch')
|
||||
View.http_method_names = list(http_method_names) # PATCH method is not implemented by Django
|
||||
|
||||
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
|
||||
if django.VERSION >= (1, 4):
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
|
@ -340,7 +376,7 @@ try:
|
|||
"""
|
||||
|
||||
extensions = ['headerid(level=2)']
|
||||
safe_mode = False,
|
||||
safe_mode = False
|
||||
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
|
||||
return md.convert(text)
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ def api_view(http_method_names):
|
|||
)
|
||||
|
||||
# Note, the above allows us to set the docstring.
|
||||
# It is the equivelent of:
|
||||
# It is the equivalent of:
|
||||
#
|
||||
# class WrappedAPIView(APIView):
|
||||
# pass
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import copy
|
||||
import datetime
|
||||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
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, smart_unicode
|
||||
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 urlparse import urlparse
|
||||
|
||||
|
||||
def is_simple_callable(obj):
|
||||
|
@ -28,9 +28,12 @@ def is_simple_callable(obj):
|
|||
|
||||
|
||||
class Field(object):
|
||||
read_only = True
|
||||
creation_counter = 0
|
||||
empty = ''
|
||||
type_name = None
|
||||
_use_files = None
|
||||
form_field_class = forms.CharField
|
||||
|
||||
def __init__(self, source=None):
|
||||
self.parent = None
|
||||
|
@ -50,8 +53,10 @@ class Field(object):
|
|||
self.parent = parent
|
||||
self.root = parent.root or parent
|
||||
self.context = self.root.context
|
||||
if self.root.partial:
|
||||
self.required = False
|
||||
|
||||
def field_from_native(self, data, field_name, into):
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
"""
|
||||
Given a dictionary and a field name, updates the dictionary `into`,
|
||||
with the field and it's deserialized value.
|
||||
|
@ -125,7 +130,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 = {}
|
||||
|
@ -166,7 +171,7 @@ class WritableField(Field):
|
|||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def field_from_native(self, data, field_name, into):
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
"""
|
||||
Given a dictionary and a field name, updates the dictionary `into`,
|
||||
with the field and it's deserialized value.
|
||||
|
@ -175,9 +180,14 @@ class WritableField(Field):
|
|||
return
|
||||
|
||||
try:
|
||||
native = data[field_name]
|
||||
if self._use_files:
|
||||
files = files or {}
|
||||
native = files[field_name]
|
||||
else:
|
||||
native = data[field_name]
|
||||
except KeyError:
|
||||
if self.default is not None:
|
||||
if self.default is not None and not self.root.partial:
|
||||
# Note: partial updates shouldn't set defaults
|
||||
native = self.default
|
||||
else:
|
||||
if self.required:
|
||||
|
@ -209,8 +219,19 @@ class ModelField(WritableField):
|
|||
self.model_field = kwargs.pop('model_field')
|
||||
except:
|
||||
raise ValueError("ModelField requires 'model_field' kwarg")
|
||||
|
||||
self.min_length = kwargs.pop('min_length',
|
||||
getattr(self.model_field, 'min_length', None))
|
||||
self.max_length = kwargs.pop('max_length',
|
||||
getattr(self.model_field, 'max_length', None))
|
||||
|
||||
super(ModelField, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.min_length is not None:
|
||||
self.validators.append(validators.MinLengthValidator(self.min_length))
|
||||
if self.max_length is not None:
|
||||
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
||||
|
||||
def from_native(self, value):
|
||||
rel = getattr(self.model_field, "rel", None)
|
||||
if rel is not None:
|
||||
|
@ -229,428 +250,12 @@ 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_unicode(obj)
|
||||
ident = smart_unicode(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 serializier 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, 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, 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
|
||||
|
||||
# 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_unicode(obj)
|
||||
ident = smart_unicode(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_unicode(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
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(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_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
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 #####
|
||||
|
||||
class BooleanField(WritableField):
|
||||
type_name = 'BooleanField'
|
||||
form_field_class = forms.BooleanField
|
||||
widget = widgets.CheckboxInput
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value must be either True or False."),
|
||||
|
@ -663,15 +268,16 @@ 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)
|
||||
|
||||
|
||||
class CharField(WritableField):
|
||||
type_name = 'CharField'
|
||||
form_field_class = forms.CharField
|
||||
|
||||
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
||||
self.max_length, self.min_length = max_length, min_length
|
||||
|
@ -697,8 +303,26 @@ class CharField(WritableField):
|
|||
return smart_unicode(value)
|
||||
|
||||
|
||||
class URLField(CharField):
|
||||
type_name = 'URLField'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 200)
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class SlugField(CharField):
|
||||
type_name = 'SlugField'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 50)
|
||||
super(SlugField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ChoiceField(WritableField):
|
||||
type_name = 'ChoiceField'
|
||||
form_field_class = forms.ChoiceField
|
||||
widget = widgets.Select
|
||||
default_error_messages = {
|
||||
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
|
||||
|
@ -738,13 +362,14 @@ class ChoiceField(WritableField):
|
|||
if value == smart_unicode(k2):
|
||||
return True
|
||||
else:
|
||||
if value == smart_unicode(k):
|
||||
if value == smart_unicode(k) or value == k:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class EmailField(CharField):
|
||||
type_name = 'EmailField'
|
||||
form_field_class = forms.EmailField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a valid e-mail address.'),
|
||||
|
@ -765,8 +390,39 @@ class EmailField(CharField):
|
|||
return result
|
||||
|
||||
|
||||
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)
|
||||
self.regex = regex
|
||||
|
||||
def _get_regex(self):
|
||||
return self._regex
|
||||
|
||||
def _set_regex(self, regex):
|
||||
if isinstance(regex, basestring):
|
||||
regex = re.compile(regex)
|
||||
self._regex = regex
|
||||
if hasattr(self, '_regex_validator') and self._regex_validator in self.validators:
|
||||
self.validators.remove(self._regex_validator)
|
||||
self._regex_validator = validators.RegexValidator(regex=regex)
|
||||
self.validators.append(self._regex_validator)
|
||||
|
||||
regex = property(_get_regex, _set_regex)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
result = copy.copy(self)
|
||||
memo[id(self)] = result
|
||||
result.validators = self.validators[:]
|
||||
return result
|
||||
|
||||
|
||||
class DateField(WritableField):
|
||||
type_name = 'DateField'
|
||||
widget = widgets.DateInput
|
||||
form_field_class = forms.DateField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value has an invalid date format. It must be "
|
||||
|
@ -804,6 +460,8 @@ class DateField(WritableField):
|
|||
|
||||
class DateTimeField(WritableField):
|
||||
type_name = 'DateTimeField'
|
||||
widget = widgets.DateTimeInput
|
||||
form_field_class = forms.DateTimeField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value has an invalid format. It must be in "
|
||||
|
@ -858,6 +516,7 @@ class DateTimeField(WritableField):
|
|||
|
||||
class IntegerField(WritableField):
|
||||
type_name = 'IntegerField'
|
||||
form_field_class = forms.IntegerField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a whole number.'),
|
||||
|
@ -887,6 +546,7 @@ class IntegerField(WritableField):
|
|||
|
||||
class FloatField(WritableField):
|
||||
type_name = 'FloatField'
|
||||
form_field_class = forms.FloatField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _("'%s' value must be a float."),
|
||||
|
@ -901,3 +561,108 @@ class FloatField(WritableField):
|
|||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid'] % value
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
class FileField(WritableField):
|
||||
_use_files = True
|
||||
type_name = 'FileField'
|
||||
form_field_class = forms.FileField
|
||||
widget = widgets.FileInput
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _("No file was submitted. Check the encoding type on the form."),
|
||||
'missing': _("No file was submitted."),
|
||||
'empty': _("The submitted file is empty."),
|
||||
'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
|
||||
'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||
super(FileField, self).__init__(*args, **kwargs)
|
||||
|
||||
def from_native(self, data):
|
||||
if data in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
# UploadedFile objects should have name and size attributes.
|
||||
try:
|
||||
file_name = data.name
|
||||
file_size = data.size
|
||||
except AttributeError:
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
|
||||
if self.max_length is not None and len(file_name) > self.max_length:
|
||||
error_values = {'max': self.max_length, 'length': len(file_name)}
|
||||
raise ValidationError(self.error_messages['max_length'] % error_values)
|
||||
if not file_name:
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
if not self.allow_empty_file and not file_size:
|
||||
raise ValidationError(self.error_messages['empty'])
|
||||
|
||||
return data
|
||||
|
||||
def to_native(self, value):
|
||||
return value.name
|
||||
|
||||
|
||||
class ImageField(FileField):
|
||||
_use_files = True
|
||||
form_field_class = forms.ImageField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
|
||||
}
|
||||
|
||||
def from_native(self, data):
|
||||
"""
|
||||
Checks that the file-upload field data contains a valid image (GIF, JPG,
|
||||
PNG, possibly others -- whatever the Python Imaging Library supports).
|
||||
"""
|
||||
f = super(ImageField, self).from_native(data)
|
||||
if f is None:
|
||||
return None
|
||||
|
||||
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.
|
||||
if hasattr(data, 'temporary_file_path'):
|
||||
file = data.temporary_file_path()
|
||||
else:
|
||||
if hasattr(data, 'read'):
|
||||
file = BytesIO(data.read())
|
||||
else:
|
||||
file = BytesIO(data['content'])
|
||||
|
||||
try:
|
||||
# load() could spot a truncated JPEG, but it loads the entire
|
||||
# image in memory, which is a DoS vector. See #3848 and #18520.
|
||||
# verify() must be called immediately after the constructor.
|
||||
Image.open(file).verify()
|
||||
except ImportError:
|
||||
# Under PyPy, it is possible to import PIL. However, the underlying
|
||||
# _imaging C module isn't available, so an ImportError will be
|
||||
# raised. Catch and re-raise.
|
||||
raise
|
||||
except Exception: # Python Imaging Library doesn't recognize it as an image
|
||||
raise ValidationError(self.error_messages['invalid_image'])
|
||||
if hasattr(f, 'seek') and callable(f.seek):
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
class SerializerMethodField(Field):
|
||||
"""
|
||||
A field that gets its value by calling a method on the serializer it's attached to.
|
||||
"""
|
||||
|
||||
def __init__(self, method_name):
|
||||
self.method_name = method_name
|
||||
super(SerializerMethodField, self).__init__()
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(self.parent, self.method_name)(obj)
|
||||
return self.to_native(value)
|
||||
|
|
|
@ -45,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
class AutoFilterSet(self.default_filter_set):
|
||||
class Meta:
|
||||
model = view_model
|
||||
fields = filter_fields
|
||||
fields = filter_fields
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
|
|
@ -14,6 +14,8 @@ class GenericAPIView(views.APIView):
|
|||
"""
|
||||
Base class for all other generic views.
|
||||
"""
|
||||
|
||||
model = None
|
||||
serializer_class = None
|
||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
|
||||
|
@ -30,8 +32,10 @@ class GenericAPIView(views.APIView):
|
|||
def get_serializer_class(self):
|
||||
"""
|
||||
Return the class to use for the serializer.
|
||||
Use `self.serializer_class`, falling back to constructing a
|
||||
model serializer class from `self.model_serializer_class`
|
||||
|
||||
Defaults to using `self.serializer_class`, falls back to constructing a
|
||||
model serializer class using `self.model_serializer_class`, with
|
||||
`self.model` as the model.
|
||||
"""
|
||||
serializer_class = self.serializer_class
|
||||
|
||||
|
@ -43,12 +47,16 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
return serializer_class
|
||||
|
||||
def get_serializer(self, instance=None, data=None, files=None):
|
||||
# TODO: add support for files
|
||||
# TODO: add support for seperate serializer/deserializer
|
||||
def get_serializer(self, instance=None, data=None,
|
||||
files=None, partial=False):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
context = self.get_serializer_context()
|
||||
return serializer_class(instance, data=data, context=context)
|
||||
return serializer_class(instance, data=data, files=files,
|
||||
partial=partial, context=context)
|
||||
|
||||
|
||||
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
||||
|
@ -56,47 +64,59 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
|||
Base class for generic views onto a queryset.
|
||||
"""
|
||||
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
filter_backend = api_settings.FILTER_BACKEND
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Given a queryset, filter it with whichever filter backend is in use.
|
||||
"""
|
||||
if not self.filter_backend:
|
||||
return queryset
|
||||
backend = self.filter_backend()
|
||||
return backend.filter_queryset(self.request, queryset, self)
|
||||
|
||||
def get_filtered_queryset(self):
|
||||
return self.filter_queryset(self.get_queryset())
|
||||
|
||||
def get_pagination_serializer_class(self):
|
||||
def get_pagination_serializer(self, page=None):
|
||||
"""
|
||||
Return the class to use for the pagination serializer.
|
||||
Return a serializer instance to use with paginated data.
|
||||
"""
|
||||
class SerializerClass(self.pagination_serializer_class):
|
||||
class Meta:
|
||||
object_serializer_class = self.get_serializer_class()
|
||||
|
||||
return SerializerClass
|
||||
|
||||
def get_pagination_serializer(self, page=None):
|
||||
pagination_serializer_class = self.get_pagination_serializer_class()
|
||||
pagination_serializer_class = SerializerClass
|
||||
context = self.get_serializer_context()
|
||||
return pagination_serializer_class(instance=page, context=context)
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
"""
|
||||
Return the size of pages to use with pagination.
|
||||
"""
|
||||
if self.paginate_by_param:
|
||||
query_params = self.request.QUERY_PARAMS
|
||||
try:
|
||||
return int(query_params[self.paginate_by_param])
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
return self.paginate_by
|
||||
|
||||
|
||||
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
|
||||
"""
|
||||
Base class for generic views onto a model instance.
|
||||
"""
|
||||
|
||||
pk_url_kwarg = 'pk' # Not provided in Django 1.3
|
||||
slug_url_kwarg = 'slug' # Not provided in Django 1.3
|
||||
slug_field = 'slug'
|
||||
|
||||
def get_object(self):
|
||||
def get_object(self, queryset=None):
|
||||
"""
|
||||
Override default to add support for object-level permissions.
|
||||
"""
|
||||
obj = super(SingleObjectAPIView, self).get_object()
|
||||
obj = super(SingleObjectAPIView, self).get_object(queryset)
|
||||
if not self.has_permission(self.request, obj):
|
||||
self.permission_denied(self.request)
|
||||
return obj
|
||||
|
@ -153,6 +173,10 @@ class UpdateAPIView(mixins.UpdateModelMixin,
|
|||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ListCreateAPIView(mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
|
@ -167,6 +191,23 @@ class ListCreateAPIView(mixins.ListModelMixin,
|
|||
return self.create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
SingleObjectAPIView):
|
||||
"""
|
||||
Concrete view for retrieving, updating a model instance.
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
SingleObjectAPIView):
|
||||
|
@ -193,5 +234,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
|
|||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return self.destroy(request, *args, **kwargs)
|
||||
|
|
|
@ -15,13 +15,23 @@ class CreateModelMixin(object):
|
|||
Should be mixed in with any `BaseView`.
|
||||
"""
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA)
|
||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get_success_headers(self, data):
|
||||
try:
|
||||
return {'Location': data['url']}
|
||||
except (TypeError, KeyError):
|
||||
return {}
|
||||
|
||||
def pre_save(self, obj):
|
||||
pass
|
||||
|
||||
|
@ -34,14 +44,16 @@ class ListModelMixin(object):
|
|||
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
self.object_list = self.get_filtered_queryset()
|
||||
queryset = self.get_queryset()
|
||||
self.object_list = self.filter_queryset(queryset)
|
||||
|
||||
# Default is to allow empty querysets. This can be altered by setting
|
||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||
allow_empty = self.get_allow_empty()
|
||||
if not allow_empty and len(self.object_list) == 0:
|
||||
error_args = {'class_name': self.__class__.__name__}
|
||||
raise Http404(self.empty_error % error_args)
|
||||
if not allow_empty and not self.object_list:
|
||||
class_name = self.__class__.__name__
|
||||
error_msg = self.empty_error % {'class_name': class_name}
|
||||
raise Http404(error_msg)
|
||||
|
||||
# Pagination size is set by the `.paginate_by` attribute,
|
||||
# which may be `None` to disable pagination.
|
||||
|
@ -73,19 +85,21 @@ class UpdateModelMixin(object):
|
|||
Should be mixed in with `SingleObjectBaseView`.
|
||||
"""
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
success_status = status.HTTP_200_OK
|
||||
success_status_code = status.HTTP_200_OK
|
||||
except Http404:
|
||||
self.object = None
|
||||
success_status = status.HTTP_201_CREATED
|
||||
success_status_code = status.HTTP_201_CREATED
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA)
|
||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||
files=request.FILES, partial=partial)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data, status=success_status)
|
||||
return Response(serializer.data, status=success_status_code)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
@ -103,6 +117,11 @@ 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.
|
||||
if hasattr(obj, 'full_clean'):
|
||||
obj.full_clean()
|
||||
|
||||
|
||||
class DestroyModelMixin(object):
|
||||
"""
|
||||
|
@ -110,6 +129,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)
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.http import Http404
|
|||
from rest_framework import exceptions
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
|
||||
from rest_framework.utils.mediatypes import _MediaType
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
|
@ -48,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
for media_type in media_type_set:
|
||||
if media_type_matches(renderer.media_type, media_type):
|
||||
# Return the most specific media type as accepted.
|
||||
if len(renderer.media_type) > len(media_type):
|
||||
if (_MediaType(renderer.media_type).precedence >
|
||||
_MediaType(media_type).precedence):
|
||||
# Eg client requests '*/*'
|
||||
# Accepted media type is 'application/json'
|
||||
return renderer, renderer.media_type
|
||||
|
|
|
@ -8,11 +8,11 @@ on the request, such as form content or json encoded data.
|
|||
from django.http import QueryDict
|
||||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||
from django.http.multipartparser import MultiPartParserError
|
||||
from django.utils import simplejson as json
|
||||
from rest_framework.compat import yaml, ETParseError
|
||||
from rest_framework.exceptions import ParseError
|
||||
from xml.etree import ElementTree as ET
|
||||
from xml.parsers.expat import ExpatError
|
||||
import json
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
|
|
503
rest_framework/relations.py
Normal file
503
rest_framework/relations.py
Normal file
|
@ -0,0 +1,503 @@
|
|||
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
|
||||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(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
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
# 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_unicode(obj)
|
||||
ident = smart_unicode(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 = self.error_messages['does_not_exist'] % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid']
|
||||
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
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(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 = self.error_messages['does_not_exist'] % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid']
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
default_error_messages = {
|
||||
'does_not_exist': _("Object with %s=%s does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop('slug_field', None)
|
||||
assert self.slug_field, 'slug_field is required'
|
||||
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(self.error_messages['does_not_exist'] %
|
||||
(self.slug_field, unicode(data)))
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid']
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
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
|
||||
|
||||
default_error_messages = {
|
||||
'no_match': _('Invalid hyperlink - No URL match'),
|
||||
'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
|
||||
'configuration_error': _('Invalid hyperlink due to configuration error'),
|
||||
'does_not_exist': _("Invalid hyperlink - object does not exist."),
|
||||
'invalid': _('Invalid value.'),
|
||||
}
|
||||
|
||||
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 Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
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 Exception('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')
|
||||
|
||||
try:
|
||||
http_prefix = value.startswith('http:') or value.startswith('https:')
|
||||
except AttributeError:
|
||||
msg = self.error_messages['invalid']
|
||||
raise ValidationError(msg)
|
||||
|
||||
if http_prefix:
|
||||
# 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(self.error_messages['no_match'])
|
||||
|
||||
if match.view_name != self.view_name:
|
||||
raise ValidationError(self.error_messages['incorrect_match'])
|
||||
|
||||
pk = match.kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = match.kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
# 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 probably a configuation error.
|
||||
else:
|
||||
raise ValidationError(self.error_messages['configuration_error'])
|
||||
|
||||
try:
|
||||
obj = queryset.get()
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(self.error_messages['does_not_exist'])
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid']
|
||||
raise ValidationError(msg)
|
||||
|
||||
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)
|
||||
# Optionally the format of the target hyperlink may be specified
|
||||
self.format = kwargs.pop('format', None)
|
||||
|
||||
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||
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.context.get('format', None)
|
||||
view_name = self.view_name or self.parent.opts.view_name
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
|
||||
# By default use whatever format is given for the current context
|
||||
# unless the target is a different type to the source.
|
||||
#
|
||||
# Eg. Consider a HyperlinkedIdentityField pointing from a json
|
||||
# representation to an html property of that representation...
|
||||
#
|
||||
# '/snippets/1/' should link to '/snippets/1/highlight/'
|
||||
# ...but...
|
||||
# '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
|
||||
if format and self.format and self.format != format:
|
||||
format = self.format
|
||||
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
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 Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
|
@ -4,14 +4,14 @@ Renderers are used to serialize a response into specific media types.
|
|||
They give us a generic way of being able to handle various media types
|
||||
on the response, such as JSON encoded data or HTML output.
|
||||
|
||||
REST framework also provides an HTML renderer the renders the browseable API.
|
||||
REST framework also provides an HTML renderer the renders the browsable API.
|
||||
"""
|
||||
import copy
|
||||
import string
|
||||
import json
|
||||
from django import forms
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.template import RequestContext, loader, Template
|
||||
from django.utils import simplejson as json
|
||||
from rest_framework.compat import yaml
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -19,8 +19,8 @@ from rest_framework.request import clone_request
|
|||
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
|
||||
from rest_framework import serializers, parsers
|
||||
from rest_framework import VERSION, status
|
||||
from rest_framework import parsers
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
|
@ -306,25 +306,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
return True
|
||||
|
||||
def serializer_to_form_fields(self, serializer):
|
||||
field_mapping = {
|
||||
serializers.FloatField: forms.FloatField,
|
||||
serializers.IntegerField: forms.IntegerField,
|
||||
serializers.DateTimeField: forms.DateTimeField,
|
||||
serializers.DateField: forms.DateField,
|
||||
serializers.EmailField: forms.EmailField,
|
||||
serializers.CharField: forms.CharField,
|
||||
serializers.ChoiceField: forms.ChoiceField,
|
||||
serializers.BooleanField: forms.BooleanField,
|
||||
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
|
||||
serializers.SlugRelatedField: forms.ChoiceField,
|
||||
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
||||
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
||||
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
|
||||
}
|
||||
|
||||
fields = {}
|
||||
for k, v in serializer.get_fields(True).items():
|
||||
for k, v in serializer.get_fields().items():
|
||||
if getattr(v, 'read_only', True):
|
||||
continue
|
||||
|
||||
|
@ -337,6 +320,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
|
||||
|
@ -346,13 +332,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
kwargs['label'] = k
|
||||
|
||||
try:
|
||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||
except KeyError:
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
fields[k] = forms.ChoiceField(**kwargs)
|
||||
else:
|
||||
fields[k] = forms.CharField(**kwargs)
|
||||
fields[k] = v.form_field_class(**kwargs)
|
||||
return fields
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
|
@ -479,7 +459,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
# Munge DELETE Response code to allow us to return content
|
||||
# (Do this *after* we've rendered the template so that we include
|
||||
# the normal deletion response code in the output)
|
||||
if response.status_code == 204:
|
||||
response.status_code = 200
|
||||
if response.status_code == status.HTTP_204_NO_CONTENT:
|
||||
response.status_code = status.HTTP_200_OK
|
||||
|
||||
return ret
|
||||
|
|
|
@ -170,6 +170,15 @@ class Request(object):
|
|||
self._authenticator, 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._authenticator, 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
|
||||
|
||||
@property
|
||||
def successful_authenticator(self):
|
||||
"""
|
||||
|
|
|
@ -15,14 +15,17 @@ class Response(SimpleTemplateResponse):
|
|||
Alters the init arguments slightly.
|
||||
For example, drop 'template_name', and instead use 'data'.
|
||||
|
||||
Setting 'renderer' and 'media_type' will typically be defered,
|
||||
Setting 'renderer' and 'media_type' will typically be deferred,
|
||||
For example being set automatically by the `APIView`.
|
||||
"""
|
||||
super(Response, self).__init__(None, status=status)
|
||||
self.data = data
|
||||
self.headers = headers and headers[:] or []
|
||||
self.template_name = template_name
|
||||
self.exception = exception
|
||||
|
||||
if headers:
|
||||
for name,value in headers.iteritems():
|
||||
self[name] = value
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('',
|
||||
)
|
||||
|
|
|
@ -14,7 +14,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 *
|
||||
|
||||
|
||||
|
@ -22,7 +22,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):
|
||||
|
@ -60,7 +69,7 @@ def _get_declared_fields(bases, attrs):
|
|||
|
||||
# If this class is subclassing another Serializer, add that Serializer's
|
||||
# fields. Note that we loop over the bases in *reverse*. This is necessary
|
||||
# in order to the correct order of fields.
|
||||
# in order to maintain the correct order of fields.
|
||||
for base in bases[::-1]:
|
||||
if hasattr(base, 'base_fields'):
|
||||
fields = base.base_fields.items() + fields
|
||||
|
@ -89,50 +98,53 @@ class BaseSerializer(Field):
|
|||
pass
|
||||
|
||||
_options_class = SerializerOptions
|
||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations.
|
||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
|
||||
|
||||
def __init__(self, instance=None, data=None, context=None, **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.fields = copy.deepcopy(self.base_fields)
|
||||
self.parent = None
|
||||
self.root = None
|
||||
self.partial = partial
|
||||
|
||||
self.context = context or {}
|
||||
|
||||
self.init_data = data
|
||||
self.init_files = files
|
||||
self.object = instance
|
||||
self.fields = self.get_fields()
|
||||
|
||||
self._data = None
|
||||
self._files = None
|
||||
self._errors = None
|
||||
|
||||
#####
|
||||
# Methods to determine which fields to use when (de)serializing objects.
|
||||
|
||||
def default_fields(self, nested=False):
|
||||
def get_default_fields(self):
|
||||
"""
|
||||
Return the complete set of default fields for the object, as a dict.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_fields(self, nested=False):
|
||||
def get_fields(self):
|
||||
"""
|
||||
Returns the complete set of fields for the object as a dict.
|
||||
|
||||
This will be the set of any explicitly declared fields,
|
||||
plus the set of fields returned by default_fields().
|
||||
plus the set of fields returned by get_default_fields().
|
||||
"""
|
||||
ret = SortedDict()
|
||||
|
||||
# Get the explicitly declared fields
|
||||
for key, field in self.fields.items():
|
||||
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
|
||||
fields = self.default_fields(nested)
|
||||
for key, val in fields.items():
|
||||
default_fields = self.get_default_fields()
|
||||
for key, val in default_fields.items():
|
||||
if key not in ret:
|
||||
ret[key] = val
|
||||
|
||||
|
@ -148,6 +160,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
|
||||
|
||||
#####
|
||||
|
@ -163,7 +178,7 @@ class BaseSerializer(Field):
|
|||
self.opts.depth = parent.opts.depth - 1
|
||||
|
||||
#####
|
||||
# Methods to convert or revert from objects <--> primative representations.
|
||||
# Methods to convert or revert from objects <--> primitive representations.
|
||||
|
||||
def get_field_key(self, field_name):
|
||||
"""
|
||||
|
@ -179,24 +194,29 @@ class BaseSerializer(Field):
|
|||
ret = self._dict_class()
|
||||
ret.fields = {}
|
||||
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
for field_name, field in fields.items():
|
||||
for field_name, field in self.fields.items():
|
||||
field.initialize(parent=self, field_name=field_name)
|
||||
key = self.get_field_key(field_name)
|
||||
value = field.field_to_native(obj, field_name)
|
||||
ret[key] = value
|
||||
ret.fields[key] = field
|
||||
return ret
|
||||
|
||||
def restore_fields(self, data):
|
||||
def restore_fields(self, data, files):
|
||||
"""
|
||||
Core of deserialization, together with `restore_object`.
|
||||
Converts a dictionary of data into a dictionary of deserialized fields.
|
||||
"""
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
reverted_data = {}
|
||||
for field_name, field in fields.items():
|
||||
|
||||
if data is not None and not isinstance(data, dict):
|
||||
self._errors['non_field_errors'] = [u'Invalid data']
|
||||
return None
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
field.initialize(parent=self, field_name=field_name)
|
||||
try:
|
||||
field.field_from_native(data, field_name, reverted_data)
|
||||
field.field_from_native(data, files, field_name, reverted_data)
|
||||
except ValidationError as err:
|
||||
self._errors[field_name] = list(err.messages)
|
||||
|
||||
|
@ -206,10 +226,7 @@ class BaseSerializer(Field):
|
|||
"""
|
||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
||||
"""
|
||||
# TODO: refactor this so we're not determining the fields again
|
||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
||||
|
||||
for field_name, field in fields.items():
|
||||
for field_name, field in self.fields.items():
|
||||
try:
|
||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
||||
if validate_method:
|
||||
|
@ -218,10 +235,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
|
||||
|
||||
|
@ -244,23 +269,23 @@ class BaseSerializer(Field):
|
|||
|
||||
def to_native(self, obj):
|
||||
"""
|
||||
Serialize objects -> primatives.
|
||||
Serialize objects -> primitives.
|
||||
"""
|
||||
if hasattr(obj, '__iter__'):
|
||||
return [self.convert_object(item) for item in obj]
|
||||
return self.convert_object(obj)
|
||||
|
||||
def from_native(self, data):
|
||||
def from_native(self, data, files):
|
||||
"""
|
||||
Deserialize primatives -> objects.
|
||||
Deserialize primitives -> objects.
|
||||
"""
|
||||
if hasattr(data, '__iter__') and not isinstance(data, dict):
|
||||
# TODO: error data when deserializing lists
|
||||
return (self.from_native(item) for item in data)
|
||||
return [self.from_native(item, None) for item in data]
|
||||
|
||||
self._errors = {}
|
||||
if data is not None:
|
||||
attrs = self.restore_fields(data)
|
||||
if data is not None or files is not None:
|
||||
attrs = self.restore_fields(data, files)
|
||||
attrs = self.perform_validation(attrs)
|
||||
else:
|
||||
self._errors['non_field_errors'] = ['No input provided']
|
||||
|
@ -273,12 +298,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 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
|
||||
|
@ -288,7 +324,7 @@ class BaseSerializer(Field):
|
|||
setting self.object if no errors occurred.
|
||||
"""
|
||||
if self._errors is None:
|
||||
obj = self.from_native(self.init_data)
|
||||
obj = self.from_native(self.init_data, self.init_files)
|
||||
if not self._errors:
|
||||
self.object = obj
|
||||
return self._errors
|
||||
|
@ -321,6 +357,7 @@ class ModelSerializerOptions(SerializerOptions):
|
|||
def __init__(self, meta):
|
||||
super(ModelSerializerOptions, self).__init__(meta)
|
||||
self.model = getattr(meta, 'model', None)
|
||||
self.read_only_fields = getattr(meta, 'read_only_fields', ())
|
||||
|
||||
|
||||
class ModelSerializer(Serializer):
|
||||
|
@ -329,16 +366,10 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
_options_class = ModelSerializerOptions
|
||||
|
||||
def default_fields(self, nested=False):
|
||||
def get_default_fields(self):
|
||||
"""
|
||||
Return all the fields that should be serialized for the model.
|
||||
"""
|
||||
# TODO: Modfiy this so that it's called on init, and drop
|
||||
# serialize/obj/data arguments.
|
||||
#
|
||||
# We *could* provide a hook for dynamic fields, but
|
||||
# it'd be nice if the default was to generate fields statically
|
||||
# at the point of __init__
|
||||
|
||||
cls = self.opts.model
|
||||
opts = get_concrete_model(cls)._meta
|
||||
|
@ -350,6 +381,7 @@ class ModelSerializer(Serializer):
|
|||
fields += [field for field in opts.many_to_many if field.serialize]
|
||||
|
||||
ret = SortedDict()
|
||||
nested = bool(self.opts.depth)
|
||||
is_pk = True # First field in the list is the pk
|
||||
|
||||
for model_field in fields:
|
||||
|
@ -366,9 +398,14 @@ 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:
|
||||
assert field_name in ret, \
|
||||
"read_only_fields on '%s' included invalid item '%s'" % \
|
||||
(self.__class__.__name__, field_name)
|
||||
ret[field_name].read_only = True
|
||||
|
||||
return ret
|
||||
|
||||
def get_pk_field(self, model_field):
|
||||
|
@ -381,7 +418,10 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
Creates a default instance of a nested relational field.
|
||||
"""
|
||||
return ModelSerializer()
|
||||
class NestedModelSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = model_field.rel.to
|
||||
return NestedModelSerializer()
|
||||
|
||||
def get_related_field(self, model_field, to_many=False):
|
||||
"""
|
||||
|
@ -389,10 +429,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):
|
||||
"""
|
||||
|
@ -402,7 +446,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():
|
||||
|
@ -427,49 +471,86 @@ class ModelSerializer(Serializer):
|
|||
models.DateField: DateField,
|
||||
models.EmailField: EmailField,
|
||||
models.CharField: CharField,
|
||||
models.URLField: URLField,
|
||||
models.SlugField: SlugField,
|
||||
models.TextField: CharField,
|
||||
models.CommaSeparatedIntegerField: CharField,
|
||||
models.BooleanField: BooleanField,
|
||||
models.FileField: FileField,
|
||||
models.ImageField: ImageField,
|
||||
}
|
||||
try:
|
||||
return field_mapping[model_field.__class__](**kwargs)
|
||||
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:
|
||||
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)
|
||||
|
||||
def save(self, save_m2m=True):
|
||||
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 the deserialized object and return it.
|
||||
"""
|
||||
self.object.save()
|
||||
|
||||
if getattr(self, 'm2m_data', None) and save_m2m:
|
||||
if getattr(self, 'm2m_data', None):
|
||||
for accessor_name, object_list in self.m2m_data.items():
|
||||
setattr(self.object, accessor_name, object_list)
|
||||
self.m2m_data = {}
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -516,9 +597,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:
|
||||
|
|
|
@ -54,19 +54,26 @@ DEFAULTS = {
|
|||
'user': None,
|
||||
'anon': None,
|
||||
},
|
||||
|
||||
# Pagination
|
||||
'PAGINATE_BY': None,
|
||||
'PAGINATE_BY_PARAM': None,
|
||||
|
||||
# Filtering
|
||||
'FILTER_BACKEND': None,
|
||||
|
||||
# Authentication
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
||||
# Browser enhancements
|
||||
'FORM_METHOD_OVERRIDE': '_method',
|
||||
'FORM_CONTENT_OVERRIDE': '_content',
|
||||
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
|
||||
'URL_ACCEPT_OVERRIDE': 'accept',
|
||||
'URL_FORMAT_OVERRIDE': 'format',
|
||||
|
||||
'FORMAT_SUFFIX_KWARG': 'format'
|
||||
'FORMAT_SUFFIX_KWARG': 'format',
|
||||
}
|
||||
|
||||
|
||||
|
@ -108,8 +115,8 @@ def import_from_string(val, setting_name):
|
|||
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
except:
|
||||
msg = "Could not import '%s' for API setting '%s'" % (val, setting_name)
|
||||
except ImportError as e:
|
||||
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
||||
|
@ -152,7 +159,7 @@ class APISettings(object):
|
|||
|
||||
def validate_setting(self, attr, val):
|
||||
if attr == 'FILTER_BACKEND' and val is not None:
|
||||
# Make sure we can initilize the class
|
||||
# Make sure we can initialize the class
|
||||
val()
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -11,6 +11,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.staticfiles 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
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from django.utils import simplejson as json
|
||||
from django.http import HttpResponse
|
||||
|
||||
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 json
|
||||
import base64
|
||||
|
||||
|
||||
|
@ -27,6 +25,7 @@ MockView.authentication_classes += (TokenAuthentication,)
|
|||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', MockView.as_view()),
|
||||
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -152,3 +151,33 @@ class TokenAuthTests(TestCase):
|
|||
self.token.delete()
|
||||
token = Token.objects.create(user=self.user)
|
||||
self.assertTrue(bool(token.key))
|
||||
|
||||
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/',
|
||||
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content)['token'], self.key)
|
||||
|
||||
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/',
|
||||
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
|
||||
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/',
|
||||
json.dumps({'username': self.username}), 'application/json')
|
||||
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/',
|
||||
{'username': self.username, 'password': self.password})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content)['token'], self.key)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -17,6 +17,8 @@ from rest_framework.decorators import (
|
|||
permission_classes,
|
||||
)
|
||||
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
|
||||
|
||||
class DecoratorTestCase(TestCase):
|
||||
|
||||
|
@ -63,6 +65,20 @@ class DecoratorTestCase(TestCase):
|
|||
response = view(request)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_calling_patch_method(self):
|
||||
|
||||
@api_view(['GET', 'PATCH'])
|
||||
def view(request):
|
||||
return Response({})
|
||||
|
||||
request = self.factory.patch('/')
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
request = self.factory.post('/')
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_renderer_classes(self):
|
||||
|
||||
@api_view(['GET'])
|
||||
|
|
0
rest_framework/tests/extras/__init__.py
Normal file
0
rest_framework/tests/extras/__init__.py
Normal file
1
rest_framework/tests/extras/bad_import.py
Normal file
1
rest_framework/tests/extras/bad_import.py
Normal file
|
@ -0,0 +1 @@
|
|||
raise ValueError
|
|
@ -1,34 +1,51 @@
|
|||
# from django.test import TestCase
|
||||
# from django import forms
|
||||
import StringIO
|
||||
import datetime
|
||||
|
||||
# from django.test.client import RequestFactory
|
||||
# from rest_framework.views import View
|
||||
# from rest_framework.response import Response
|
||||
from django.test import TestCase
|
||||
|
||||
# import StringIO
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# class UploadFilesTests(TestCase):
|
||||
# """Check uploading of files"""
|
||||
# def setUp(self):
|
||||
# self.factory = RequestFactory()
|
||||
class UploadedFile(object):
|
||||
def __init__(self, file, created=None):
|
||||
self.file = file
|
||||
self.created = created or datetime.datetime.now()
|
||||
|
||||
# def test_upload_file(self):
|
||||
|
||||
# class FileForm(forms.Form):
|
||||
# file = forms.FileField()
|
||||
class UploadedFileSerializer(serializers.Serializer):
|
||||
file = serializers.FileField()
|
||||
created = serializers.DateTimeField()
|
||||
|
||||
# class MockView(View):
|
||||
# permissions = ()
|
||||
# form = FileForm
|
||||
def restore_object(self, attrs, instance=None):
|
||||
if instance:
|
||||
instance.file = attrs['file']
|
||||
instance.created = attrs['created']
|
||||
return instance
|
||||
return UploadedFile(**attrs)
|
||||
|
||||
# def post(self, request, *args, **kwargs):
|
||||
# return Response({'FILE_NAME': self.CONTENT['file'].name,
|
||||
# 'FILE_CONTENT': self.CONTENT['file'].read()})
|
||||
|
||||
# file = StringIO.StringIO('stuff')
|
||||
# file.name = 'stuff.txt'
|
||||
# request = self.factory.post('/', {'file': file})
|
||||
# view = MockView.as_view()
|
||||
# response = view(request)
|
||||
# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
|
||||
class FileSerializerTests(TestCase):
|
||||
def test_create(self):
|
||||
now = datetime.datetime.now()
|
||||
file = StringIO.StringIO('stuff')
|
||||
file.name = 'stuff.txt'
|
||||
file.size = file.len
|
||||
serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
|
||||
uploaded_file = UploadedFile(file=file, created=now)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.object.created, uploaded_file.created)
|
||||
self.assertEquals(serializer.object.file, uploaded_file.file)
|
||||
self.assertFalse(serializer.object is uploaded_file)
|
||||
|
||||
def test_creation_failure(self):
|
||||
"""
|
||||
Passing files=None should result in an ValidationError
|
||||
|
||||
Regression test for:
|
||||
https://github.com/tomchristie/django-rest-framework/issues/542
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
|
||||
serializer = UploadedFileSerializer(data={'created': now})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('file', serializer.errors)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import json
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import simplejson as json
|
||||
from rest_framework import generics, serializers, status
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
||||
|
||||
|
||||
|
@ -174,6 +175,20 @@ class TestInstanceView(TestCase):
|
|||
content = {'text': 'foobar'}
|
||||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk='1').render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEquals(updated.text, 'foobar')
|
||||
|
||||
def test_patch_instance_view(self):
|
||||
"""
|
||||
PATCH requests to RetrieveUpdateDestroyAPIView should update an object.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.patch('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||
|
@ -301,3 +316,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)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
import json
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status, serializers
|
||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
|
||||
text = serializers.CharField()
|
||||
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
||||
|
||||
class Meta:
|
||||
model = BlogPostComment
|
||||
fields = ('text', 'blog_post_url')
|
||||
fields = ('text', 'blog_post_url', 'url')
|
||||
|
||||
|
||||
class PhotoSerializer(serializers.Serializer):
|
||||
|
@ -54,6 +56,11 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
|||
serializer_class = BlogPostCommentSerializer
|
||||
|
||||
|
||||
class BlogPostCommentDetail(generics.RetrieveAPIView):
|
||||
model = BlogPostComment
|
||||
serializer_class = BlogPostCommentSerializer
|
||||
|
||||
|
||||
class BlogPostDetail(generics.RetrieveAPIView):
|
||||
model = BlogPost
|
||||
|
||||
|
@ -67,6 +74,11 @@ class AlbumDetail(generics.RetrieveAPIView):
|
|||
model = Album
|
||||
|
||||
|
||||
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = OptionalRelationModel
|
||||
model_serializer_class = serializers.HyperlinkedModelSerializer
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
||||
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
||||
|
@ -75,8 +87,10 @@ urlpatterns = patterns('',
|
|||
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||
url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
||||
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list')
|
||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
||||
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -151,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)
|
||||
|
||||
|
@ -160,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])
|
||||
|
||||
|
@ -183,8 +197,9 @@ 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)
|
||||
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
||||
|
||||
|
@ -207,7 +222,41 @@ 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)
|
||||
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
||||
|
||||
|
||||
class TestOptionalRelationHyperlinkedView(TestCase):
|
||||
urls = 'rest_framework.tests.hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 1 OptionalRelationModel intances.
|
||||
"""
|
||||
OptionalRelationModel().save()
|
||||
self.objects = OptionalRelationModel.objects
|
||||
self.detail_view = OptionalRelationDetail.as_view()
|
||||
self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
|
||||
|
||||
def test_get_detail_view(self):
|
||||
"""
|
||||
GET requests to RetrieveAPIView with optional relations should return None
|
||||
for non existing relations.
|
||||
"""
|
||||
request = factory.get('/optionalrelationmodel-detail/1')
|
||||
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)
|
||||
|
|
|
@ -35,6 +35,13 @@ def foobar():
|
|||
return 'foobar'
|
||||
|
||||
|
||||
class CustomField(models.CharField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = 12
|
||||
super(CustomField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RESTFrameworkModel(models.Model):
|
||||
"""
|
||||
Base for test models that sets app_label, so they play nicely.
|
||||
|
@ -44,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')
|
||||
|
||||
|
@ -59,6 +71,7 @@ class SlugBasedModel(RESTFrameworkModel):
|
|||
|
||||
class DefaultValueModel(RESTFrameworkModel):
|
||||
text = models.CharField(default='foobar', max_length=100)
|
||||
extra = models.CharField(blank=True, null=True, max_length=100)
|
||||
|
||||
|
||||
class CallableDefaultValueModel(RESTFrameworkModel):
|
||||
|
@ -113,11 +126,28 @@ class Comment(RESTFrameworkModel):
|
|||
class ActionItem(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=200)
|
||||
done = models.BooleanField(default=False)
|
||||
info = CustomField(default='---', max_length=12)
|
||||
|
||||
|
||||
# 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]
|
||||
|
||||
|
||||
class BlogPostComment(RESTFrameworkModel):
|
||||
|
@ -134,18 +164,44 @@ 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
|
||||
class OptionalRelationModel(RESTFrameworkModel):
|
||||
other = models.ForeignKey('OptionalRelationModel', blank=True, null=True)
|
||||
|
||||
|
||||
# Model for RegexField
|
||||
class Book(RESTFrameworkModel):
|
||||
isbn = models.CharField(max_length=13)
|
||||
|
||||
|
||||
# Models for relations tests
|
||||
# ManyToMany
|
||||
class ManyToManyTarget(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
|
||||
|
||||
|
||||
# ForeignKey
|
||||
class ForeignKeyTarget(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ForeignKeySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
|
||||
|
||||
|
||||
# Nullable ForeignKey
|
||||
class NullableForeignKeySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
|
||||
related_name='nullable_sources')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -34,6 +34,21 @@ if django_filters:
|
|||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
|
||||
class DefaultPageSizeKwargView(generics.ListAPIView):
|
||||
"""
|
||||
View for testing default paginate_by_param usage
|
||||
"""
|
||||
model = BasicModel
|
||||
|
||||
|
||||
class PaginateByParamView(generics.ListAPIView):
|
||||
"""
|
||||
View for testing custom paginate_by_param usage
|
||||
"""
|
||||
model = BasicModel
|
||||
paginate_by_param = 'page_size'
|
||||
|
||||
|
||||
class IntegrationTestPagination(TestCase):
|
||||
"""
|
||||
Integration tests for paginated list views.
|
||||
|
@ -133,9 +148,14 @@ 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 primative objects.
|
||||
Unit tests for pagination of primitive objects.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -156,3 +176,106 @@ class UnitTestPagination(TestCase):
|
|||
self.assertEquals(serializer.data['next'], None)
|
||||
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):
|
||||
"""
|
||||
Tests for list views without pagination.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 13 BasicModel instances.
|
||||
"""
|
||||
for i in range(13):
|
||||
BasicModel(text=i).save()
|
||||
self.objects = BasicModel.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = DefaultPageSizeKwargView.as_view()
|
||||
|
||||
def test_unpaginated(self):
|
||||
"""
|
||||
Tests the default page size for this view.
|
||||
no page size --> no limit --> no meta data
|
||||
"""
|
||||
request = factory.get('/')
|
||||
response = self.view(request)
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
|
||||
class TestCustomPaginateByParam(TestCase):
|
||||
"""
|
||||
Tests for list views with default page size kwarg
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 13 BasicModel instances.
|
||||
"""
|
||||
for i in range(13):
|
||||
BasicModel(text=i).save()
|
||||
self.objects = BasicModel.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = PaginateByParamView.as_view()
|
||||
|
||||
def test_default_page_size(self):
|
||||
"""
|
||||
Tests the default page size for this view.
|
||||
no page size --> no limit --> no meta data
|
||||
"""
|
||||
request = factory.get('/')
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
def test_paginate_by_param(self):
|
||||
"""
|
||||
If paginate_by_param is set, the new kwarg should limit per view requests.
|
||||
"""
|
||||
request = factory.get('/?page_size=5')
|
||||
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)
|
||||
|
||||
|
|
|
@ -1,205 +0,0 @@
|
|||
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(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ForeignKeyTarget
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||
|
||||
class PrimaryKeyManyToManyTests(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())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure source 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(queryset)
|
||||
expected = [
|
||||
{'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())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure target 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(queryset)
|
||||
expected = [
|
||||
{'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_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 PrimaryKeyForeignKeyTests(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)
|
||||
|
||||
# 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)
|
33
rest_framework/tests/relations.py
Normal file
33
rest_framework/tests/relations.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
General tests for relational fields.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class NullModel(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class FieldTests(TestCase):
|
||||
def test_pk_related_field_with_empty_string(self):
|
||||
"""
|
||||
Regression test for #446
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/446
|
||||
"""
|
||||
field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||
|
||||
def test_hyperlinked_related_field_with_empty_string(self):
|
||||
field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||
|
||||
def test_slug_related_field_with_empty_string(self):
|
||||
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, '')
|
||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
411
rest_framework/tests/relations_hyperlink.py
Normal file
411
rest_framework/tests/relations_hyperlink.py
Normal file
|
@ -0,0 +1,411 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
|
||||
sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail')
|
||||
|
||||
class Meta:
|
||||
model = ManyToManyTarget
|
||||
|
||||
|
||||
class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
|
||||
|
||||
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)
|
84
rest_framework/tests/relations_nested.py
Normal file
84
rest_framework/tests/relations_nested.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
385
rest_framework/tests/relations_pk.py
Normal file
385
rest_framework/tests/relations_pk.py
Normal file
|
@ -0,0 +1,385 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource
|
||||
|
||||
|
||||
class ManyToManyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = ManyToManyTarget
|
||||
|
||||
|
||||
class ManyToManySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
|
||||
|
||||
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField()
|
||||
|
||||
class Meta:
|
||||
model = ForeignKeyTarget
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
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)
|
|
@ -1,13 +1,12 @@
|
|||
import pickle
|
||||
import re
|
||||
|
||||
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, \
|
||||
|
@ -444,19 +443,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):
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
"""
|
||||
Tests for content parsing, and form-overloaded content parsing.
|
||||
"""
|
||||
from django.conf.urls.defaults import patterns
|
||||
import json
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase, Client
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
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,
|
||||
|
@ -276,3 +277,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')
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import unittest
|
||||
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import datetime
|
||||
import pickle
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel)
|
||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
||||
|
||||
|
||||
class SubComment(object):
|
||||
|
@ -40,7 +41,15 @@ class CommentSerializer(serializers.Serializer):
|
|||
return instance
|
||||
|
||||
|
||||
class BookSerializer(serializers.ModelSerializer):
|
||||
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
|
||||
|
||||
class ActionItemSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ActionItem
|
||||
|
||||
|
@ -51,6 +60,20 @@ class PersonSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Person
|
||||
fields = ('name', 'age', 'info')
|
||||
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):
|
||||
|
@ -106,8 +129,21 @@ class BasicTests(TestCase):
|
|||
self.assertTrue(serializer.object is expected)
|
||||
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||
|
||||
def test_partial_update(self):
|
||||
msg = 'Merry New Year!'
|
||||
partial_data = {'content': msg}
|
||||
serializer = CommentSerializer(self.comment, data=partial_data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
|
||||
expected = self.comment
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.object, expected)
|
||||
self.assertTrue(serializer.object is expected)
|
||||
self.assertEquals(serializer.data['content'], msg)
|
||||
|
||||
def test_model_fields_as_expected(self):
|
||||
""" Make sure that the fields returned are the same as defined
|
||||
"""
|
||||
Make sure that the fields returned are the same as defined
|
||||
in the Meta data
|
||||
"""
|
||||
serializer = PersonSerializer(self.person)
|
||||
|
@ -115,12 +151,25 @@ class BasicTests(TestCase):
|
|||
set(['name', 'age', 'info']))
|
||||
|
||||
def test_field_with_dictionary(self):
|
||||
""" Make sure that dictionaries from fields are left intact
|
||||
"""
|
||||
Make sure that dictionaries from fields are left intact
|
||||
"""
|
||||
serializer = PersonSerializer(self.person)
|
||||
expected = self.person_data
|
||||
self.assertEquals(serializer.data['info'], expected)
|
||||
|
||||
def test_read_only_fields(self):
|
||||
"""
|
||||
Attempting to update fields set as read_only should have no effect.
|
||||
"""
|
||||
|
||||
serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
self.assertEquals(serializer.errors, {})
|
||||
# Assert age is unchanged (35)
|
||||
self.assertEquals(instance.age, self.person_data['age'])
|
||||
|
||||
|
||||
class ValidationTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -134,7 +183,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):
|
||||
|
@ -192,6 +241,25 @@ class ValidationTests(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'content': [u'Test not in value']})
|
||||
|
||||
def test_bad_type_data_is_false(self):
|
||||
"""
|
||||
Data of the wrong type is not valid.
|
||||
"""
|
||||
data = ['i am', 'a', 'list']
|
||||
serializer = CommentSerializer(self.comment, data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
|
||||
|
||||
data = 'and i am a string'
|
||||
serializer = CommentSerializer(self.comment, data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
|
||||
|
||||
data = 42
|
||||
serializer = CommentSerializer(self.comment, data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']})
|
||||
|
||||
def test_cross_field_validation(self):
|
||||
|
||||
class CommentSerializerWithCrossFieldValidator(CommentSerializer):
|
||||
|
@ -224,6 +292,92 @@ class ValidationTests(TestCase):
|
|||
self.assertEquals(serializer.is_valid(), True)
|
||||
self.assertEquals(serializer.errors, {})
|
||||
|
||||
def test_modelserializer_max_length_exceeded(self):
|
||||
data = {
|
||||
'title': 'x' * 201,
|
||||
}
|
||||
serializer = ActionItemSerializer(data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
|
||||
|
||||
def test_default_modelfield_max_length_exceeded(self):
|
||||
data = {
|
||||
'title': 'Testing "info" field...',
|
||||
'info': 'x' * 13,
|
||||
}
|
||||
serializer = ActionItemSerializer(data=data)
|
||||
self.assertEquals(serializer.is_valid(), False)
|
||||
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
|
||||
|
||||
|
||||
class PositiveIntegerAsChoiceTests(TestCase):
|
||||
def test_positive_integer_in_json_is_correctly_parsed(self):
|
||||
data = {'some_integer':1}
|
||||
serializer = PositiveIntegerAsChoiceSerializer(data=data)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
|
||||
class ModelValidationTests(TestCase):
|
||||
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'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']})
|
||||
|
||||
serializer = BookSerializer(data={'isbn': '12345678901234'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']})
|
||||
|
||||
serializer = BookSerializer(data={'isbn': 'abcdefghijklm'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']})
|
||||
|
||||
def test_create_success(self):
|
||||
serializer = BookSerializer(data={'isbn': '1234567890123'})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
|
||||
class MetadataTests(TestCase):
|
||||
def test_empty(self):
|
||||
|
@ -416,6 +570,21 @@ class DefaultValueTests(TestCase):
|
|||
self.assertEquals(instance.pk, 1)
|
||||
self.assertEquals(instance.text, 'overridden')
|
||||
|
||||
def test_partial_update_default(self):
|
||||
""" Regression test for issue #532 """
|
||||
data = {'text': 'overridden'}
|
||||
serializer = self.serializer_class(data=data, partial=True)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
|
||||
data = {'extra': 'extra_value'}
|
||||
serializer = self.serializer_class(instance=instance, data=data, partial=True)
|
||||
self.assertEquals(serializer.is_valid(), True)
|
||||
instance = serializer.save()
|
||||
|
||||
self.assertEquals(instance.extra, 'extra_value')
|
||||
self.assertEquals(instance.text, 'overridden')
|
||||
|
||||
|
||||
class CallableDefaultValueTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -446,7 +615,10 @@ class CallableDefaultValueTests(TestCase):
|
|||
|
||||
|
||||
class ManyRelatedTests(TestCase):
|
||||
def setUp(self):
|
||||
def test_reverse_relations(self):
|
||||
post = BlogPost.objects.create(title="Test blog post")
|
||||
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
class BlogPostCommentSerializer(serializers.Serializer):
|
||||
text = serializers.CharField()
|
||||
|
@ -455,14 +627,7 @@ class ManyRelatedTests(TestCase):
|
|||
title = serializers.CharField()
|
||||
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
||||
|
||||
self.serializer_class = BlogPostSerializer
|
||||
|
||||
def test_reverse_relations(self):
|
||||
post = BlogPost.objects.create(title="Test blog post")
|
||||
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
serializer = self.serializer_class(instance=post)
|
||||
serializer = BlogPostSerializer(instance=post)
|
||||
expected = {
|
||||
'title': 'Test blog post',
|
||||
'comments': [
|
||||
|
@ -473,6 +638,100 @@ class ManyRelatedTests(TestCase):
|
|||
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_callable_source(self):
|
||||
post = BlogPost.objects.create(title="Test blog post")
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
class BlogPostCommentSerializer(serializers.Serializer):
|
||||
text = serializers.CharField()
|
||||
|
||||
class BlogPostSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
first_comment = BlogPostCommentSerializer(source='get_first_comment')
|
||||
|
||||
serializer = BlogPostSerializer(post)
|
||||
|
||||
expected = {
|
||||
'title': 'Test blog post',
|
||||
'first_comment': {'text': 'I love this blog post'}
|
||||
}
|
||||
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):
|
||||
|
||||
class BoopSerializer(serializers.Serializer):
|
||||
beep = serializers.SerializerMethodField('get_beep')
|
||||
boop = serializers.Field()
|
||||
boop_count = serializers.SerializerMethodField('get_boop_count')
|
||||
|
||||
def get_beep(self, obj):
|
||||
return 'hello!'
|
||||
|
||||
def get_boop_count(self, obj):
|
||||
return len(obj.boop)
|
||||
|
||||
self.serializer_class = BoopSerializer
|
||||
|
||||
def test_serializer_method_field(self):
|
||||
|
||||
class MyModel(object):
|
||||
boop = ['a', 'b', 'c']
|
||||
|
||||
source_data = MyModel()
|
||||
|
||||
serializer = self.serializer_class(source_data)
|
||||
|
||||
expected = {
|
||||
'beep': u'hello!',
|
||||
'boop': [u'a', u'b', u'c'],
|
||||
'boop_count': 3,
|
||||
}
|
||||
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
# Test for issue #324
|
||||
class BlankFieldTests(TestCase):
|
||||
|
@ -506,6 +765,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
|
||||
|
@ -521,3 +784,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
|
||||
|
|
21
rest_framework/tests/settings.py
Normal file
21
rest_framework/tests/settings.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
"""Tests for the settings module"""
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.settings import APISettings, DEFAULTS, IMPORT_STRINGS
|
||||
|
||||
|
||||
class TestSettings(TestCase):
|
||||
"""Tests relating to the api settings"""
|
||||
|
||||
def test_non_import_errors(self):
|
||||
"""Make sure other errors aren't suppressed."""
|
||||
settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
|
||||
with self.assertRaises(ValueError):
|
||||
settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
|
||||
def test_import_error_message_maintained(self):
|
||||
"""Make sure real import errors are captured and raised sensibly."""
|
||||
settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||
self.assertTrue('ImportError' in str(cm.exception))
|
|
@ -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',)
|
||||
|
|
|
@ -106,7 +106,7 @@ class ThrottlingTests(TestCase):
|
|||
if expect is not None:
|
||||
self.assertEquals(response['X-Throttle-Wait-Seconds'], expect)
|
||||
else:
|
||||
self.assertFalse('X-Throttle-Wait-Seconds' in response.headers)
|
||||
self.assertFalse('X-Throttle-Wait-Seconds' in response)
|
||||
|
||||
def test_seconds_fields(self):
|
||||
"""
|
||||
|
|
27
rest_framework/tests/utils.py
Normal file
27
rest_framework/tests/utils.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.test.client import RequestFactory, FakePayload
|
||||
from django.test.client import MULTIPART_CONTENT
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
class RequestFactory(RequestFactory):
|
||||
|
||||
def __init__(self, **defaults):
|
||||
super(RequestFactory, self).__init__(**defaults)
|
||||
|
||||
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
|
||||
**extra):
|
||||
"Construct a PATCH request."
|
||||
|
||||
patch_data = self._encode_data(data, content_type)
|
||||
|
||||
parsed = urlparse(path)
|
||||
r = {
|
||||
'CONTENT_LENGTH': len(patch_data),
|
||||
'CONTENT_TYPE': content_type,
|
||||
'PATH_INFO': self._get_path(parsed),
|
||||
'QUERY_STRING': parsed[4],
|
||||
'REQUEST_METHOD': 'PATCH',
|
||||
'wsgi.input': FakePayload(patch_data),
|
||||
}
|
||||
r.update(extra)
|
||||
return self.request(**r)
|
|
@ -18,7 +18,7 @@ class BasicView(APIView):
|
|||
return Response({'method': 'POST', 'data': request.DATA})
|
||||
|
||||
|
||||
@api_view(['GET', 'POST', 'PUT'])
|
||||
@api_view(['GET', 'POST', 'PUT', 'PATCH'])
|
||||
def basic_view(request):
|
||||
if request.method == 'GET':
|
||||
return {'method': 'GET'}
|
||||
|
@ -26,6 +26,8 @@ def basic_view(request):
|
|||
return {'method': 'POST', 'data': request.DATA}
|
||||
elif request.method == 'PUT':
|
||||
return {'method': 'PUT', 'data': request.DATA}
|
||||
elif request.method == 'PATCH':
|
||||
return {'method': 'PATCH', 'data': request.DATA}
|
||||
|
||||
|
||||
def sanitise_json_error(error_dict):
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from django.conf.urls.defaults import url
|
||||
from rest_framework.compat import url
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||
"""
|
||||
Supplement existing urlpatterns with corrosponding patterns that also
|
||||
Supplement existing urlpatterns with corresponding patterns that also
|
||||
include a '.format' suffix. Retains urlpattern ordering.
|
||||
|
||||
urlpatterns:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Login and logout views for the browseable API.
|
||||
Login and logout views for the browsable API.
|
||||
|
||||
Add these to your root URLconf if you're using the browseable API and
|
||||
Add these to your root URLconf if you're using the browsable API and
|
||||
your API requires authentication.
|
||||
|
||||
The urls must be namespaced as 'rest_framework', and you should make sure
|
||||
|
@ -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'}
|
||||
|
|
|
@ -4,7 +4,7 @@ Helper classes for parsers.
|
|||
import datetime
|
||||
import decimal
|
||||
import types
|
||||
from django.utils import simplejson as json
|
||||
import json
|
||||
from django.utils.datastructures import SortedDict
|
||||
from rest_framework.compat import timezone
|
||||
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||
|
|
|
@ -140,7 +140,7 @@ class APIView(View):
|
|||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
Called if `request.method` does not corrospond to a handler method.
|
||||
Called if `request.method` does not correspond to a handler method.
|
||||
"""
|
||||
raise exceptions.MethodNotAllowed(request.method)
|
||||
|
||||
|
|
20
tox.ini
20
tox.ini
|
@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
|||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = https://github.com/django/django/zipball/master
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
django-filter==0.5.4
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django==1.4.1
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
deps = django==1.4.3
|
||||
django-filter==0.5.4
|
||||
|
||||
[testenv:py2.7-django1.3]
|
||||
basepython = python2.7
|
||||
deps = django==1.3.3
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = https://github.com/django/django/zipball/master
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
django-filter==0.5.4
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django==1.4.1
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
deps = django==1.4.3
|
||||
django-filter==0.5.4
|
||||
|
||||
[testenv:py2.6-django1.3]
|
||||
basepython = python2.6
|
||||
deps = django==1.3.3
|
||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
|
|
Loading…
Reference in New Issue
Block a user