Support for bulk create. Closes #1965.

This commit is contained in:
Tom Christie 2014-11-06 10:34:59 +00:00
parent 73daf40715
commit ed541864e6
4 changed files with 157 additions and 114 deletions

View File

@ -943,7 +943,7 @@ class ChoiceField(Field):
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
default_error_messages = { default_error_messages = {
'invalid_choice': _('`{input}` is not a valid choice.'), 'invalid_choice': _('`{input}` is not a valid choice.'),
'not_a_list': _('Expected a list of items but got type `{input_type}`') 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
} }
default_empty_html = [] default_empty_html = []

View File

@ -90,12 +90,10 @@ class BaseSerializer(Field):
raise NotImplementedError('`create()` must be implemented.') raise NotImplementedError('`create()` must be implemented.')
def save(self, **kwargs): def save(self, **kwargs):
validated_data = self.validated_data validated_data = dict(
if kwargs: list(self.validated_data.items()) +
validated_data = dict( list(kwargs.items())
list(validated_data.items()) + )
list(kwargs.items())
)
if self.instance is not None: if self.instance is not None:
self.instance = self.update(self.instance, validated_data) self.instance = self.update(self.instance, validated_data)
@ -210,9 +208,9 @@ class BoundField(object):
class NestedBoundField(BoundField): class NestedBoundField(BoundField):
""" """
This BoundField additionally implements __iter__ and __getitem__ This `BoundField` additionally implements __iter__ and __getitem__
in order to support nested bound fields. This class is the type of in order to support nested bound fields. This class is the type of
BoundField that is used for serializer fields. `BoundField` that is used for serializer fields.
""" """
def __iter__(self): def __iter__(self):
for field in self.fields.values(): for field in self.fields.values():
@ -460,6 +458,10 @@ class ListSerializer(BaseSerializer):
child = None child = None
many = True many = True
default_error_messages = {
'not_a_list': _('Expected a list of items but got type `{input_type}`.')
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.' assert self.child is not None, '`child` is a required argument.'
@ -485,7 +487,31 @@ class ListSerializer(BaseSerializer):
""" """
if html.is_html_input(data): if html.is_html_input(data):
data = html.parse_html_list(data) data = html.parse_html_list(data)
return [self.child.run_validation(item) for item in data]
if not isinstance(data, list):
message = self.error_messages['not_a_list'].format(
input_type=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
})
ret = []
errors = ReturnList(serializer=self)
for item in data:
try:
validated = self.child.run_validation(item)
except ValidationError, exc:
errors.append(exc.detail)
else:
ret.append(validated)
errors.append({})
if any(errors):
raise ValidationError(errors)
return ret
def to_representation(self, data): def to_representation(self, data):
""" """
@ -497,8 +523,25 @@ class ListSerializer(BaseSerializer):
serializer=self serializer=self
) )
def create(self, attrs_list): def save(self, **kwargs):
return [self.child.create(attrs) for attrs in attrs_list] assert self.instance is None, (
"Serializers do not support multiple update by default, because "
"it would be unclear how to deal with insertions, updates and "
"deletions. If you need to support multiple update, use a "
"`ListSerializer` class and override `.save()` so you can specify "
"the behavior exactly."
)
validated_data = [
dict(list(attrs.items()) + list(kwargs.items()))
for attrs in self.validated_data
]
self.instance = [
self.child.create(attrs) for attrs in validated_data
]
return self.instance
def __repr__(self): def __repr__(self):
return representation.list_repr(self, indent=1) return representation.list_repr(self, indent=1)

View File

