This commit is contained in:
Xavier Ordoquy 2014-04-30 20:12:13 +00:00
commit 613899e58e
2 changed files with 327 additions and 24 deletions

View File

@ -35,6 +35,11 @@ from rest_framework.settings import api_settings
from rest_framework.relations import *
from rest_framework.fields import *
# nested mode constants
BATCH_ADD = 1
BATCH_UPDATE = 2
BATCH_DELETE = 4
def _resolve_model(obj):
"""
@ -179,17 +184,35 @@ class BaseSerializer(WritableField):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata
batch_mode = BATCH_UPDATE
def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=None,
allow_add_remove=False, **kwargs):
batch_mode=None, **kwargs):
allow_add_remove = kwargs.pop('allow_add_remove', None)
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None
self.root = None
self.partial = partial
self.many = many
self.allow_add_remove = allow_add_remove
# Handle allow_add_remove depreaction
if hasattr(self, 'allow_add_remove'):
# If we already have allow_add_remove, it's a class argument.
# By reassigning it we'll trigger the batch_mode configuration
warnings.warn('The `allow_add_remove` keyword argument is deprecated. '
'Use the `batch_mode` keyword argument instead.',
DeprecationWarning, stacklevel=2)
self.allow_add_remove = self.allow_add_remove
if allow_add_remove:
warnings.warn('The `allow_add_remove` keyword argument is deprecated. '
'Use the `batch_mode` keyword argument instead.',
DeprecationWarning, stacklevel=2)
self.allow_add_remove = allow_add_remove
if batch_mode:
self.batch_mode = batch_mode
self.context = context or {}
@ -205,9 +228,23 @@ class BaseSerializer(WritableField):
if many and instance is not None and not hasattr(instance, '__iter__'):
raise ValueError('instance should be a queryset or other iterable with many=True')
if allow_add_remove and not many:
if 'allow_add_remove' in kwargs and not many:
raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
# Compatibility between allow_add_remove and batch_mode
def set_allow_add_remove(self, value):
self.batch_mode = BATCH_UPDATE
if value:
self.batch_mode = BATCH_ADD | BATCH_UPDATE | BATCH_DELETE
def get_allow_add_remove(self):
if self.batch_mode == BATCH_ADD | BATCH_UPDATE | BATCH_DELETE:
return True
if self.batch_mode == BATCH_UPDATE:
return False
allow_add_remove = property(get_allow_add_remove, set_allow_add_remove)
#####
# Methods to determine which fields to use when (de)serializing objects.
@ -464,7 +501,8 @@ class BaseSerializer(WritableField):
'context': self.context,
'partial': self.partial,
'many': self.many,
'allow_add_remove': self.allow_add_remove
'allow_add_remove': self.allow_add_remove,
'batch_mode': self.batch_mode
}
serializer = self.__class__(**kwargs)
@ -526,15 +564,19 @@ class BaseSerializer(WritableField):
# Determine which object we're updating
identity = self.get_identity(item)
self.object = identity_to_objects.pop(identity, None)
if self.object is None and not self.allow_add_remove:
if self.object is None and not (self.batch_mode & BATCH_ADD):
ret.append(None)
errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
continue
if self.object is not None and not (self.batch_mode & BATCH_UPDATE):
ret.append(None)
errors.append({'non_field_errors': ['Cannot update an item.']})
continue
ret.append(self.from_native(item, None))
errors.append(self._errors)
if update and self.allow_add_remove:
if update and (self.batch_mode & BATCH_DELETE):
ret._deleted = identity_to_objects.values()
self._errors = any(errors) and errors or []

View File

@ -6,9 +6,30 @@ Doesn't cover model serializers.
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import serializers
from rest_framework.serializers import BATCH_ADD, BATCH_UPDATE, BATCH_DELETE
from . import models
class BlogPostCommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.BlogPostComment
fields = ('id', 'text')
class BlogPostSerializer(serializers.ModelSerializer):
comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
class Meta:
model = models.BlogPost
fields = ('id', 'title', 'comments')
class PersonSerializer(serializers.ModelSerializer):
posts = BlogPostSerializer(many=True, source='blogpost_set')
class Meta:
model = models.Person
fields = ('id', 'name', 'age', 'posts')
class WritableNestedSerializerBasicTests(TestCase):
"""
Tests for deserializing nested entities.
@ -315,33 +336,273 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
class NestedModelSerializerUpdateTests(TestCase):
def test_second_nested_level(self):
def test_allows_second_nesting_level(self):
"""
Make sure we can span relations for nested representations
"""
john = models.Person.objects.create(name="john")
post = john.blogpost_set.create(title="Test blog post")
post.blogpostcomment_set.create(text="I hate this blog post")
post.blogpostcomment_set.create(text="I love this blog post")
class BlogPostCommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.BlogPostComment
class BlogPostSerializer(serializers.ModelSerializer):
comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
class Meta:
model = models.BlogPost
fields = ('id', 'title', 'comments')
class PersonSerializer(serializers.ModelSerializer):
posts = BlogPostSerializer(many=True, source='blogpost_set')
class Meta:
model = models.Person
fields = ('id', 'name', 'age', 'posts')
serialize = PersonSerializer(instance=john)
deserialize = PersonSerializer(data=serialize.data, instance=john)
self.assertTrue(deserialize.is_valid())
self.assertTrue(deserialize.is_valid(), deserialize.errors)
result = deserialize.object
result.save()
self.assertEqual(result.id, john.id)
self.assertEqual(
[i.id for i in result.blogpost_set.all()],
[i.id for i in john.blogpost_set.all()])
class NestedModelSerializerCreationsTests(TestCase):
def test_works_in_batch_add_mode(self):
"""
Create nested while being in BATCH_ADD mode works.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_ADD)
post = models.BlogPost(title='Test blog post')
post.save()
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'text': 'I hate this blog post',
}, {
'text': 'I love this blog post',
}],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertTrue(serializer.is_valid(), serializer.errors)
post = serializer.save()
self.assertTrue(post.id)
self.assertEqual(post.blogpostcomment_set.count(), 2)
def test_fails_in_batch_update_mode(self):
"""
Create nested while being in BATCH_UPDATE mode fails.
"""
post = models.BlogPost(title='Test blog post')
post.save()
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'text': 'I hate this blog post',
}, {
'text': 'I love this blog post',
}],
}
serializer = BlogPostSerializer(data=data, instance=post)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {
'comments': [{
'non_field_errors': ['Cannot create a new item, only existing items may be updated.']
}, {
'non_field_errors': ['Cannot create a new item, only existing items may be updated.']
}]
})
def test_fails_in_batch_delete_mode(self):
"""
Create nested while being in BATCH_DELETE mode fails.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_DELETE)
post = models.BlogPost(title='Test blog post')
post.save()
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'text': 'I hate this blog post',
}, {
'text': 'I love this blog post',
}],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {
'comments': [{
'non_field_errors': ['Cannot create a new item, only existing items may be updated.']
}, {
'non_field_errors': ['Cannot create a new item, only existing items may be updated.']
}]
})
class NestedModelSerializerUpdatesTests(TestCase):
def test_fails_in_batch_add_mode(self):
"""
Update nested while being in BATCH_ADD mode fails.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_ADD)
post = models.BlogPost(title='Test blog post')
post.save()
comment1 = post.blogpostcomment_set.create(text="I hate this blog post")
comment2 = post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'id': comment1.id,
'text': 'I hate this blog post :p',
}, {
'id': comment2.id,
'text': 'I love this blog post :p',
}],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {
'comments': [{
'non_field_errors': ['Cannot update an item.']
}, {
'non_field_errors': ['Cannot update an item.']
}]
})
def test_fails_in_batch_update_mode(self):
"""
Update nested while being in BATCH_UPDATE mode works.
"""
post = models.BlogPost(title='Test blog post')
post.save()
comment1 = post.blogpostcomment_set.create(text="I hate this blog post")
comment2 = post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'id': comment1.id,
'text': 'I hate this blog post :p',
}, {
'id': comment2.id,
'text': 'I love this blog post :p',
}],
}
serializer = BlogPostSerializer(data=data, instance=post)
self.assertTrue(serializer.is_valid(), serializer.errors)
post = serializer.save()
self.assertTrue(post.id)
self.assertEqual(post.blogpostcomment_set.count(), 2)
self.assertEqual(
set(i['text'] for i in data['comments']),
set(i.text for i in post.blogpostcomment_set.all().order_by('id'))
)
def test_fails_in_batch_delete_mode(self):
"""
Update nested while being in BATCH_DELETE mode fails.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_DELETE)
post = models.BlogPost(title='Test blog post')
post.save()
comment1 = post.blogpostcomment_set.create(text="I hate this blog post")
comment2 = post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [{
'id': comment1.id,
'text': 'I hate this blog post :p',
}, {
'id': comment2.id,
'text': 'I love this blog post :p',
}],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {
'comments': [{
'non_field_errors': ['Cannot update an item.']
}, {
'non_field_errors': ['Cannot update an item.']
}]
})
class NestedModelSerializerDeletesTests(TestCase):
def test_fails_in_batch_add_mode(self):
"""
Delete nested while being in BATCH_ADD mode doesn't delete.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_ADD)
post = models.BlogPost(title='Test blog post')
post.save()
post.blogpostcomment_set.create(text="I hate this blog post")
post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertTrue(serializer.is_valid())
post = serializer.save()
self.assertEqual(post.blogpostcomment_set.count(), 2)
def test_fails_in_batch_update_mode(self):
"""
Delete nested while being in BATCH_UPDATE mode doesn't delete.
"""
post = models.BlogPost(title='Test blog post')
post.save()
post.blogpostcomment_set.create(text="I hate this blog post")
post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [],
}
serializer = BlogPostSerializer(data=data, instance=post)
self.assertTrue(serializer.is_valid(), serializer.errors)
post = serializer.save()
self.assertEqual(post.blogpostcomment_set.count(), 2)
def test_fails_in_batch_delete_mode(self):
"""
Delete nested while being in BATCH_DELETE mode works.
"""
class LocalSerializer(BlogPostSerializer):
comments = BlogPostCommentSerializer(many=True,
source='blogpostcomment_set', batch_mode=BATCH_DELETE)
post = models.BlogPost(title='Test blog post')
post.save()
post.blogpostcomment_set.create(text="I hate this blog post")
post.blogpostcomment_set.create(text="I love this blog post")
data = {
'id': post.id,
'title': 'Test blog post',
'comments': [],
}
serializer = LocalSerializer(data=data, instance=post)
self.assertTrue(serializer.is_valid(), serializer.errors)
post = serializer.save()
self.assertEqual(post.blogpostcomment_set.count(), 0)