Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into patch-support

This commit is contained in:
Andrew Hankinson 2012-12-30 14:03:08 -04:00
commit c6f212238c
41 changed files with 1049 additions and 115 deletions

5
.gitignore vendored
View File

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

View File

@ -2,7 +2,9 @@
**A toolkit for building well-connected, self-describing web APIs.** **A toolkit for building well-connected, self-describing web APIs.**
**Author:** Tom Christie. [Follow me on Twitter][twitter] **Author:** Tom Christie. [Follow me on Twitter][twitter].
**Support:** [REST framework discussion group][group].
[![build-status-image]][travis] [![build-status-image]][travis]
@ -37,14 +39,35 @@ There is also a sandbox API you can use for testing purposes, [available here][s
# Installation # Installation
Install using `pip`... Install using `pip`, including any optional packages you want...
pip install djangorestframework pip install djangorestframework
pip install markdown # Markdown support for the browseable API.
pip install pyyaml # YAML content-type support.
pip install django-filter # Filtering support
...or clone the project from github. ...or clone the project from github.
git clone git@github.com:tomchristie/django-rest-framework.git git clone git@github.com:tomchristie/django-rest-framework.git
cd django-rest-framework
pip install -r requirements.txt pip install -r requirements.txt
pip install -r optionals.txt
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
'rest_framework',
)
If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
urlpatterns = patterns('',
...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
)
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
# Development # Development
@ -58,6 +81,34 @@ To run the tests.
# Changelog # Changelog
### 2.1.13
**Date**: 28th Dec 2012
* Support configurable `STATICFILES_STORAGE` storage.
* Bugfix: Related fields now respect the required flag, and may be required=False.
### 2.1.12
**Date**: 21st Dec 2012
* Bugfix: Fix bug that could occur using ChoiceField.
* Bugfix: Fix exception in browseable API on DELETE.
* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg.
## 2.1.11
**Date**: 17th Dec 2012
* Bugfix: Fix issue with M2M fields in browseable API.
## 2.1.10
**Date**: 17th Dec 2012
* Bugfix: Ensure read-only fields don't have model validation applied.
* Bugfix: Fix hyperlinked fields in paginated results.
## 2.1.9 ## 2.1.9
**Date**: 11th Dec 2012 **Date**: 11th Dec 2012
@ -198,6 +249,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2 [build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[twitter]: https://twitter.com/_tomchristie [twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html [rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html

View File

@ -150,7 +150,7 @@ Provides a base view for acting on a single object, by combining REST framework'
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`. * `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+] * `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+] * `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`. * `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
--- ---

View File

@ -4,8 +4,7 @@
> Expanding the usefulness of the serializers is something that we would > Expanding the usefulness of the serializers is something that we would
like to address. However, it's not a trivial problem, and it like to address. However, it's not a trivial problem, and it
will take some serious design work. Any offers to help out in this will take some serious design work.
area would be gratefully accepted.
> >
> — Russell Keith-Magee, [Django users group][cite] > — Russell Keith-Magee, [Django users group][cite]
@ -110,7 +109,22 @@ Your `validate_<fieldname>` methods should either just return the `attrs` dictio
### Object-level validation ### Object-level validation
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example:
from rest_framework import serializers
class EventSerializer(serializers.Serializer):
description = serializers.CahrField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()
def validate(self, attrs):
"""
Check that the start is before the stop.
"""
if attrs['start'] < attrs['finish']:
raise serializers.ValidationError("finish must occur after start")
return attrs
## Saving object state ## Saving object state

View File

@ -15,7 +15,7 @@ Django REST framework is a lightweight library that makes it easy to build Web A
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
There is also a sandbox API you can use for testing purposes, [available here][sandbox]. There is also a sandbox API you can use for testing purposes, [available here][sandbox].
@ -52,21 +52,21 @@ Install using `pip`, including any optional packages you want...
pip install -r requirements.txt pip install -r requirements.txt
pip install -r optionals.txt pip install -r optionals.txt
Add `rest_framework` to your `INSTALLED_APPS`. Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = ( INSTALLED_APPS = (
... ...
'rest_framework', 'rest_framework',
) )
If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file. If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
urlpatterns = patterns('', urlpatterns = patterns('',
... ...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
) )
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace. Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
## Quickstart ## Quickstart

View File

