8000 one-many writable nested modelserializer support · dhepper/django-rest-framework@73efa96 · GitHub
[go: up one dir, main page]

Skip to content
< 8000 /div>

Commit 73efa96

Browse files
committed
one-many writable nested modelserializer support
1 parent d97e72c commit 73efa96

File tree

3 files changed

+139
-21
lines changed

3 files changed

+139
-21
lines changed

rest_framework/serializers.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,14 @@ class Meta(object):
130130

131131
def __init__(self, instance=None, data=None, files=None,
132132
context=None, partial=False, many=None,
133-
allow_delete=False, **kwargs):
133+
allow_add_remove=False, **kwargs):
134134
super(BaseSerializer, self).__init__(**kwargs)
135135
self.opts = self._options_class(self.Meta)
136136
self.parent = None
137137
self.root = None
138138
self.partial = partial
139139
self.many = many
140-
self.allow_delete = allow_delete
140+
self.allow_add_remove = allow_add_remove
141141

142142
self.context = context or {}
143143

@@ -154,8 +154,8 @@ def __init__(self, instance=None, data=None, files=None,
154154
if many and instance is not None and not hasattr(instance, '__iter__'):
155155
raise ValueError('instance should be a queryset or other iterable with many=True')
156156

157-
if allow_delete and not many:
158-
raise ValueError('allow_delete should only be used for bulk updates, but you have not set many=True')
157+
if allow_add_remove and not many:
158+
raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
159159

160160
#####
161161
# Methods to determine which fields to use when (de)serializing objects.
@@ -288,8 +288,15 @@ def restore_object(self, attrs, instance=None):
288288
You should override this method to control how deserialized objects
289289
are instantiated.
290290
"""
291+
removed_relations = []
292+
293+
# Deleted related objects
294+
if self._deleted:
295+
removed_relations = list(self._deleted)
296+
291297
if instance is not None:
292298
instance.update(attrs)
299+
instance._removed_relations = removed_relations
293300
return instance
294301
return attrs
295302

@@ -377,6 +384,7 @@ def field_from_native(self, data, files, field_name, into):
377384

378385
# Set the serializer object if it exists
379386
obj = getattr(self.parent.object, field_name) if self.parent.object else None
387+
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
380388

381389
if value in (None, ''):
382390
into[(self.source or field_name)] = None
@@ -386,7 +394,8 @@ def field_from_native(self, data, files, field_name, into):
386394
'data': value,
387395
'context': self.context,
388396
'partial': self.partial,
389-
'many': self.many
397+
'many': self.many,
398+
'allow_add_remove': self.allow_add_remove
390399
}
391400
serializer = self.__class__(**kwargs)
392401

@@ -496,6 +505,9 @@ def data(self):
496505
def save_object(self, obj, **kwargs):
497506
obj.save(**kwargs)
498507

508+
if self.allow_add_remove and hasattr(obj, '_removed_relations'):
509+
[self.delete_object(item) for item in obj._removed_relations]
510+
499511
def delete_object(self, obj):
500512
obj.delete()
501513

@@ -508,7 +520,7 @@ def save(self, **kwargs):
508520
else:
509521
self.save_object(self.object, **kwargs)
510522

511-
if self.allow_delete and self._deleted:
523+
if self.allow_add_remove and self._deleted:
512524
[self.delete_object(item) for item in self._deleted]
513525

514526
return self.object
@@ -699,6 +711,7 @@ def restore_object(self, attrs, instance=None):
699711
m2m_data = {}
700712
related_data = {}
701713
nested_forward_relations = {}
714+
removed_relations = []
702715
meta = self.opts.model._meta
703716

704717
# Reverse fk or one-to-one relations
@@ -724,6 +737,10 @@ def restore_object(self, attrs, instance=None):
724737
if isinstance(self.fields.get(field_name, None), Serializer):
725738
nested_forward_relations[field_name] = attrs[field_name]
726739

740+
# Deleted related objects
741+
if self._deleted:
742+
removed_relations = list(self._deleted)
743+
727744
# Update an existing instance...
728745
if instance is not None:
729746
for key, val in attrs.items():
@@ -740,6 +757,7 @@ def restore_object(self, attrs, instance=None):
740757
instance._related_data = related_data
741758
instance._m2m_data = m2m_data
742759
instance._nested_forward_relations = nested_forward_relations
760+
instance._removed_relations = removed_relations
743761

744762
return instance
745763

@@ -764,6 +782,9 @@ def save_object(self, obj, **kwargs):
764782

765783
obj.save(**kwargs)
766784

785+
if self.allow_add_remove and hasattr(obj, '_removed_relations'):
786+
[self.delete_object(item) for item in obj._removed_relations]
787+
767788
if getattr(obj, '_m2m_data', None):
768789
for accessor_name, object_list in obj._m2m_data.items():
769790
setattr(obj, accessor_name, object_list)
@@ -773,18 +794,17 @@ def save_object(self B41A , obj, **kwargs):
773794
for accessor_name, related in obj._related_data.items():
774795
field = self.fields.get(accessor_name, None)
775796
if isinstance(field, Serializer):
776-
# TODO: Following will be needed for reverse FK
777-
# if field.many:
778-
# # Nested reverse fk relationship
779-
# for related_item in related:
780-
# fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
781-
# setattr(related_item, fk_field, obj)
782-
# self.save_object(related_item)
783-
# else:
784-
# Nested reverse one-one relationship
785-
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
786-
setattr(related, fk_field, obj)
787-
self.save_object(related)
797+
if field.many:
798+
# Nested reverse fk relationship
799+
for related_item in related:
800+
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
801+
setattr(related_item, fk_field, obj)
802+
self.save_object(related_item)
803+
else:
804+
# Nested reverse one-one relationship
805+
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
806+
setattr(related, fk_field, obj)
807+
self.save_object(related)
788808
else:
789809
# Reverse FK or reverse one-one
790810
setattr(obj, accessor_name, related)

re F438 st_framework/tests/relations_nested.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ class OneToOneSource(models.Model):
1313
target = models.OneToOneField(OneToOneTarget, related_name='source')
1414

1515

16+
class OneToManyTarget(models.Model):
17+
name = models.CharField(max_length=100)
18+
19+
20+
class OneToManySource(models.Model):
21+
name = models.CharField(max_length=100)
22+
target = models.ForeignKey(OneToManyTarget, related_name='sources')
23+
24+
1625
class ReverseNestedOneToOneTests(TestCase):
1726
def setUp(self):
1827
class OneToOneSourceSerializer(serializers.ModelSerializer):
@@ -189,3 +198,92 @@ def test_one_to_one_update(self):
189198
# {'id': 3, 'name': 'target-3', 'source': None}
190199
# ]
191200
# self.assertEqual(serializer.data, expected)
201+
202+
203+
class ReverseNestedOneToManyTests(TestCase):
204+
def setUp(self):
205+
class OneToManySourceSerializer(serializers.ModelSerializer):
206+
class Meta:
207+
model = OneToManySource
208+
fields = ('id', 'name')
209+
210+
class OneToManyTargetSerializer(serializers.ModelSerializer):
211+
sources = OneToManySourceSerializer(many=True, allow_add_remove=True)
212+
213+
class Meta:
214+
model = OneToManyTarget
215+
fields = ('id', 'name', 'sources')
216+
217+
self.Serializer = OneToManyTargetSerializer
218+
219+
target = OneToManyTarget(name='target-1')
220+
target.save()
221+
for idx in range(1, 4):
222+
source = OneToManySource(name='source-%d' % idx, target=target)
223+
source.save()
224+
225+
def test_one_to_many_retrieve(self):
226+
queryset = OneToManyTarget.objects.all()
227+
serializer = self.Serializer(queryset)
228+
expected = [
229+
{'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
230+
{'id': 2, 'name': 'source-2'},
231+
{'id': 3, 'name': 'source-3'}]},
232+
]
233+
self.assertEqual(serializer.data, expected)
234+
235+
def test_one_to_many_create(self):
236+
data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
237+
{'id': 2, 'name': 'source-2'},
238+
{'id': 3, 'name': 'source-3'},
239+
{'id': 4, 'name': 'source-4'}]}
240+
instance = OneToManyTarget.objects.get(pk=1)
241+
serializer = self.Serializer(instance, data=data)
242+
self.assertTrue(serializer.is_valid())
243+
obj = serializer.save()
244+
self.assertEqual(serializer.data, data)
245+
self.assertEqual(obj.name, 'target-1')
246+
247+
# Ensure source 4 is added, and everything else is as
248+
# expected.
249+
queryset = OneToManyTarget.objects.all()
250+
serializer = self.Serializer(queryset)
251+
expected = [
252+
{'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
253+
{'id': 2, 'name': 'source-2'},
254+
{'id': 3, 'name': 'source-3'},
255+
{'id': 4, 'name': 'source-4'}]}
256+
]
257+
self.assertEqual(serializer.data, expected)
258+
259+
def test_one_to_many_create_with_invalid_data(self):
260+
data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
261+
{'id': 2, 'name': 'source-2'},
262+
{'id': 3, 'name': 'source-3'},
263+
{'id': 4}]}
264+
serializer = self.Serializer(data=data)
265+
self.assertFalse(serializer.is_valid())
266+
self.assertEqual(serializer.errors, {'sources': [{}, {}, {}, {'name': ['This field is required.']}]})
267+
268+
def test_one_to_many_update(self):
269+
data = {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
270+
{'id': 2, 'name': 'source-2'},
271+
{'id': 3, 'name': 'source-3'}]}
272+
instance = OneToManyTarget.objects.get(pk=1)
273+
serializer = self.Serializer(instance, data=data)
274+
self.assertTrue(serializer.is_valid())
275+
obj = serializer.save()
276+
self.assertEqual(serializer.data, data)
277+
self.assertEqual(obj.name, 'target-1-updated')
278+
279+
# Ensure (target 1, source 1) are updated,
280+
# and everything else is as expected.
281+
queryset = OneToManyTarget.objects.all()
282+
serializer = self.Serializer(queryset)
283+
expected = [
284+
{'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
285+
{'id': 2, 'name': 'source-2'},
286+
{'id': 3, 'name': 'source-3'}]}
287+
288+
]
289+
self.assertEqual(serializer.data, expected)

rest_framework/tests/serializer_bulk_update.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def test_bulk_update_success(self):
201201
'author': 'Haruki Murakami'
202202
}
203203
]
204-
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
204+
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
205205
self.assertEqual(serializer.is_valid(), True)
206206
self.assertEqual(serializer.data, data)
207207
serializer.save()
@@ -223,7 +223,7 @@ def test_bulk_update_and_create(self):
223223
'author': 'Haruki Murakami'
224224
}
225225
]
226-
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
226+
s 732D erializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
227227
self.assertEqual(serializer.is_valid(), True)
228228
self.assertEqual(serializer.data, data)
229229
serializer.save()
@@ -249,6 +249,6 @@ def test_bulk_update_error(self):
249249
{},
250250
{'id': ['Enter a whole number.']}
251251
]
252-
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True)
252+
serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
253253
self.assertEqual(serializer.is_valid(), False)
254254
self.assertEqual(serializer.errors, expected_errors)

0 commit comments

Comments
 (0)
0