diff options
| author | Tom Christie | 2014-12-08 14:56:45 +0000 | 
|---|---|---|
| committer | Tom Christie | 2014-12-08 14:56:45 +0000 | 
| commit | eee02a47d997bd4439fe5fbdc01979d8f372247a (patch) | |
| tree | d87d8b4b17198ccf33c8515033a2378e3dc4f3f0 | |
| parent | ef89c1566329f723e8a54f58ebe5aef51489f1b9 (diff) | |
| download | django-rest-framework-eee02a47d997bd4439fe5fbdc01979d8f372247a.tar.bz2 | |
Added ListSerializer.validate(). Closes #2168.
| -rw-r--r-- | rest_framework/fields.py | 38 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 108 | ||||
| -rw-r--r-- | 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 37adbe16..0c6c2d39 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -294,31 +294,47 @@ class Field(object):              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 39523077..fb6c826b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -229,6 +229,35 @@ class SerializerMetaclass(type):          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 @@ class Serializer(BaseSerializer):          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 @@ class Serializer(BaseSerializer):          """          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 @@ class ListSerializer(BaseSerializer):              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 @@ class ListSerializer(BaseSerializer):              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 640067e3..35b68ae7 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -272,3 +272,19 @@ class TestNestedListOfListsSerializer:          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']} | 