@ -79,6 +79,11 @@ The following people have helped make REST framework great.
* Colin Murtaugh - [cmurtaugh] * Colin Murtaugh - [cmurtaugh]
* Simon Pantzare - [pilt] * Simon Pantzare - [pilt]
* Szymon Teżewski - [sunscrapers] * Szymon Teżewski - [sunscrapers]
* Joel Marcotte - [joual]
* Trey Hunner - [treyhunner]
* Roman Akinfold - [akinfold]
* Toran Billups - [toranb]
* Sébastien Béal - [sebastibe]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
@ -98,10 +103,9 @@ Development of REST framework 2.0 was sponsored by [DabApps].
## Contact ## Contact
To contact the author directly: For usage questions please see the [REST framework discussion group][group].
* twitter: [@_tomchristie][twitter] You can also contact [@_tomchristie][twitter] directly on twitter.
* email: [tom@tomchristie.com][email]
[email]: mailto:tom@tomchristie.com [email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie [twitter]: http://twitter.com/_tomchristie
@ -115,6 +119,7 @@ To contact the author directly:
[dabapps]: http://lab.dabapps.com [dabapps]: http://lab.dabapps.com
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
[heroku]: http://www.heroku.com/ [heroku]: http://www.heroku.com/
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[tomchristie]: https://github.com/tomchristie [tomchristie]: https://github.com/tomchristie
[markotibold]: https://github.com/markotibold [markotibold]: https://github.com/markotibold
@ -193,3 +198,8 @@ To contact the author directly:
[cmurtaugh]: https://github.com/cmurtaugh [cmurtaugh]: https://github.com/cmurtaugh
[pilt]: https://github.com/pilt [pilt]: https://github.com/pilt
[sunscrapers]: https://github.com/sunscrapers [sunscrapers]: https://github.com/sunscrapers
[joual]: https://github.com/joual
[treyhunner]: https://github.com/treyhunner
[akinfold]: https://github.com/akinfold
[toranb]: https://github.com/toranb
[sebastibe]: https://github.com/sebastibe

View File

@ -4,10 +4,49 @@
> >
> &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite]. > &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite].
## Versioning
Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
Medium version numbers (0.x.0) may include minor API changes. You should read the release notes carefully before upgrading between medium point releases.
Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned.
---
## 2.1.x series ## 2.1.x series
### Master ### Master
* Bugfix: ModelSerializers now include reverse FK fields on creation.
* Bugfix: Model fields with `blank=True` are now `required=False` by default.
* Bugfix: Nested serializers now support nullable relationships.
### 2.1.13
**Date**: 28th Dec 2012
* Support configurable `STATICFILES_STORAGE` storage.
* Bugfix: Related fields now respect the required flag, and may be required=False.
### 2.1.12
**Date**: 21st Dec 2012
* Bugfix: Fix bug that could occur using ChoiceField.
* Bugfix: Fix exception in browseable API on DELETE.
* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg.
### 2.1.11
**Date**: 17th Dec 2012
* Bugfix: Fix issue with M2M fields in browseable API.
### 2.1.10
**Date**: 17th Dec 2012
* Bugfix: Ensure read-only fields don't have model validation applied. * Bugfix: Ensure read-only fields don't have model validation applied.
* Bugfix: Fix hyperlinked fields in paginated results. * Bugfix: Fix hyperlinked fields in paginated results.
@ -85,7 +124,7 @@
* Support use of HTML exception templates. Eg. `403.html` * Support use of HTML exception templates. Eg. `403.html`
* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments. * Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs. * Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs.
* Bugfix: Make textareas same width as other fields in browsable API. * Bugfix: Make textareas same width as other fields in browsable API.
* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. * Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
@ -93,8 +132,6 @@
**Date**: 5th Nov 2012 **Date**: 5th Nov 2012
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
* **Serializer `instance` and `data` keyword args have their position swapped.** * **Serializer `instance` and `data` keyword args have their position swapped.**
* `queryset` argument is now optional on writable model fields. * `queryset` argument is now optional on writable model fields.
* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments. * Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
@ -103,6 +140,8 @@
* Bugfix: Support choice field in Browseable API. * Bugfix: Support choice field in Browseable API.
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument. * Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
--- ---
## 2.0.x series ## 2.0.x series
@ -139,9 +178,9 @@
* Allow views to specify template used by TemplateRenderer * Allow views to specify template used by TemplateRenderer
* More consistent error responses * More consistent error responses
* Some serializer fixes * Some serializer fixes
* Fix internet explorer ajax behaviour * Fix internet explorer ajax behavior
* Minor xml and yaml fixes * Minor xml and yaml fixes
* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) * Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
* Sensible absolute URL generation, not using hacky set_script_prefix * Sensible absolute URL generation, not using hacky set_script_prefix
--- ---
@ -152,13 +191,13 @@
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files. * Use `staticfiles` for css files.
- Easier to override. Won't conflict with customised admin styles (eg grappelli) - Easier to override. Won't conflict with customized admin styles (e.g. grappelli)
* Templates are now nicely namespaced. * Templates are now nicely namespaced.
- Allows easier overriding. - Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed. * Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit. - Too magical. Explicit is better than implicit.
* Saner template variable autoescaping. * Saner template variable auto-escaping.
* Tider setup.py * Tidier setup.py
* Updated for URLObject 2.0 * Updated for URLObject 2.0
* Bugfixes: * Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars. - Bug with PerUserThrottling when user contains unicode chars.
@ -246,5 +285,7 @@
* Initial release. * Initial release.
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[announcement]: rest-framework-2-announcement.md [announcement]: rest-framework-2-announcement.md