@ -859,7 +859,7 @@ class TestMultipleChoiceField(FieldValues):
('aircon', 'manual'): set(['aircon', 'manual']), ('aircon', 'manual'): set(['aircon', 'manual']),
} }
invalid_inputs = { invalid_inputs = {
'abc': ['Expected a list of items but got type `str`'], 'abc': ['Expected a list of items but got type `str`.'],
('aircon', 'incorrect'): ['`incorrect` is not a valid choice.'] ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
} }
outputs = [ outputs = [

View File

@ -1,123 +1,123 @@
# """ """
# Tests to cover bulk create and update using serializers. Tests to cover bulk create and update using 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
# class BulkCreateSerializerTests(TestCase): class BulkCreateSerializerTests(TestCase):
# """ """
# Creating multiple instances using serializers. Creating multiple instances using serializers.
# """ """
# def setUp(self): def setUp(self):
# class BookSerializer(serializers.Serializer): class BookSerializer(serializers.Serializer):
# id = serializers.IntegerField() id = serializers.IntegerField()
# title = serializers.CharField(max_length=100) title = serializers.CharField(max_length=100)
# author = serializers.CharField(max_length=100) author = serializers.CharField(max_length=100)
# self.BookSerializer = BookSerializer self.BookSerializer = BookSerializer
# def test_bulk_create_success(self): def test_bulk_create_success(self):
# """ """
# Correct bulk update serialization should return the input data. Correct bulk update serialization should return the input data.
# """ """
# data = [ data = [
# { {
# 'id': 0, 'id': 0,
# 'title': 'The electric kool-aid acid test', 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe' 'author': 'Tom Wolfe'
# }, { }, {
# 'id': 1, 'id': 1,
# 'title': 'If this is a man', 'title': 'If this is a man',
# 'author': 'Primo Levi' 'author': 'Primo Levi'
# }, { }, {
# 'id': 2, 'id': 2,
# 'title': 'The wind-up bird chronicle', 'title': 'The wind-up bird chronicle',
# 'author': 'Haruki Murakami' 'author': 'Haruki Murakami'
# } }
# ] ]
# serializer = self.BookSerializer(data=data, many=True) serializer = self.BookSerializer(data=data, many=True)
# self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.is_valid(), True)
# self.assertEqual(serializer.object, data) self.assertEqual(serializer.validated_data, data)
# def test_bulk_create_errors(self): def test_bulk_create_errors(self):
# """ """
# Correct bulk update serialization should return the input data. Incorrect bulk create serialization should return errors.
# """ """
# data = [ data = [
# { {
# 'id': 0, 'id': 0,
# 'title': 'The electric kool-aid acid test', 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe' 'author': 'Tom Wolfe'
# }, { }, {
# 'id': 1, 'id': 1,
# 'title': 'If this is a man', 'title': 'If this is a man',
# 'author': 'Primo Levi' 'author': 'Primo Levi'
# }, { }, {
# 'id': 'foo', 'id': 'foo',
# 'title': 'The wind-up bird chronicle', 'title': 'The wind-up bird chronicle',
# 'author': 'Haruki Murakami' 'author': 'Haruki Murakami'
# } }
# ] ]
# expected_errors = [ expected_errors = [
# {}, {},
# {}, {},
# {'id': ['Enter a whole number.']} {'id': ['A valid integer is required.']}
# ] ]
# serializer = self.BookSerializer(data=data, many=True) serializer = self.BookSerializer(data=data, many=True)
# self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.is_valid(), False)
# self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
# def test_invalid_list_datatype(self): def test_invalid_list_datatype(self):
# """ """
# Data containing list of incorrect data type should return errors. Data containing list of incorrect data type should return errors.
# """ """
# data = ['foo', 'bar', 'baz'] data = ['foo', 'bar', 'baz']
# serializer = self.BookSerializer(data=data, many=True) serializer = self.BookSerializer(data=data, many=True)
# self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.is_valid(), False)
# expected_errors = [ expected_errors = [
# {'non_field_errors': ['Invalid data']}, {'non_field_errors': ['Invalid data. Expected a dictionary, but got unicode.']},
# {'non_field_errors': ['Invalid data']}, {'non_field_errors': ['Invalid data. Expected a dictionary, but got unicode.']},
# {'non_field_errors': ['Invalid data']} {'non_field_errors': ['Invalid data. Expected a dictionary, but got unicode.']}
# ] ]
# self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
# def test_invalid_single_datatype(self): def test_invalid_single_datatype(self):
# """ """
# Data containing a single incorrect data type should return errors. Data containing a single incorrect data type should return errors.
# """ """
# data = 123 data = 123
# serializer = self.BookSerializer(data=data, many=True) serializer = self.BookSerializer(data=data, many=True)
# self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.is_valid(), False)
# expected_errors = {'non_field_errors': ['Expected a list of items.']} expected_errors = {'non_field_errors': ['Expected a list of items but got type `int`.']}
# self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
# def test_invalid_single_object(self): def test_invalid_single_object(self):
# """ """
# Data containing only a single object, instead of a list of objects Data containing only a single object, instead of a list of objects
# should return errors. should return errors.
# """ """
# data = { data = {
# 'id': 0, 'id': 0,
# 'title': 'The electric kool-aid acid test', 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe' 'author': 'Tom Wolfe'
# } }
# serializer = self.BookSerializer(data=data, many=True) serializer = self.BookSerializer(data=data, many=True)
# self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.is_valid(), False)
# expected_errors = {'non_field_errors': ['Expected a list of items.']} expected_errors = {'non_field_errors': ['Expected a list of items but got type `dict`.']}
# self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
# class BulkUpdateSerializerTests(TestCase): # class BulkUpdateSerializerTests(TestCase):