Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ListSerializer.validate(). #2232

Merged
merged 1 commit into from
Dec 8, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 65 additions & 43 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -293,62 +322,32 @@ 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

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 = [
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 "
Expand Down
16 changes: 16 additions & 0 deletions tests/test_serializer_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']}