View File

@ -163,9 +163,9 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
* Contribute on [GitHub][github] by reviewing and subitting issues, and making pull requests. * Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community. * Join the [REST framework discussion group][group], and help build the community.
* Follow the author [on Twitter][twitter] and say hi. * [Follow the author on Twitter][twitter] and say hi.
**Now go build awesome things.** **Now go build awesome things.**

View File

@ -1,3 +1,3 @@
__version__ = '2.1.9' __version__ = '2.1.13'
VERSION = __version__ # synonym VERSION = __version__ # synonym

View File

@ -5,6 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa # flake8: noqa
import django import django
# location of patterns, url, include changes in 1.4 onwards
try:
from django.conf.urls import patterns, url, include
except:
from django.conf.urls.defaults import patterns, url, include
# django-filter is optional # django-filter is optional
try: try:
import django_filters import django_filters

View File

@ -351,7 +351,12 @@ class RelatedField(WritableField):
if self.read_only: if self.read_only:
return return
value = data.get(field_name) try:
value = data[field_name]
except KeyError:
if self.required:
raise ValidationError(self.error_messages['required'])
return
if value in (None, '') and not self.null: if value in (None, '') and not self.null:
raise ValidationError('Value may not be null') raise ValidationError('Value may not be null')
@ -384,6 +389,7 @@ class ManyRelatedMixin(object):
else: else:
if value == ['']: if value == ['']:
value = [] value = []
into[field_name] = [self.from_native(item) for item in value] into[field_name] = [self.from_native(item) for item in value]
@ -795,7 +801,7 @@ class ChoiceField(WritableField):
if value == smart_unicode(k2): if value == smart_unicode(k2):
return True return True
else: else:
if value == smart_unicode(k): if value == smart_unicode(k) or value == k:
return True return True
return False return False

View File

@ -113,6 +113,10 @@ class UpdateModelMixin(object):
slug_field = self.get_slug_field() slug_field = self.get_slug_field()
setattr(obj, slug_field, slug) setattr(obj, slug_field, slug)
# Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg.
obj.full_clean()
class DestroyModelMixin(object): class DestroyModelMixin(object):
""" """
@ -120,6 +124,6 @@ class DestroyModelMixin(object):
Should be mixed in with `SingleObjectBaseView`. Should be mixed in with `SingleObjectBaseView`.
""" """
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
self.object = self.get_object() obj = self.get_object()
self.object.delete() obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -20,7 +20,7 @@ from rest_framework.utils import dict2xml
from rest_framework.utils import encoders from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import VERSION, status from rest_framework import VERSION, status
from rest_framework import serializers, parsers from rest_framework import parsers
class BaseRenderer(object): class BaseRenderer(object):

View File

@ -188,6 +188,14 @@ class Request(object):
self._user, self._auth = self._authenticate() self._user, self._auth = self._authenticate()
return self._auth return self._auth
@auth.setter
def auth(self, value):
"""
Sets any non-user authentication information associated with the
request, such as an authentication token.
"""
self._auth = value
def _load_data_and_files(self): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into self.DATA and self.FILES.

View File

@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os import os
import sys import sys
# fix sys path so we don't need to setup PYTHONPATH
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from coverage import coverage from coverage import coverage
@ -55,6 +58,12 @@ def main():
if 'compat.py' in files: if 'compat.py' in files:
files.remove('compat.py') files.remove('compat.py')
# Same applies to template tags module.
# This module has to include branching on Django versions,
# so it's never possible for it to have full coverage.
if 'rest_framework.py' in files:
files.remove('rest_framework.py')
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')]) cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files) cov.report(cov_files)

View File

@ -5,10 +5,8 @@
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os import os
import sys import sys
"""
Need to fix sys path so following works without specifically messing with PYTHONPATH # fix sys path so we don't need to setup PYTHONPATH
python ./rest_framework/runtests/runtests.py
"""
sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'

View File

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

View File

