Commit 13794baf by Tom Christie

Bit of extra tidying and plenty of docs

parent b4210f9a
...@@ -273,6 +273,49 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. ...@@ -273,6 +273,49 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
--- ---
# 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 intial datatype, and a primative, serializable datatype. Primative 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 primative objects.
The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
## Examples
Let's look at an example of serializing a class that represents an RGB color value:
class Color(object):
"""
A color represented in the RGB colorspace.
"""
def __init__(self, red, green, blue):
assert(red >= 0 and green >= 0 and blue >= 0)
assert(red < 256 and green < 256 and blue < 256)
self.red, self.green, self.blue = red, green, blue
class ColourField(serializers.WritableField):
"""
Color objects are serialized into "rgb(#, #, #)" notation.
"""
def to_native(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
def from_native(self, data):
data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue)
By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`.
As an example, let's create a field that can be used represent the class name of the object being serialized:
class ClassNameField(serializers.Field):
def field_to_native(self, obj, field_name):
"""
Serialize the object's class name.
"""
return obj.__class__
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
......
...@@ -110,13 +110,15 @@ class BaseSerializer(Field): ...@@ -110,13 +110,15 @@ class BaseSerializer(Field):
_dict_class = SortedDictWithMetadata _dict_class = SortedDictWithMetadata
def __init__(self, instance=None, data=None, files=None, def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=None, source=None): context=None, partial=False, many=None, source=None,
allow_delete=False):
super(BaseSerializer, self).__init__(source=source) super(BaseSerializer, self).__init__(source=source)
self.opts = self._options_class(self.Meta) self.opts = self._options_class(self.Meta)
self.parent = None self.parent = None
self.root = None self.root = None
self.partial = partial self.partial = partial
self.many = many self.many = many
self.allow_delete = allow_delete
self.context = context or {} self.context = context or {}
...@@ -130,6 +132,12 @@ class BaseSerializer(Field): ...@@ -130,6 +132,12 @@ class BaseSerializer(Field):
self._errors = None self._errors = None
self._deleted = None self._deleted = None
if many and instance is not None and not hasattr(instance, '__iter__'):
raise ValueError('instance should be a queryset or other iterable with many=True')
if allow_delete and not many:
raise ValueError('allow_delete should only be used for bulk updates, but you have not set many=True')
##### #####
# Methods to determine which fields to use when (de)serializing objects. # Methods to determine which fields to use when (de)serializing objects.
...@@ -336,8 +344,15 @@ class BaseSerializer(Field): ...@@ -336,8 +344,15 @@ class BaseSerializer(Field):
""" """
This hook is required for bulk update. This hook is required for bulk update.
It is used to determine the canonical identity of a given object. It is used to determine the canonical identity of a given object.
Note that the data has not been validated at this point, so we need
to make sure that we catch any cases of incorrect datatypes being
passed to this method.
""" """
return data.get('id') try:
return data.get('id', None)
except AttributeError:
return None
@property @property
def errors(self): def errors(self):
...@@ -371,21 +386,11 @@ class BaseSerializer(Field): ...@@ -371,21 +386,11 @@ class BaseSerializer(Field):
identities = [self.get_identity(self.to_native(obj)) for obj in objects] identities = [self.get_identity(self.to_native(obj)) for obj in objects]
identity_to_objects = dict(zip(identities, objects)) identity_to_objects = dict(zip(identities, objects))
try: if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
iter(data)
if isinstance(data, dict):
raise TypeError
except TypeError:
self._errors = {'non_field_errors': ['Expected a list of items']}
else:
for item in data: for item in data:
if update: if update:
# Determine which object we're updating # Determine which object we're updating
try:
identity = self.get_identity(item) identity = self.get_identity(item)
except:
self.object = None
else:
self.object = identity_to_objects.pop(identity, None) self.object = identity_to_objects.pop(identity, None)
ret.append(self.from_native(item, None)) ret.append(self.from_native(item, None))
...@@ -396,6 +401,8 @@ class BaseSerializer(Field): ...@@ -396,6 +401,8 @@ class BaseSerializer(Field):
self._errors = any(errors) and errors or [] self._errors = any(errors) and errors or []
else: else:
self._errors = {'non_field_errors': ['Expected a list of items']}
else:
ret = self.from_native(data, files) ret = self.from_native(data, files)
if not self._errors: if not self._errors:
...@@ -445,7 +452,7 @@ class BaseSerializer(Field): ...@@ -445,7 +452,7 @@ class BaseSerializer(Field):
else: else:
self.save_object(self.object, **kwargs) self.save_object(self.object, **kwargs)
if self._deleted: if self.allow_delete and self._deleted:
[self.delete_object(item) for item in self._deleted] [self.delete_object(item) for item in self._deleted]
return self.object return self.object
...@@ -736,3 +743,13 @@ class HyperlinkedModelSerializer(ModelSerializer): ...@@ -736,3 +743,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
'many': to_many 'many': to_many
} }
return HyperlinkedRelatedField(**kwargs) return HyperlinkedRelatedField(**kwargs)
def get_identity(self, data):
"""
This hook is required for bulk update.
We need to override the default, to use the url as the identity.
"""
try:
return data.get('url', None)
except AttributeError:
return None
...@@ -201,7 +201,29 @@ class BulkUpdateSerializerTests(TestCase): ...@@ -201,7 +201,29 @@ class BulkUpdateSerializerTests(TestCase):
'author': 'Haruki Murakami' 'author': 'Haruki Murakami'
} }
] ]
serializer = self.BookSerializer(self.books(), data=data, many=True) serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.data, data)
serializer.save()
new_data = self.BookSerializer(self.books(), many=True).data
self.assertEqual(data, new_data)
def test_bulk_update_and_create(self):
"""
Bulk update serialization may also include created items.
"""
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 3,
'title': 'Kafka on the shore',
'author': 'Haruki Murakami'
}
]
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.data, data) self.assertEqual(serializer.data, data)
serializer.save() serializer.save()
...@@ -227,6 +249,6 @@ class BulkUpdateSerializerTests(TestCase): ...@@ -227,6 +249,6 @@ class BulkUpdateSerializerTests(TestCase):
{}, {},
{'id': ['Enter a whole number.']} {'id': ['Enter a whole number.']}
] ]
serializer = self.BookSerializer(self.books(), data=data, many=True) serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.is_valid(), False)
self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment