Basic bulk create and bulk update

This commit is contained in:
Tom Christie 2013-03-19 14:26:48 +00:00
parent 09e4ee7ae3
commit b2dc664485
4 changed files with 223 additions and 8 deletions

View File

@ -37,9 +37,6 @@ Declaring a serializer looks very similar to declaring a form:
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance is not None:
instance.title = attrs.get('title', instance.title)
@ -48,7 +45,9 @@ Declaring a serializer looks very similar to declaring a form:
return instance
return Comment(**attrs)
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. The `restore_object` method is optional, and is only required if we want our serializer to support deserialization.
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
The `restore_object` method is optional, and is only required if we want our serializer to support deserialization into fully fledged object instances. If we don't define this method, then deserializing data will simply return a dictionary of items.
## Serializing objects
@ -88,18 +87,22 @@ By default, serializers must be passed values for all required fields or they wi
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
## Serializing querysets
## Serializing multiple objects
To serialize a queryset instead of an object instance, you should pass the `many=True` flag when instantiating the serializer.
To serialize a queryset or list of objects instead of a single object instance, you should pass the `many=True` flag when instantiating the serializer.
queryset = Comment.objects.all()
serializer = CommentSerializer(queryset, many=True)
serializer.data
# [{'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}, {'email': u'jamie@example.com', 'content': u'baz', 'created': datetime.datetime(2013, 1, 12, 16, 12, 45, 104445)}]
# [
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)},
# {'email': u'jamie@example.com', 'content': u'baz', 'created': datetime.datetime(2013, 1, 12, 16, 12, 45, 104445)}
# ]
## Validation
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` property will contain a dictionary representing the resulting error messages.
Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors.
When deserializing a list of items, errors will be returned as a list of dictionaries representing each of the deserialized items.

View File

@ -128,6 +128,7 @@ class BaseSerializer(Field):
self._data = None
self._files = None
self._errors = None
self._deleted = None
#####
# Methods to determine which fields to use when (de)serializing objects.
@ -331,6 +332,13 @@ class BaseSerializer(Field):
return [self.to_native(item) for item in obj]
return self.to_native(obj)
def get_identity(self, data):
"""
This hook is required for bulk update.
It is used to determine the canonical identity of a given object.
"""
return data.get('id')
@property
def errors(self):
"""
@ -352,9 +360,32 @@ class BaseSerializer(Field):
if many:
ret = []
errors = []
update = self.object is not None
if update:
# If this is a bulk update we need to map all the objects
# to a canonical identity so we can determine which
# individual object is being updated for each item in the
# incoming data
objects = self.object
identities = [self.get_identity(self.to_native(obj)) for obj in objects]
identity_to_objects = dict(zip(identities, objects))
for item in data:
if update:
# Determine which object we're updating
try:
identity = self.get_identity(item)
except:
self.object = None
else:
self.object = identity_to_objects.pop(identity, None)
ret.append(self.from_native(item, None))
errors.append(self._errors)
if update:
self._deleted = identity_to_objects.values()
self._errors = any(errors) and errors or []
else:
ret = self.from_native(data, files)
@ -394,6 +425,9 @@ class BaseSerializer(Field):
def save_object(self, obj, **kwargs):
obj.save(**kwargs)
def delete_object(self, obj):
obj.delete()
def save(self, **kwargs):
"""
Save the deserialized object and return it.
@ -402,6 +436,10 @@ class BaseSerializer(Field):
[self.save_object(item, **kwargs) for item in self.object]
else:
self.save_object(self.object, **kwargs)
if self._deleted:
[self.delete_object(item) for item in self._deleted]
return self.object

View File

@ -266,7 +266,7 @@ class ValidationTests(TestCase):
Data of the wrong type is not valid.
"""
data = ['i am', 'a', 'list']
serializer = CommentSerializer(self.comment, data=data, many=True)
serializer = CommentSerializer([self.comment], data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
self.assertTrue(isinstance(serializer.errors, list))

View File

@ -0,0 +1,174 @@
"""
Tests to cover bulk create and update using serializers.
"""
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import serializers
class BulkCreateSerializerTests(TestCase):
def setUp(self):
class BookSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=100)
author = serializers.CharField(max_length=100)
self.BookSerializer = BookSerializer
def test_bulk_create_success(self):
"""
Correct bulk update serialization should return the input data.
"""
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 1,
'title': 'If this is a man',
'author': 'Primo Levi'
}, {
'id': 2,
'title': 'The wind-up bird chronicle',
'author': 'Haruki Murakami'
}
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.object, data)
def test_bulk_create_errors(self):
"""
Correct bulk update serialization should return the input data.
"""
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 1,
'title': 'If this is a man',
'author': 'Primo Levi'
}, {
'id': 'foo',
'title': 'The wind-up bird chronicle',
'author': 'Haruki Murakami'
}
]
expected_errors = [
{},
{},
{'id': ['Enter a whole number.']}
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
self.assertEqual(serializer.errors, expected_errors)
class BulkUpdateSerializerTests(TestCase):
def setUp(self):
class Book(object):
object_map = {}
def __init__(self, id, title, author):
self.id = id
self.title = title
self.author = author
def save(self):
Book.object_map[self.id] = self
def delete(self):
del Book.object_map[self.id]
class BookSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=100)
author = serializers.CharField(max_length=100)
def restore_object(self, attrs, instance=None):
if instance:
instance.id = attrs['id']
instance.title = attrs['title']
instance.author = attrs['author']
return instance
return Book(**attrs)
self.Book = Book
self.BookSerializer = BookSerializer
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 1,
'title': 'If this is a man',
'author': 'Primo Levi'
}, {
'id': 2,
'title': 'The wind-up bird chronicle',
'author': 'Haruki Murakami'
}
]
for item in data:
book = Book(item['id'], item['title'], item['author'])
book.save()
def books(self):
return self.Book.object_map.values()
def test_bulk_update_success(self):
"""
Correct bulk update serialization should return the input data.
"""
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 2,
'title': 'Kafka on the shore',
'author': 'Haruki Murakami'
}
]
serializer = self.BookSerializer(self.books(), data=data, many=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_error(self):
"""
Correct bulk update serialization should return the input data.
"""
data = [
{
'id': 0,
'title': 'The electric kool-aid acid test',
'author': 'Tom Wolfe'
}, {
'id': 'foo',
'title': 'Kafka on the shore',
'author': 'Haruki Murakami'
}
]
expected_errors = [
{},
{'id': ['Enter a whole number.']}
]
serializer = self.BookSerializer(self.books(), data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
self.assertEqual(serializer.errors, expected_errors)