@ -160,6 +160,9 @@ class BaseSerializer(Field):
for key in self.opts.exclude: for key in self.opts.exclude:
ret.pop(key, None) ret.pop(key, None)
for key, field in ret.items():
field.initialize(parent=self, field_name=key)
return ret return ret
##### #####
@ -174,13 +177,6 @@ class BaseSerializer(Field):
if parent.opts.depth: if parent.opts.depth:
self.opts.depth = parent.opts.depth - 1 self.opts.depth = parent.opts.depth - 1
# We need to call initialize here to ensure any nested
# serializers that will have already called initialize on their
# descendants get updated with *their* parent.
# We could be a bit more smart about this, but it'll do for now.
for key, field in self.fields.items():
field.initialize(parent=self, field_name=key)
##### #####
# Methods to convert or revert from objects <--> primitive representations. # Methods to convert or revert from objects <--> primitive representations.
@ -311,6 +307,9 @@ class BaseSerializer(Field):
if is_simple_callable(getattr(obj, 'all', None)): if is_simple_callable(getattr(obj, 'all', None)):
return [self.to_native(item) for item in obj.all()] return [self.to_native(item) for item in obj.all()]
if obj is None:
return None
return self.to_native(obj) return self.to_native(obj)
@property @property
@ -442,7 +441,7 @@ class ModelSerializer(Serializer):
kwargs['blank'] = model_field.blank kwargs['blank'] = model_field.blank
if model_field.null: if model_field.null or model_field.blank:
kwargs['required'] = False kwargs['required'] = False
if model_field.has_default(): if model_field.has_default():
@ -497,29 +496,38 @@ class ModelSerializer(Serializer):
Restore the model instance. Restore the model instance.
""" """
self.m2m_data = {} self.m2m_data = {}
self.related_data = {}
if instance is not None: if instance is not None:
for key, val in attrs.items(): for key, val in attrs.items():
setattr(instance, key, val) setattr(instance, key, val)
return instance
# Reverse relations else:
# Reverse fk relations
for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.related_data[field_name] = attrs.pop(field_name)
# Reverse m2m relations
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
field_name = obj.field.related_query_name() field_name = obj.field.related_query_name()
if field_name in attrs: if field_name in attrs:
self.m2m_data[field_name] = attrs.pop(field_name) self.m2m_data[field_name] = attrs.pop(field_name)
# Forward relations # Forward m2m relations
for field in self.opts.model._meta.many_to_many: for field in self.opts.model._meta.many_to_many:
if field.name in attrs: if field.name in attrs:
self.m2m_data[field.name] = attrs.pop(field.name) self.m2m_data[field.name] = attrs.pop(field.name)
instance = self.opts.model(**attrs) instance = self.opts.model(**attrs)
try: try:
instance.full_clean(exclude=self.get_validation_exclusions()) instance.full_clean(exclude=self.get_validation_exclusions())
except ValidationError, err: except ValidationError, err:
self._errors = err.message_dict self._errors = err.message_dict
return None return None
return instance return instance
def save(self, save_m2m=True): def save(self, save_m2m=True):
@ -533,6 +541,11 @@ class ModelSerializer(Serializer):
setattr(self.object, accessor_name, object_list) setattr(self.object, accessor_name, object_list)
self.m2m_data = {} self.m2m_data = {}
if getattr(self, 'related_data', None):
for accessor_name, object_list in self.related_data.items():
setattr(self.object, accessor_name, object_list)
self.related_data = {}
return self.object return self.object

View File

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

View File

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

View File

@ -11,6 +11,89 @@ import string
register = template.Library() register = template.Library()
# Note we don't use 'load staticfiles', because we need a 1.3 compatible
# version, so instead we include the `static` template tag ourselves.
# When 1.3 becomes unsupported by REST framework, we can instead start to
# use the {% load staticfiles %} tag, remove the following code,
# and add a dependancy that `django.contrib.staticfiles` must be installed.
# Note: We can't put this into the `compat` module because the compat import
# from rest_framework.compat import ...
# conflicts with this rest_framework template tag module.
try: # Django 1.5+
from django.contrib.staticfiles.templatetags import StaticFilesNode
@register.tag('static')
def do_static(parser, token):
return StaticFilesNode.handle_token(parser, token)
except:
try: # Django 1.4
from django.contrib.staticfiles.storage import staticfiles_storage
@register.simple_tag
def static(path):
"""
A template tag that returns the URL to a file
using staticfiles' storage backend
"""
return staticfiles_storage.url(path)
except: # Django 1.3
from urlparse import urljoin
from django import template
from django.templatetags.static import PrefixNode
class StaticNode(template.Node):
def __init__(self, varname=None, path=None):
if path is None:
raise template.TemplateSyntaxError(
"Static template nodes must be given a path to return.")
self.path = path
self.varname = varname
def url(self, context):
path = self.path.resolve(context)
return self.handle_simple(path)
def render(self, context):
url = self.url(context)
if self.varname is None:
return url
context[self.varname] = url
return ''
@classmethod
def handle_simple(cls, path):
return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
@classmethod
def handle_token(cls, parser, token):
"""
Class method to parse prefix node and return a Node.
"""
bits = token.split_contents()
if len(bits) < 2:
raise template.TemplateSyntaxError(
"'%s' takes at least one argument (path to file)" % bits[0])
path = parser.compile_filter(bits[1])
if len(bits) >= 2 and bits[-2] == 'as':
varname = bits[3]
else:
varname = None
return cls(varname, path)
@register.tag('static')
def do_static_13(parser, token):
return StaticNode.handle_token(parser, token)
def replace_query_param(url, key, val): def replace_query_param(url, key, val):
""" """
Given a URL and a key/val pair, set or replace an item in the query Given a URL and a key/val pair, set or replace an item in the query

