From eee02a47d997bd4439fe5fbdc01979d8f372247a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Dec 2014 14:56:45 +0000 Subject: [PATCH] Added ListSerializer.validate(). Closes #2168. --- rest_framework/fields.py | 38 ++++++++---- rest_framework/serializers.py | 108 ++++++++++++++++++++------------- tests/test_serializer_lists.py | 16 +++++ 3 files changed, 108 insertions(+), 54 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 37adbe16f6..0c6c2d390d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -294,31 +294,47 @@ def get_default(self): return self.default() return self.default - def run_validation(self, data=empty): + def validate_empty_values(self, data): """ - Validate a simple representation and return the internal value. - - The provided data may be `empty` if no representation was included - in the input. - - May raise `SkipField` if the field should not be included in the - validated data. + Validate empty values, and either: + + * Raise `ValidationError`, indicating invalid data. + * Raise `SkipField`, indicating that the field should be ignored. + * Return (True, data), indicating an empty value that should be + returned without any furhter validation being applied. + * Return (False, data), indicating a non-empty value, that should + have validation applied as normal. """ if self.read_only: - return self.get_default() + return (True, self.get_default()) if data is empty: if getattr(self.root, 'partial', False): raise SkipField() if self.required: self.fail('required') - return self.get_default() + return (True, self.get_default()) if data is None: if not self.allow_null: self.fail('null') - return None + return (True, None) + + return (False, data) + def run_validation(self, data=empty): + """ + Validate a simple representation and return the internal value. + + The provided data may be `empty` if no representation was included + in the input. + + May raise `SkipField` if the field should not be included in the + validated data. + """ + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data value = self.to_internal_value(data) self.run_validators(value) return value diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 39523077af..fb6c826b8b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -229,6 +229,35 @@ def __new__(cls, name, bases, attrs): return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) +def get_validation_error_detail(exc): + assert isinstance(exc, (ValidationError, DjangoValidationError)) + + if isinstance(exc, DjangoValidationError): + # Normally you should raise `serializers.ValidationError` + # inside your codebase, but we handle Django's validation + # exception class as well for simpler compat. + # Eg. Calling Model.clean() explicitly inside Serializer.validate() + return { + api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) + } + elif isinstance(exc.detail, dict): + # If errors may be a dict we use the standard {key: list of values}. + # Here we ensure that all the values are *lists* of errors. + return dict([ + (key, value if isinstance(value, list) else [value]) + for key, value in exc.detail.items() + ]) + elif isinstance(exc.detail, list): + # Errors raised as a list are non-field errors. + return { + api_settings.NON_FIELD_ERRORS_KEY: exc.detail + } + # Errors raised as a string are non-field errors. + return { + api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] + } + + @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): default_error_messages = { @@ -293,55 +322,17 @@ def run_validation(self, data=empty): performed by validators and the `.validate()` method should be coerced into an error dictionary with a 'non_fields_error' key. """ - if data is empty: - if getattr(self.root, 'partial', False): - raise SkipField() - if self.required: - self.fail('required') - return self.get_default() - - if data is None: - if not self.allow_null: - self.fail('null') - return None - - if not isinstance(data, dict): - message = self.error_messages['invalid'].format( - datatype=type(data).__name__ - ) - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }) + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data value = self.to_internal_value(data) try: self.run_validators(value) value = self.validate(value) assert value is not None, '.validate() should return the validated data' - except ValidationError as exc: - if isinstance(exc.detail, dict): - # .validate() errors may be a dict, in which case, use - # standard {key: list of values} style. - raise ValidationError(dict([ - (key, value if isinstance(value, list) else [value]) - for key, value in exc.detail.items() - ])) - elif isinstance(exc.detail, list): - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: exc.detail - }) - else: - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] - }) - except DjangoValidationError as exc: - # Normally you should raise `serializers.ValidationError` - # inside your codebase, but we handle Django's validation - # exception class as well for simpler compat. - # Eg. Calling Model.clean() explicitly inside Serializer.validate() - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) - }) + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=get_validation_error_detail(exc)) return value @@ -349,6 +340,14 @@ def to_internal_value(self, data): """ Dict of native values <- Dict of primitive datatypes. """ + if not isinstance(data, dict): + message = self.error_messages['invalid'].format( + datatype=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }) + ret = OrderedDict() errors = OrderedDict() fields = [ @@ -462,6 +461,26 @@ def get_value(self, dictionary): return html.parse_html_list(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) + def run_validation(self, data=empty): + """ + We override the default `run_validation`, because the validation + performed by validators and the `.validate()` method should + be coerced into an error dictionary with a 'non_fields_error' key. + """ + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data + + value = self.to_internal_value(data) + try: + self.run_validators(value) + value = self.validate(value) + assert value is not None, '.validate() should return the validated data' + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=get_validation_error_detail(exc)) + + return value + def to_internal_value(self, data): """ List of dicts of native values <- List of dicts of primitive datatypes. @@ -503,6 +522,9 @@ def to_representation(self, data): self.child.to_representation(item) for item in iterable ] + def validate(self, attrs): + return attrs + def update(self, instance, validated_data): raise NotImplementedError( "Serializers with many=True do not support multiple update by " diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index 640067e3a6..35b68ae7de 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -272,3 +272,19 @@ def test_validate_html_input(self): serializer = self.Serializer(data=input_data) assert serializer.is_valid() assert serializer.validated_data == expected_output + + +class TestListSerializerClass: + """Tests for a custom list_serializer_class.""" + def test_list_serializer_class_validate(self): + class CustomListSerializer(serializers.ListSerializer): + def validate(self, attrs): + raise serializers.ValidationError('Non field error') + + class TestSerializer(serializers.Serializer): + class Meta: + list_serializer_class = CustomListSerializer + + serializer = TestSerializer(data=[], many=True) + assert not serializer.is_valid() + assert serializer.errors == {'non_field_errors': ['Non field error']}