Skip to content

Commit 5185cc9

Browse files
authored
Handle unset fields with 'many=True' (#7574)
* Handle unset fields with 'many=True' The docs note: When serializing fields with dotted notation, it may be necessary to provide a `default` value if any object is not present or is empty during attribute traversal. However, this doesn't work for fields with 'many=True'. When using these, the default is simply ignored. The solution is simple: do in 'ManyRelatedField' what we were already doing for 'Field', namely, catch possible 'AttributeError' and 'KeyError' exceptions and return the default if there is one set. Signed-off-by: Stephen Finucane <[email protected]> Closes: #7550 * Add test cases for #7550 Signed-off-by: Stephen Finucane <[email protected]>
1 parent 26830c3 commit 5185cc9

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

rest_framework/relations.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.utils.translation import gettext_lazy as _
1111

1212
from rest_framework.fields import (
13-
Field, empty, get_attribute, is_simple_callable, iter_options
13+
Field, SkipField, empty, get_attribute, is_simple_callable, iter_options
1414
)
1515
from rest_framework.reverse import reverse
1616
from rest_framework.settings import api_settings
@@ -535,7 +535,30 @@ def get_attribute(self, instance):
535535
if hasattr(instance, 'pk') and instance.pk is None:
536536
return []
537537

538-
relationship = get_attribute(instance, self.source_attrs)
538+
try:
539+
relationship = get_attribute(instance, self.source_attrs)
540+
except (KeyError, AttributeError) as exc:
541+
if self.default is not empty:
542+
return self.get_default()
543+
if self.allow_null:
544+
return None
545+
if not self.required:
546+
raise SkipField()
547+
msg = (
548+
'Got {exc_type} when attempting to get a value for field '
549+
'`{field}` on serializer `{serializer}`.\nThe serializer '
550+
'field might be named incorrectly and not match '
551+
'any attribute or key on the `{instance}` instance.\n'
552+
'Original exception text was: {exc}.'.format(
553+
exc_type=type(exc).__name__,
554+
field=self.field_name,
555+
serializer=self.parent.__class__.__name__,
556+
instance=instance.__class__.__name__,
557+
exc=exc
558+
)
559+
)
560+
raise type(exc)(msg)
561+
539562
return relationship.all() if hasattr(relationship, 'all') else relationship
540563

541564
def to_representation(self, iterable):

tests/test_model_serializer.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,73 @@ class Meta:
10251025
assert serializer.data == expected
10261026

10271027

1028+
class Issue7550FooModel(models.Model):
1029+
text = models.CharField(max_length=100)
1030+
bar = models.ForeignKey(
1031+
'Issue7550BarModel', null=True, blank=True, on_delete=models.SET_NULL,
1032+
related_name='foos', related_query_name='foo')
1033+
1034+
1035+
class Issue7550BarModel(models.Model):
1036+
pass
1037+
1038+
1039+
class Issue7550TestCase(TestCase):
1040+
1041+
def test_dotted_source(self):
1042+
1043+
class _FooSerializer(serializers.ModelSerializer):
1044+
class Meta:
1045+
model = Issue7550FooModel
1046+
fields = ('id', 'text')
1047+
1048+
class FooSerializer(serializers.ModelSerializer):
1049+
other_foos = _FooSerializer(source='bar.foos', many=True)
1050+
1051+
class Meta:
1052+
model = Issue7550BarModel
1053+
fields = ('id', 'other_foos')
1054+
1055+
bar = Issue7550BarModel.objects.create()
1056+
foo_a = Issue7550FooModel.objects.create(bar=bar, text='abc')
1057+
foo_b = Issue7550FooModel.objects.create(bar=bar, text='123')
1058+
1059+
assert FooSerializer(foo_a).data == {
1060+
'id': foo_a.id,
1061+
'other_foos': [
1062+
{
1063+
'id': foo_a.id,
1064+
'text': foo_a.text,
1065+
},
1066+
{
1067+
'id': foo_b.id,
1068+
'text': foo_b.text,
1069+
},
1070+
],
1071+
}
1072+
1073+
def test_dotted_source_with_default(self):
1074+
1075+
class _FooSerializer(serializers.ModelSerializer):
1076+
class Meta:
1077+
model = Issue7550FooModel
1078+
fields = ('id', 'text')
1079+
1080+
class FooSerializer(serializers.ModelSerializer):
1081+
other_foos = _FooSerializer(source='bar.foos', default=[], many=True)
1082+
1083+
class Meta:
1084+
model = Issue7550FooModel
1085+
fields = ('id', 'other_foos')
1086+
1087+
foo = Issue7550FooModel.objects.create(bar=None, text='abc')
1088+
1089+
assert FooSerializer(foo).data == {
1090+
'id': foo.id,
1091+
'other_foos': [],
1092+
}
1093+
1094+
10281095
class DecimalFieldModel(models.Model):
10291096
decimal_field = models.DecimalField(
10301097
max_digits=3,

0 commit comments

Comments
 (0)