View File

@ -1,15 +1,13 @@
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.utils import simplejson as json
from django.http import HttpResponse from django.http import HttpResponse
from django.test import Client, TestCase
from django.utils import simplejson as json
from rest_framework.views import APIView
from rest_framework import permissions from rest_framework import permissions
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework.compat import patterns
from rest_framework.views import APIView
import base64 import base64

View File

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

View File

@ -1,4 +1,5 @@
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer

View File

@ -1,3 +1,4 @@
from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import simplejson as json from django.utils import simplejson as json
from rest_framework import generics, serializers, status from rest_framework import generics, serializers, status
@ -174,7 +175,7 @@ class TestInstanceView(TestCase):
content = {'text': 'foobar'} content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content), request = factory.put('/1', json.dumps(content),
content_type='application/json') content_type='application/json')
response = self.view(request, pk=1).render() response = self.view(request, pk='1').render()
self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1) updated = self.objects.get(id=1)
@ -315,3 +316,36 @@ class TestCreateModelWithAutoNowAddField(TestCase):
self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.status_code, status.HTTP_201_CREATED)
created = self.objects.get(id=1) created = self.objects.get(id=1)
self.assertEquals(created.content, 'foobar') self.assertEquals(created.content, 'foobar')
# Test for particularly ugly reression with m2m in browseable API
class ClassB(models.Model):
name = models.CharField(max_length=255)
class ClassA(models.Model):
name = models.CharField(max_length=255)
childs = models.ManyToManyField(ClassB, blank=True, null=True)
class ClassASerializer(serializers.ModelSerializer):
childs = serializers.ManyPrimaryKeyRelatedField(source='childs')
class Meta:
model = ClassA
class ExampleView(generics.ListCreateAPIView):
serializer_class = ClassASerializer
model = ClassA
class TestM2MBrowseableAPI(TestCase):
def test_m2m_in_browseable_api(self):
"""
Test for particularly ugly reression with m2m in browseable API
"""
request = factory.get('/', HTTP_ACCEPT='text/html')
view = ExampleView().as_view()
response = view(request).render()
self.assertEquals(response.status_code, status.HTTP_200_OK)

View File

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

View File

@ -1,8 +1,8 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import simplejson as json from django.utils import simplejson as json
from rest_framework import generics, status, serializers from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
factory = RequestFactory() factory = RequestFactory()

View File

