mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 04:20:12 +03:00
Merge 066e4402c4
into 5333a93126
This commit is contained in:
commit
613899e58e
|
@ -35,6 +35,11 @@ from rest_framework.settings import api_settings
|
||||||
from rest_framework.relations import *
|
from rest_framework.relations import *
|
||||||
from rest_framework.fields import *
|
from rest_framework.fields import *
|
||||||
|
|
||||||
|
# nested mode constants
|
||||||
|
BATCH_ADD = 1
|
||||||
|
BATCH_UPDATE = 2
|
||||||
|
BATCH_DELETE = 4
|
||||||
|
|
||||||
|
|
||||||
def _resolve_model(obj):
|
def _resolve_model(obj):
|
||||||
"""
|
"""
|
||||||
|
@ -179,18 +184,36 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
_options_class = SerializerOptions
|
_options_class = SerializerOptions
|
||||||
_dict_class = SortedDictWithMetadata
|
_dict_class = SortedDictWithMetadata
|
||||||
|
batch_mode = BATCH_UPDATE
|
||||||
|
|
||||||
def __init__(self, instance=None, data=None, files=None,
|
def __init__(self, instance=None, data=None, files=None,
|
||||||
context=None, partial=False, many=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)
|
super(BaseSerializer, self).__init__(**kwargs)
|
||||||
self.opts = self._options_class(self.Meta)
|
self.opts = self._options_class(self.Meta)
|
||||||
self.parent = None
|
self.parent = None
|
||||||
self.root = None
|
self.root = None
|
||||||
self.partial = partial
|
self.partial = partial
|
||||||
self.many = many
|
self.many = many
|
||||||
|
|
||||||
|
# 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
|
self.allow_add_remove = allow_add_remove
|
||||||
|
|
||||||
|
if batch_mode:
|
||||||
|
self.batch_mode = batch_mode
|
||||||
|
|
||||||
self.context = context or {}
|
self.context = context or {}
|
||||||
|
|
||||||
self.init_data = data
|
self.init_data = data
|
||||||
|
@ -205,9 +228,23 @@ class BaseSerializer(WritableField):
|
||||||
if many and instance is not None and not hasattr(instance, '__iter__'):
|
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')
|
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')
|
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.
|
# Methods to determine which fields to use when (de)serializing objects.
|
||||||
|
|
||||||
|
@ -464,7 +501,8 @@ class BaseSerializer(WritableField):
|
||||||
'context': self.context,
|
'context': self.context,
|
||||||
'partial': self.partial,
|
'partial': self.partial,
|
||||||
'many': self.many,
|
'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)
|
serializer = self.__class__(**kwargs)
|
||||||
|
|
||||||
|
@ -526,15 +564,19 @@ class BaseSerializer(WritableField):
|
||||||
# Determine which object we're updating
|
# Determine which object we're updating
|
||||||
identity = self.get_identity(item)
|
identity = self.get_identity(item)
|
||||||
self.object = identity_to_objects.pop(identity, None)
|
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)
|
ret.append(None)
|
||||||
errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
|
errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
|
||||||
continue
|
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))
|
ret.append(self.from_native(item, None))
|
||||||
errors.append(self._errors)
|
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()
|
ret._deleted = identity_to_objects.values()
|
||||||
|
|
||||||
self._errors = any(errors) and errors or []
|
self._errors = any(errors) and errors or []
|
||||||
|
|
|
@ -6,9 +6,30 @@ Doesn't cover model serializers.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.serializers import BATCH_ADD, BATCH_UPDATE, BATCH_DELETE
|
||||||
from . import models
|
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):
|
class WritableNestedSerializerBasicTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for deserializing nested entities.
|
Tests for deserializing nested entities.
|
||||||
|
@ -315,33 +336,273 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class NestedModelSerializerUpdateTests(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")
|
john = models.Person.objects.create(name="john")
|
||||||
|
|
||||||
post = john.blogpost_set.create(title="Test blog post")
|
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 hate this blog post")
|
||||||
post.blogpostcomment_set.create(text="I love 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)
|
serialize = PersonSerializer(instance=john)
|
||||||
deserialize = PersonSerializer(data=serialize.data, 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 = deserialize.object
|
||||||
result.save()
|
result.save()
|
||||||
self.assertEqual(result.id, john.id)
|
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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user