mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-02 19:40:13 +03:00
Merge 877ef5c9ed
into 12d1544101
This commit is contained in:
commit
abfb1c0085
|
@ -297,6 +297,35 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
|||
|
||||
---
|
||||
|
||||
# Compound Fields
|
||||
|
||||
These fields represent compound datatypes, which build on other fields with some additional aspect such collecting multiple elements.
|
||||
|
||||
## ListField
|
||||
|
||||
A list representation, whose elements are described by a given item field. This means that all elements must meet the definition of
|
||||
that field. The item field can be another field type (e.g., CharField) or a serializer. If `item_field` is not given, then the
|
||||
list-items are passed through as-is, and can be anything. Note that in this case, any non-native list elements wouldn't be properly
|
||||
prepared for data rendering.
|
||||
|
||||
**Signature:** `ListField(item_field=None)`
|
||||
|
||||
## DictField
|
||||
|
||||
A dictionary representation, whose values are described by a given value field. This means that all values must meet the definition of
|
||||
that field. The value field can be another field type (e.g., CharField) or a serializer. If `value_field` is not given, then the `dict`
|
||||
values are passed through-as-is, and can be anything. Note that in this case, any non-native `dict` values wouldn't be properly
|
||||
prepared for data rendering.
|
||||
|
||||
Dictionary keys are presumed to be character strings or convertible to such, and so during processing are casted to `unicode`. If
|
||||
necessary, options for unicode conversion (such as the encoding, or error processing) can be provided to a `DictField`. For more info,
|
||||
see [py_unicode].
|
||||
|
||||
**Signature:** `DictField(value_field=None, unicode_options=None)`
|
||||
|
||||
If given, unicode_options must be a dict providing options per the [unicode](http://docs.python.org/2/library/functions.html#unicode)
|
||||
function.
|
||||
|
||||
# Custom fields
|
||||
|
||||
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
|
||||
|
@ -345,3 +374,4 @@ As an example, let's create a field that can be used represent the class name of
|
|||
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
||||
[py_unicode]: http://docs.python.org/2/howto/unicode.html
|
||||
|
|
108
rest_framework/compound_fields.py
Normal file
108
rest_framework/compound_fields.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
"""
|
||||
Compound fields for processing values that are lists and dicts of values described by embedded
|
||||
fields.
|
||||
"""
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .fields import WritableField
|
||||
from rest_framework.compat import six
|
||||
|
||||
|
||||
class ListField(WritableField):
|
||||
"""
|
||||
A field whose values are lists of items described by the given item field. The item field can
|
||||
be another field type (e.g., CharField) or a serializer.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_type': _('%(value)s is not a list.'),
|
||||
}
|
||||
|
||||
def __init__(self, item_field=None, *args, **kwargs):
|
||||
super(ListField, self).__init__(*args, **kwargs)
|
||||
self.item_field = item_field
|
||||
|
||||
def to_native(self, obj):
|
||||
if self.item_field and obj:
|
||||
return [
|
||||
self.item_field.to_native(item)
|
||||
for item in obj
|
||||
]
|
||||
return obj
|
||||
|
||||
def from_native(self, data):
|
||||
if self.item_field and data:
|
||||
return [
|
||||
self.item_field.from_native(item_data)
|
||||
for item_data in data
|
||||
]
|
||||
return data
|
||||
|
||||
def validate(self, value):
|
||||
super(ListField, self).validate(value)
|
||||
|
||||
if not isinstance(value, list):
|
||||
raise ValidationError(self.error_messages['invalid_type'] % {'value': value})
|
||||
|
||||
if self.item_field:
|
||||
errors = {}
|
||||
for index, item in enumerate(value):
|
||||
try:
|
||||
self.item_field.validate(item)
|
||||
self.item_field.run_validators(item)
|
||||
except ValidationError as e:
|
||||
errors[index] = [e]
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
class DictField(WritableField):
|
||||
"""
|
||||
A field whose values are dicts of values described by the given value field. The value field
|
||||
can be another field type (e.g., CharField) or a serializer.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_type': _('%(value)s is not a dict.'),
|
||||
}
|
||||
|
||||
def __init__(self, value_field=None, unicode_options=None, *args, **kwargs):
|
||||
super(DictField, self).__init__(*args, **kwargs)
|
||||
self.value_field = value_field
|
||||
self.unicode_options = unicode_options or {}
|
||||
|
||||
def to_native(self, obj):
|
||||
if self.value_field and obj:
|
||||
return dict(
|
||||
(six.text_type(key, **self.unicode_options), self.value_field.to_native(value))
|
||||
for key, value in obj.items()
|
||||
)
|
||||
return obj
|
||||
|
||||
def from_native(self, data):
|
||||
if self.value_field and data:
|
||||
return dict(
|
||||
(six.text_type(key, **self.unicode_options), self.value_field.from_native(value))
|
||||
for key, value in data.items()
|
||||
)
|
||||
return data
|
||||
|
||||
def validate(self, value):
|
||||
super(DictField, self).validate(value)
|
||||
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(self.error_messages['invalid_type'] % {'value': value})
|
||||
|
||||
if self.value_field:
|
||||
errors = {}
|
||||
for k, v in six.iteritems(value):
|
||||
try:
|
||||
self.value_field.validate(v)
|
||||
self.value_field.run_validators(v)
|
||||
except ValidationError as e:
|
||||
errors[k] = [e]
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
|
@ -331,7 +331,7 @@ class BaseSerializer(WritableField):
|
|||
|
||||
return ret
|
||||
|
||||
def from_native(self, data, files):
|
||||
def from_native(self, data, files=None):
|
||||
"""
|
||||
Deserialize primitives -> objects.
|
||||
"""
|
||||
|
|
195
rest_framework/tests/test_compound_fields.py
Normal file
195
rest_framework/tests/test_compound_fields.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
"""
|
||||
General serializer field tests.
|
||||
"""
|
||||
|
||||
|
||||
from datetime import date
|
||||
import unittest
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compound_fields import DictField
|
||||
from rest_framework.compound_fields import ListField
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import DateField
|
||||
|
||||
|
||||
class ListFieldTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the ListField behavior
|
||||
"""
|
||||
|
||||
def test_from_native_no_item_field(self):
|
||||
"""
|
||||
When a ListField has no item-field, from_native should return the data it was given
|
||||
un-processed.
|
||||
"""
|
||||
field = ListField()
|
||||
data = range(5)
|
||||
obj = field.from_native(data)
|
||||
self.assertEqual(data, obj)
|
||||
|
||||
def test_to_native_no_item_field(self):
|
||||
"""
|
||||
When a ListField has no item-field, to_native should return the data it was given
|
||||
un-processed.
|
||||
"""
|
||||
field = ListField()
|
||||
obj = range(5)
|
||||
data = field.to_native(obj)
|
||||
self.assertEqual(obj, data)
|
||||
|
||||
def test_from_native_with_item_field(self):
|
||||
"""
|
||||
When a ListField has an item-field, from_native should return a list of elements resulting
|
||||
from the application of the item-field's from_native method to each element of the input
|
||||
data list.
|
||||
"""
|
||||
field = ListField(DateField())
|
||||
data = ["2000-01-01", "2000-01-02"]
|
||||
obj = field.from_native(data)
|
||||
self.assertEqual([date(2000, 1, 1), date(2000, 1, 2)], obj)
|
||||
|
||||
def test_to_native_with_item_field(self):
|
||||
"""
|
||||
When a ListField has an item-field, to_native should return a list of elements resulting
|
||||
from the application of the item-field's to_native method to each element of the input
|
||||
object list.
|
||||
"""
|
||||
field = ListField(DateField(format=ISO_8601))
|
||||
obj = [date(2000, 1, 1), date(2000, 1, 2)]
|
||||
data = field.to_native(obj)
|
||||
self.assertEqual(["2000-01-01", "2000-01-02"], data)
|
||||
|
||||
def test_missing_required_list(self):
|
||||
"""
|
||||
When a ListField requires a value, then validate will raise a ValidationError on a missing
|
||||
(None) value.
|
||||
"""
|
||||
field = ListField()
|
||||
self.assertRaises(ValidationError, field.validate, None)
|
||||
|
||||
def test_validate_non_list(self):
|
||||
"""
|
||||
When a ListField is given a non-list value, then validate will raise a ValidationError.
|
||||
"""
|
||||
field = ListField()
|
||||
self.assertRaises(ValidationError, field.validate, 'notAList')
|
||||
|
||||
def test_validate_empty_list(self):
|
||||
"""
|
||||
When a ListField requires a value, then validate will raise a ValidationError on an empty
|
||||
value.
|
||||
"""
|
||||
field = ListField()
|
||||
self.assertRaises(ValidationError, field.validate, [])
|
||||
|
||||
def test_validate_elements_valid(self):
|
||||
"""
|
||||
When a ListField is given a list whose elements are valid for the item-field, then validate
|
||||
will not raise a ValidationError.
|
||||
"""
|
||||
field = ListField(CharField(max_length=5))
|
||||
try:
|
||||
field.validate(["a", "b", "c"])
|
||||
except ValidationError:
|
||||
self.fail("ValidationError was raised")
|
||||
|
||||
def test_validate_elements_invalid(self):
|
||||
"""
|
||||
When a ListField is given a list containing elements that are invalid for the item-field,
|
||||
then validate will raise a ValidationError.
|
||||
"""
|
||||
field = ListField(CharField(max_length=5))
|
||||
self.assertRaises(ValidationError, field.validate, ["012345", "012345"])
|
||||
|
||||
|
||||
class DictFieldTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the DictField behavior
|
||||
"""
|
||||
|
||||
def test_from_native_no_value_field(self):
|
||||
"""
|
||||
When a DictField has no value-field, from_native should return the data it was given
|
||||
un-processed.
|
||||
"""
|
||||
field = DictField()
|
||||
data = {"a": 1, "b": 2}
|
||||
obj = field.from_native(data)
|
||||
self.assertEqual(data, obj)
|
||||
|
||||
def test_to_native_no_value_field(self):
|
||||
"""
|
||||
When a DictField has no value-field, to_native should return the data it was given
|
||||
un-processed.
|
||||
"""
|
||||
field = DictField()
|
||||
obj = {"a": 1, "b": 2}
|
||||
data = field.to_native(obj)
|
||||
self.assertEqual(obj, data)
|
||||
|
||||
def test_from_native_with_value_field(self):
|
||||
"""
|
||||
When a DictField has an value-field, from_native should return a dict of elements resulting
|
||||
from the application of the value-field's from_native method to each value of the input
|
||||
data dict.
|
||||
"""
|
||||
field = DictField(DateField())
|
||||
data = {"a": "2000-01-01", "b": "2000-01-02"}
|
||||
obj = field.from_native(data)
|
||||
self.assertEqual({"a": date(2000, 1, 1), "b": date(2000, 1, 2)}, obj)
|
||||
|
||||
def test_to_native_with_value_field(self):
|
||||
"""
|
||||
When a DictField has an value-field, to_native should return a dict of elements resulting
|
||||
from the application of the value-field's to_native method to each value of the input
|
||||
object dict.
|
||||
"""
|
||||
field = DictField(DateField(format=ISO_8601))
|
||||
obj = {"a": date(2000, 1, 1), "b": date(2000, 1, 2)}
|
||||
data = field.to_native(obj)
|
||||
self.assertEqual({"a": "2000-01-01", "b": "2000-01-02"}, data)
|
||||
|
||||
def test_missing_required_dict(self):
|
||||
"""
|
||||
When a DictField requires a value, then validate will raise a ValidationError on a missing
|
||||
(None) value.
|
||||
"""
|
||||
field = DictField()
|
||||
self.assertRaises(ValidationError, field.validate, None)
|
||||
|
||||
def test_validate_non_dict(self):
|
||||
"""
|
||||
When a DictField is given a non-dict value, then validate will raise a ValidationError.
|
||||
"""
|
||||
field = DictField()
|
||||
self.assertRaises(ValidationError, field.validate, 'notADict')
|
||||
|
||||
def test_validate_empty_dict(self):
|
||||
"""
|
||||
When a DictField requires a value, then validate will raise a ValidationError on an empty
|
||||
value.
|
||||
"""
|
||||
field = DictField()
|
||||
self.assertRaises(ValidationError, field.validate, {})
|
||||
|
||||
def test_validate_elements_valid(self):
|
||||
"""
|
||||
When a DictField is given a dict whose values are valid for the value-field, then validate
|
||||
will not raise a ValidationError.
|
||||
"""
|
||||
field = DictField(CharField(max_length=5))
|
||||
try:
|
||||
field.validate({"a": "a", "b": "b", "c": "c"})
|
||||
except ValidationError:
|
||||
self.fail("ValidationError was raised")
|
||||
|
||||
def test_validate_elements_invalid(self):
|
||||
"""
|
||||
When a DictField is given a dict containing values that are invalid for the value-field,
|
||||
then validate will raise a ValidationError.
|
||||
"""
|
||||
field = DictField(CharField(max_length=5))
|
||||
self.assertRaises(ValidationError, field.validate, {"a": "012345", "b": "012345"})
|
Loading…
Reference in New Issue
Block a user