@ -51,6 +51,11 @@ class RESTFrameworkModel(models.Model):
abstract = True abstract = True
class HasPositiveIntegerAsChoice(RESTFrameworkModel):
some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
some_integer = models.PositiveIntegerField(choices=some_choices)
class Anchor(RESTFrameworkModel): class Anchor(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor') text = models.CharField(max_length=100, default='anchor')
@ -160,7 +165,7 @@ class Photo(RESTFrameworkModel):
# Model for issue #324 # Model for issue #324
class BlankFieldModel(RESTFrameworkModel): class BlankFieldModel(RESTFrameworkModel):
title = models.CharField(max_length=100, blank=True, null=True) title = models.CharField(max_length=100, blank=True, null=False)
# Model for issue #380 # Model for issue #380

View File

@ -1,4 +1,4 @@
# from django.conf.urls.defaults import patterns, url # from rest_framework.compat import patterns, url
# from django.forms import ModelForm # from django.forms import ModelForm
# from django.contrib.auth.models import Group, User # from django.contrib.auth.models import Group, User
# from rest_framework.resources import ModelResource # from rest_framework.resources import ModelResource

View File

@ -0,0 +1,424 @@
from django.db import models
from django.test import TestCase
from rest_framework import serializers
from rest_framework.compat import patterns, url
def dummy_view(request, pk):
pass
urlpatterns = patterns('',
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
)
# ManyToMany
class ManyToManyTarget(models.Model):
name = models.CharField(max_length=100)
class ManyToManySource(models.Model):
name = models.CharField(max_length=100)
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail')
class Meta:
model = ManyToManyTarget
class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ManyToManySource
# ForeignKey
class ForeignKeyTarget(models.Model):
name = models.CharField(max_length=100)
class ForeignKeySource(models.Model):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail')
class Meta:
model = ForeignKeyTarget
class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ForeignKeySource
# Nullable ForeignKey
class NullableForeignKeySource(models.Model):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
related_name='nullable_sources')
class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = NullableForeignKeySource
# TODO: Add test that .data cannot be accessed prior to .is_valid
class HyperlinkedManyToManyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
def setUp(self):
for idx in range(1, 4):
target = ManyToManyTarget(name='target-%d' % idx)
target.save()
source = ManyToManySource(name='source-%d' % idx)
source.save()
for target in ManyToManyTarget.objects.all():
source.targets.add(target)
def test_many_to_many_retrieve(self):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset)
expected = [
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset)
expected = [
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
]
self.assertEquals(serializer.data, expected)
def test_many_to_many_update(self):
data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
instance = ManyToManySource.objects.get(pk=1)
serializer = ManyToManySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset)
expected = [
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_update(self):
data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}
instance = ManyToManyTarget.objects.get(pk=1)
serializer = ManyToManyTargetSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure target 1 is updated, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset)
expected = [
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']},
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
]
self.assertEquals(serializer.data, expected)
def test_many_to_many_create(self):
data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']}
serializer = ManyToManySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset)
expected = [
{'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
{'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
{'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
{'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']}
]
self.assertEquals(serializer.data, expected)
def test_reverse_many_to_many_create(self):
data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
serializer = ManyToManyTargetSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'target-4')
# Ensure target 4 is added, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset)
expected = [
{'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
{'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
{'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']},
{'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
]
self.assertEquals(serializer.data, expected)
class HyperlinkedForeignKeyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
new_target = ForeignKeyTarget(name='target-2')
new_target.save()
for idx in range(1, 4):
source = ForeignKeySource(name='source-%d' % idx, target=target)
source.save()
def test_foreign_key_retrieve(self):
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
]
self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update(self):
data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'},
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
]
self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_update(self):
data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
instance = ForeignKeyTarget.objects.get(pk=2)
serializer = ForeignKeyTargetSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create(self):
data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'}
serializer = ForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'},
{'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'},
]
self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_create(self):
data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
serializer = ForeignKeyTargetSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'target-3')
# Ensure target 4 is added, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
{'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
{'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_invalid_null(self):
data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
class HyperlinkedNullableForeignKeyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
for idx in range(1, 4):
if idx == 3:
target = None
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
source.save()
def test_foreign_key_retrieve_with_null(self):
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create_with_valid_null(self):
data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
{'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create_with_valid_emptystring(self):
"""
The emptystring should be interpreted as null in the context
of relationships.
"""
data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''}
expected_data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, expected_data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
{'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_valid_null(self):
data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_valid_emptystring(self):
"""
The emptystring should be interpreted as null in the context
of relationships.
"""
data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''}
expected_data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, expected_data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
{'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
{'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)
# reverse foreign keys MUST be read_only
# In the general case they do not provide .remove() or .clear()
# and cannot be arbitrarily set.
# def test_reverse_foreign_key_update(self):
# data = {'id': 1, 'name': u'target-1', 'sources': [1]}
# instance = ForeignKeyTarget.objects.get(pk=1)
# serializer = ForeignKeyTargetSerializer(instance, data=data)
# self.assertTrue(serializer.is_valid())
# self.assertEquals(serializer.data, data)
# serializer.save()
# # Ensure target 1 is updated, and everything else is as expected
# queryset = ForeignKeyTarget.objects.all()
# serializer = ForeignKeyTargetSerializer(queryset)
# expected = [
# {'id': 1, 'name': u'target-1', 'sources': [1]},
# {'id': 2, 'name': u'target-2', 'sources': []},
# ]
# self.assertEquals(serializer.data, expected)

View File

@ -0,0 +1,102 @@
from django.db import models
from django.test import TestCase
from rest_framework import serializers
# ForeignKey
class ForeignKeyTarget(models.Model):
name = models.CharField(max_length=100)
class ForeignKeySource(models.Model):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
class ForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
depth = 1
model = ForeignKeySource
class FlatForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeySource
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
sources = FlatForeignKeySourceSerializer()
class Meta:
model = ForeignKeyTarget
# Nullable ForeignKey
class NullableForeignKeySource(models.Model):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
related_name='nullable_sources')
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
depth = 1
model = NullableForeignKeySource
class ReverseForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
new_target = ForeignKeyTarget(name='target-2')
new_target.save()
for idx in range(1, 4):
source = ForeignKeySource(name='source-%d' % idx, target=target)
source.save()
def test_foreign_key_retrieve(self):
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}},
{'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
{'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'target-1'}},
]
self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1},
]},
{'id': 2, 'name': u'target-2', 'sources': [
]}
]
self.assertEquals(serializer.data, expected)
class NestedNullableForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
for idx in range(1, 4):
if idx == 3:
target = None
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
source.save()
def test_foreign_key_retrieve_with_null(self):
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}},
{'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
{'id': 3, 'name': u'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)

View File

@ -38,7 +38,7 @@ class ForeignKeySource(models.Model):
class ForeignKeyTargetSerializer(serializers.ModelSerializer): class ForeignKeyTargetSerializer(serializers.ModelSerializer):
sources = serializers.ManyPrimaryKeyRelatedField(read_only=True) sources = serializers.ManyPrimaryKeyRelatedField()
class Meta: class Meta:
model = ForeignKeyTarget model = ForeignKeyTarget
@ -216,6 +216,60 @@ class PKForeignKeyTests(TestCase):
] ]
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_update(self):
data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]}
instance = ForeignKeyTarget.objects.get(pk=2)
serializer = ForeignKeyTargetSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [2]},
{'id': 2, 'name': u'target-2', 'sources': [1, 3]},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create(self):
data = {'id': 4, 'name': u'source-4', 'target': 2}
serializer = ForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'source-4')
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1},
{'id': 4, 'name': u'source-4', 'target': 2},
]
self.assertEquals(serializer.data, expected)
def test_reverse_foreign_key_create(self):
data = {'id': 3, 'name': u'target-3', 'sources': [1, 3]}
serializer = ForeignKeyTargetSerializer(data=data)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEquals(serializer.data, data)
self.assertEqual(obj.name, u'target-3')
# Ensure target 4 is added, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
serializer = ForeignKeyTargetSerializer(queryset)
expected = [
{'id': 1, 'name': u'target-1', 'sources': [2]},
{'id': 2, 'name': u'target-2', 'sources': []},
{'id': 3, 'name': u'target-3', 'sources': [1, 3]},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_update_with_invalid_null(self): def test_foreign_key_update_with_invalid_null(self):
data = {'id': 1, 'name': u'source-1', 'target': None} data = {'id': 1, 'name': u'source-1', 'target': None}
instance = ForeignKeySource.objects.get(pk=1) instance = ForeignKeySource.objects.get(pk=1)
@ -229,9 +283,21 @@ class PKNullableForeignKeyTests(TestCase):
target = ForeignKeyTarget(name='target-1') target = ForeignKeyTarget(name='target-1')
target.save() target.save()
for idx in range(1, 4): for idx in range(1, 4):
if idx == 3:
target = None
source = NullableForeignKeySource(name='source-%d' % idx, target=target) source = NullableForeignKeySource(name='source-%d' % idx, target=target)
source.save() source.save()
def test_foreign_key_retrieve_with_null(self):
queryset = NullableForeignKeySource.objects.all()
serializer = NullableForeignKeySourceSerializer(queryset)
expected = [
{'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': None},
]
self.assertEquals(serializer.data, expected)
def test_foreign_key_create_with_valid_null(self): def test_foreign_key_create_with_valid_null(self):
data = {'id': 4, 'name': u'source-4', 'target': None} data = {'id': 4, 'name': u'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data) serializer = NullableForeignKeySourceSerializer(data=data)
@ -246,7 +312,7 @@ class PKNullableForeignKeyTests(TestCase):
expected = [ expected = [
{'id': 1, 'name': u'source-1', 'target': 1}, {'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1}, {'id': 3, 'name': u'source-3', 'target': None},
{'id': 4, 'name': u'source-4', 'target': None} {'id': 4, 'name': u'source-4', 'target': None}
] ]
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)
@ -270,7 +336,7 @@ class PKNullableForeignKeyTests(TestCase):
expected = [ expected = [
{'id': 1, 'name': u'source-1', 'target': 1}, {'id': 1, 'name': u'source-1', 'target': 1},
{'id': 2, 'name': u'source-2', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1}, {'id': 3, 'name': u'source-3', 'target': None},
{'id': 4, 'name': u'source-4', 'target': None} {'id': 4, 'name': u'source-4', 'target': None}
] ]
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)
@ -289,7 +355,7 @@ class PKNullableForeignKeyTests(TestCase):
expected = [ expected = [
{'id': 1, 'name': u'source-1', 'target': None}, {'id': 1, 'name': u'source-1', 'target': None},
{'id': 2, 'name': u'source-2', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1} {'id': 3, 'name': u'source-3', 'target': None}
] ]
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)
@ -312,7 +378,7 @@ class PKNullableForeignKeyTests(TestCase):
expected = [ expected = [
{'id': 1, 'name': u'source-1', 'target': None}, {'id': 1, 'name': u'source-1', 'target': None},
{'id': 2, 'name': u'source-2', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1},
{'id': 3, 'name': u'source-3', 'target': 1} {'id': 3, 'name': u'source-3', 'target': None}
] ]
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)

