aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md18
-rw-r--r--docs/topics/credits.md6
-rw-r--r--docs/topics/release-notes.md14
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md4
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/fields.py2
-rw-r--r--rest_framework/mixins.py8
-rw-r--r--rest_framework/renderers.py2
-rw-r--r--rest_framework/serializers.py59
-rw-r--r--rest_framework/tests/generics.py36
-rw-r--r--rest_framework/tests/hyperlink_relations.py358
-rw-r--r--rest_framework/tests/models.py2
-rw-r--r--rest_framework/tests/renderers.py12
13 files changed, 475 insertions, 48 deletions
diff --git a/README.md b/README.md
index 1bc9628f..b4078b15 100644
--- a/README.md
+++ b/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]
@@ -58,6 +60,19 @@ To run the tests.
# Changelog
+## 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
@@ -198,6 +213,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[twitter]: https://twitter.com/_tomchristie
+[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index ba37ce11..c169fd74 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -98,10 +98,9 @@ Development of REST framework 2.0 was sponsored by [DabApps].
## Contact
-To contact the author directly:
+For usage questions please see the [REST framework discussion group][group].
-* twitter: [@_tomchristie][twitter]
-* email: [tom@tomchristie.com][email]
+You can also contact [@_tomchristie][twitter] directly on twitter.
[email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie
@@ -115,6 +114,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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 4ea393af..c75f0879 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -8,6 +8,20 @@
### Master
+* 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
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index b5d37875..216ca433 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -163,9 +163,9 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
-* 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.**
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 83a6f302..d5cac5c6 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.9'
+__version__ = '2.1.11'
VERSION = __version__ # synonym
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 903c384e..f582a681 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -32,6 +32,7 @@ def is_simple_callable(obj):
class Field(object):
+ read_only = True
creation_counter = 0
empty = ''
type_name = None
@@ -383,6 +384,7 @@ class ManyRelatedMixin(object):
else:
if value == ['']:
value = []
+
into[field_name] = [self.from_native(item) for item in value]
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 1edcfa5c..2700606d 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -113,6 +113,10 @@ class UpdateModelMixin(object):
slug_field = self.get_slug_field()
setattr(obj, slug_field, slug)
+ # Ensure we clean the attributes so that we don't eg return integer
+ # pk using a string representation, as provided by the url conf kwarg.
+ obj.full_clean()
+
class DestroyModelMixin(object):
"""
@@ -120,6 +124,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)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 1220bca1..a4ae717d 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -20,7 +20,7 @@ from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import VERSION, status
-from rest_framework import serializers, parsers
+from rest_framework import parsers
class BaseRenderer(object):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8026205e..8156bc18 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -128,17 +128,6 @@ class BaseSerializer(Field):
"""
return {}
- def get_excluded_fieldnames(self):
- """
- Returns the fieldnames that should not be validated.
- """
- excluded_fields = list(self.opts.exclude)
- if self.opts.fields:
- for field in self.fields.keys() + self.get_default_fields().keys():
- if field not in list(self.opts.fields) + excluded_fields:
- excluded_fields.append(field)
- return excluded_fields
-
def get_fields(self):
"""
Returns the complete set of fields for the object as a dict.
@@ -171,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
#####
@@ -185,13 +177,6 @@ class BaseSerializer(Field):
if parent.opts.depth:
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.
@@ -491,6 +476,18 @@ class ModelSerializer(Serializer):
except KeyError:
return ModelField(model_field=model_field, **kwargs)
+ def get_validation_exclusions(self):
+ """
+ Return a list of field names to exclude from model validation.
+ """
+ cls = self.opts.model
+ opts = get_concrete_model(cls)._meta
+ exclusions = [field.name for field in opts.fields + opts.many_to_many]
+ for field_name, field in self.fields.items():
+ if field_name in exclusions and not field.read_only:
+ exclusions.remove(field_name)
+ return exclusions
+
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
@@ -500,25 +497,27 @@ class ModelSerializer(Serializer):
if instance is not None:
for key, val in attrs.items():
setattr(instance, key, val)
- return instance
- # Reverse 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)
+ else:
+ # Reverse 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
+ for field in self.opts.model._meta.many_to_many:
+ if field.name in attrs:
+ self.m2m_data[field.name] = attrs.pop(field.name)
- # Forward 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)
+ instance = self.opts.model(**attrs)
- instance = self.opts.model(**attrs)
try:
- instance.full_clean(exclude=list(self.get_excluded_fieldnames()))
+ instance.full_clean(exclude=self.get_validation_exclusions())
except ValidationError, err:
self._errors = err.message_dict
return None
+
return instance
def save(self, save_m2m=True):
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index a8279ef2..7c24d84e 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -1,3 +1,4 @@
+from django.db import models
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json
@@ -174,7 +175,7 @@ class TestInstanceView(TestCase):
content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
content_type='application/json')
- response = self.view(request, pk=1).render()
+ response = self.view(request, pk='1').render()
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
@@ -301,3 +302,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)
diff --git a/rest_framework/tests/hyperlink_relations.py b/rest_framework/tests/hyperlink_relations.py
new file mode 100644
index 00000000..8f023873
--- /dev/null
+++ b/rest_framework/tests/hyperlink_relations.py
@@ -0,0 +1,358 @@
+from django.conf.urls import patterns, url
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+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', read_only=True)
+
+ 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.hyperlink_relations'
+
+ 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.hyperlink_relations'
+
+ 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_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.hyperlink_relations'
+
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_create_with_valid_null(self):
+ data = {'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': '/foreignkeytarget/1/'},
+ {'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': '/foreignkeytarget/1/'},
+ {'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': '/foreignkeytarget/1/'},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'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': '/foreignkeytarget/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)
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 807bcf98..1b1def30 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -65,7 +65,7 @@ class BasicModel(RESTFrameworkModel):
class SlugBasedModel(RESTFrameworkModel):
text = models.CharField(max_length=100)
- slug = models.SlugField(max_length=32, blank=True)
+ slug = models.SlugField(max_length=32)
class DefaultValueModel(RESTFrameworkModel):
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 9be4b114..fb843676 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -444,19 +444,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):