diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b554f2382..a9e2e48bd 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -943,7 +943,7 @@ class ChoiceField(Field): class MultipleChoiceField(ChoiceField): default_error_messages = { '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 = [] diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index dfac75fc5..cbac39928 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -90,12 +90,10 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): - validated_data = self.validated_data - if kwargs: - validated_data = dict( - list(validated_data.items()) + - list(kwargs.items()) - ) + validated_data = dict( + list(self.validated_data.items()) + + list(kwargs.items()) + ) if self.instance is not None: self.instance = self.update(self.instance, validated_data) @@ -210,9 +208,9 @@ class BoundField(object): 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 - BoundField that is used for serializer fields. + `BoundField` that is used for serializer fields. """ def __iter__(self): for field in self.fields.values(): @@ -460,6 +458,10 @@ class ListSerializer(BaseSerializer): child = None many = True + default_error_messages = { + 'not_a_list': _('Expected a list of items but got type `{input_type}`.') + } + def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) assert self.child is not None, '`child` is a required argument.' @@ -485,7 +487,31 @@ class ListSerializer(BaseSerializer): """ if html.is_html_input(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): """ @@ -497,8 +523,25 @@ class ListSerializer(BaseSerializer): serializer=self ) - def create(self, attrs_list): - return [self.child.create(attrs) for attrs in attrs_list] + def save(self, **kwargs): + 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): return representation.list_repr(self, indent=1) diff --git a/tests/test_fields.py b/tests/test_fields.py index 96d09900e..5db381acc 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -859,7 +859,7 @@ class TestMultipleChoiceField(FieldValues): ('aircon', 'manual'): set(['aircon', 'manual']), } 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.'] } outputs = [ diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py index 3341ce590..85b6b2fa3 100644 --- a/tests/test_serializer_bulk_update.py +++ b/tests/test_serializer_bulk_update.py @@ -1,123 +1,123 @@ -# """ -# Tests to cover bulk create and update using serializers. -# """ -# from __future__ import unicode_literals -# from django.test import TestCase -# from rest_framework import serializers +""" +Tests to cover bulk create and update using serializers. +""" +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework import serializers -# class BulkCreateSerializerTests(TestCase): -# """ -# Creating multiple instances using serializers. -# """ +class BulkCreateSerializerTests(TestCase): + """ + Creating multiple instances using serializers. + """ -# def setUp(self): -# class BookSerializer(serializers.Serializer): -# id = serializers.IntegerField() -# title = serializers.CharField(max_length=100) -# author = serializers.CharField(max_length=100) + def setUp(self): + class BookSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField(max_length=100) + author = serializers.CharField(max_length=100) -# self.BookSerializer = BookSerializer + self.BookSerializer = BookSerializer -# def test_bulk_create_success(self): -# """ -# Correct bulk update serialization should return the input data. -# """ + def test_bulk_create_success(self): + """ + Correct bulk update serialization should return the input data. + """ -# data = [ -# { -# 'id': 0, -# 'title': 'The electric kool-aid acid test', -# 'author': 'Tom Wolfe' -# }, { -# 'id': 1, -# 'title': 'If this is a man', -# 'author': 'Primo Levi' -# }, { -# 'id': 2, -# 'title': 'The wind-up bird chronicle', -# 'author': 'Haruki Murakami' -# } -# ] + data = [ + { + 'id': 0, + 'title': 'The electric kool-aid acid test', + 'author': 'Tom Wolfe' + }, { + 'id': 1, + 'title': 'If this is a man', + 'author': 'Primo Levi' + }, { + 'id': 2, + 'title': 'The wind-up bird chronicle', + 'author': 'Haruki Murakami' + } + ] -# serializer = self.BookSerializer(data=data, many=True) -# self.assertEqual(serializer.is_valid(), True) -# self.assertEqual(serializer.object, data) + serializer = self.BookSerializer(data=data, many=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.validated_data, data) -# def test_bulk_create_errors(self): -# """ -# Correct bulk update serialization should return the input data. -# """ + def test_bulk_create_errors(self): + """ + Incorrect bulk create serialization should return errors. + """ -# data = [ -# { -# 'id': 0, -# 'title': 'The electric kool-aid acid test', -# 'author': 'Tom Wolfe' -# }, { -# 'id': 1, -# 'title': 'If this is a man', -# 'author': 'Primo Levi' -# }, { -# 'id': 'foo', -# 'title': 'The wind-up bird chronicle', -# 'author': 'Haruki Murakami' -# } -# ] -# expected_errors = [ -# {}, -# {}, -# {'id': ['Enter a whole number.']} -# ] + data = [ + { + 'id': 0, + 'title': 'The electric kool-aid acid test', + 'author': 'Tom Wolfe' + }, { + 'id': 1, + 'title': 'If this is a man', + 'author': 'Primo Levi' + }, { + 'id': 'foo', + 'title': 'The wind-up bird chronicle', + 'author': 'Haruki Murakami' + } + ] + expected_errors = [ + {}, + {}, + {'id': ['A valid integer is required.']} + ] -# serializer = self.BookSerializer(data=data, many=True) -# self.assertEqual(serializer.is_valid(), False) -# self.assertEqual(serializer.errors, expected_errors) + serializer = self.BookSerializer(data=data, many=True) + self.assertEqual(serializer.is_valid(), False) + self.assertEqual(serializer.errors, expected_errors) -# def test_invalid_list_datatype(self): -# """ -# Data containing list of incorrect data type should return errors. -# """ -# data = ['foo', 'bar', 'baz'] -# serializer = self.BookSerializer(data=data, many=True) -# self.assertEqual(serializer.is_valid(), False) + def test_invalid_list_datatype(self): + """ + Data containing list of incorrect data type should return errors. + """ + data = ['foo', 'bar', 'baz'] + serializer = self.BookSerializer(data=data, many=True) + self.assertEqual(serializer.is_valid(), False) -# expected_errors = [ -# {'non_field_errors': ['Invalid data']}, -# {'non_field_errors': ['Invalid data']}, -# {'non_field_errors': ['Invalid data']} -# ] + expected_errors = [ + {'non_field_errors': ['Invalid data. Expected a dictionary, but got unicode.']}, + {'non_field_errors': ['Invalid data. Expected a dictionary, but got unicode.']}, + {'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): -# """ -# Data containing a single incorrect data type should return errors. -# """ -# data = 123 -# serializer = self.BookSerializer(data=data, many=True) -# self.assertEqual(serializer.is_valid(), False) + def test_invalid_single_datatype(self): + """ + Data containing a single incorrect data type should return errors. + """ + data = 123 + serializer = self.BookSerializer(data=data, many=True) + 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): -# """ -# Data containing only a single object, instead of a list of objects -# should return errors. -# """ -# data = { -# 'id': 0, -# 'title': 'The electric kool-aid acid test', -# 'author': 'Tom Wolfe' -# } -# serializer = self.BookSerializer(data=data, many=True) -# self.assertEqual(serializer.is_valid(), False) + def test_invalid_single_object(self): + """ + Data containing only a single object, instead of a list of objects + should return errors. + """ + data = { + 'id': 0, + 'title': 'The electric kool-aid acid test', + 'author': 'Tom Wolfe' + } + serializer = self.BookSerializer(data=data, many=True) + 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):