View File

@ -1,13 +1,12 @@
import pickle import pickle
import re import re
from django.conf.urls.defaults import patterns, url, include
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from rest_framework import status, permissions from rest_framework import status, permissions
from rest_framework.compat import yaml from rest_framework.compat import yaml, patterns, url, include
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \

View File

@ -1,16 +1,15 @@
""" """
Tests for content parsing, and form-overloaded content parsing. Tests for content parsing, and form-overloaded content parsing.
""" """
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.utils import simplejson as json from django.utils import simplejson as json
from rest_framework import status from rest_framework import status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from django.test.client import RequestFactory from rest_framework.compat import patterns
from rest_framework.parsers import ( from rest_framework.parsers import (
BaseParser, BaseParser,
FormParser, FormParser,
@ -304,3 +303,11 @@ class TestUserSetter(TestCase):
self.assertFalse(self.request.user.is_anonymous()) self.assertFalse(self.request.user.is_anonymous())
logout(self.request) logout(self.request)
self.assertTrue(self.request.user.is_anonymous()) self.assertTrue(self.request.user.is_anonymous())
class TestAuthSetter(TestCase):
def test_auth_can_be_set(self):
request = Request(factory.get('/'))
request.auth = 'DUMMY'
self.assertEqual(request.auth, 'DUMMY')

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import datetime
import pickle import pickle
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
@ -69,6 +69,11 @@ class AlbumsSerializer(serializers.ModelSerializer):
model = Album model = Album
fields = ['title'] # lists are also valid options fields = ['title'] # lists are also valid options
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = HasPositiveIntegerAsChoice
fields = ['some_integer']
class BasicTests(TestCase): class BasicTests(TestCase):
def setUp(self): def setUp(self):
@ -285,6 +290,12 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
class PositiveIntegerAsChoiceTests(TestCase):
def test_positive_integer_in_json_is_correctly_parsed(self):
data = {'some_integer':1}
serializer = PositiveIntegerAsChoiceSerializer(data=data)
self.assertEquals(serializer.is_valid(), True)
class ModelValidationTests(TestCase): class ModelValidationTests(TestCase):
def test_validate_unique(self): def test_validate_unique(self):
""" """
@ -297,6 +308,38 @@ class ModelValidationTests(TestCase):
self.assertFalse(second_serializer.is_valid()) self.assertFalse(second_serializer.is_valid())
self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']}) self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
def test_foreign_key_with_partial(self):
"""
Test ModelSerializer validation with partial=True
Specifically test foreign key validation.
"""
album = Album(title='test')
album.save()
class PhotoSerializer(serializers.ModelSerializer):
class Meta:
model = Photo
photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
self.assertTrue(photo_serializer.is_valid())
photo = photo_serializer.save()
# Updating only the album (foreign key)
photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
self.assertTrue(photo_serializer.is_valid())
self.assertTrue(photo_serializer.save())
# Updating only the description
photo_serializer = PhotoSerializer(instance=photo,
data={'description': 'new'},
partial=True)
self.assertTrue(photo_serializer.is_valid())
self.assertTrue(photo_serializer.save())
class RegexValidationTest(TestCase): class RegexValidationTest(TestCase):
def test_create_failed(self): def test_create_failed(self):
@ -688,6 +731,10 @@ class BlankFieldTests(TestCase):
serializer = self.model_serializer_class(data=self.data) serializer = self.model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
def test_create_model_null_field(self):
serializer = self.model_serializer_class(data={'title': None})
self.assertEquals(serializer.is_valid(), True)
def test_create_not_blank_field(self): def test_create_not_blank_field(self):
""" """
Test to ensure blank data in a field not marked as blank=True Test to ensure blank data in a field not marked as blank=True
@ -704,6 +751,10 @@ class BlankFieldTests(TestCase):
serializer = self.not_blank_model_serializer_class(data=self.data) serializer = self.not_blank_model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
def test_create_model_null_field(self):
serializer = self.model_serializer_class(data={})
self.assertEquals(serializer.is_valid(), True)
#test for issue #460 #test for issue #460
class SerializerPickleTests(TestCase): class SerializerPickleTests(TestCase):

View File

@ -6,6 +6,7 @@ from django.test import TestCase
NO_SETTING = ('!', None) NO_SETTING = ('!', None)
class TestSettingsManager(object): class TestSettingsManager(object):
""" """
A class which can modify some Django settings temporarily for a A class which can modify some Django settings temporarily for a
@ -57,6 +58,7 @@ class SettingsTestCase(TestCase):
def tearDown(self): def tearDown(self):
self.settings_manager.revert() self.settings_manager.revert()
class TestModelsTestCase(SettingsTestCase): class TestModelsTestCase(SettingsTestCase):
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',) installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)

View File

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

View File

@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`.
url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
) )
""" """
from django.conf.urls.defaults import patterns, url from rest_framework.compat import patterns, url
template_name = {'template_name': 'rest_framework/login.html'} template_name = {'template_name': 'rest_framework/login.html'}

View File

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