Skip to content

Commit da6ef3d

Browse files
committed
Allow missing fields option for inherited serializers. Closes #2388.
1 parent fdeef89 commit da6ef3d

File tree

4 files changed

+64
-25
lines changed

4 files changed

+64
-25
lines changed

rest_framework/compat.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def unicode_repr(instance):
2020
# Get the repr of an instance, but ensure it is a unicode string
2121
# on both python 3 (already the case) and 2 (not the case).
2222
if six.PY2:
23-
repr(instance).decode('utf-8')
23+
return repr(instance).decode('utf-8')
2424
return repr(instance)
2525

2626

rest_framework/serializers.py

+19-13
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def _get_declared_fields(cls, bases, attrs):
253253
# If this class is subclassing another Serializer, add that Serializer's
254254
# fields. Note that we loop over the bases in *reverse*. This is necessary
255255
# in order to maintain the correct order of fields.
256-
for base in bases[::-1]:
256+
for base in reversed(bases):
257257
if hasattr(base, '_declared_fields'):
258258
fields = list(base._declared_fields.items()) + fields
259259

@@ -880,8 +880,8 @@ def get_fields(self):
880880
# Retrieve metadata about fields & relationships on the model class.
881881
info = model_meta.get_field_info(model)
882882

883-
# Use the default set of field names if none is supplied explicitly.
884883
if fields is None:
884+
# Use the default set of field names if none is supplied explicitly.
885885
fields = self._get_default_field_names(declared_fields, info)
886886
exclude = getattr(self.Meta, 'exclude', None)
887887
if exclude is not None:
@@ -891,6 +891,23 @@ def get_fields(self):
891891
field_name
892892
)
893893
fields.remove(field_name)
894+
else:
895+
# Check that any fields declared on the class are
896+
# also explicitly included in `Meta.fields`.
897+
898+
# Note that we ignore any fields that were declared on a parent
899+
# class, in order to support only including a subset of fields
900+
# when subclassing serializers.
901+
declared_field_names = set(declared_fields.keys())
902+
for cls in self.__class__.__bases__:
903+
declared_field_names -= set(getattr(cls, '_declared_fields', []))
904+
905+
missing_fields = declared_field_names - set(fields)
906+
assert not missing_fields, (
907+
'Field `%s` has been declared on serializer `%s`, but '
908+
'is missing from `Meta.fields`.' %
909+
(list(missing_fields)[0], self.__class__.__name__)
910+
)
894911

895912
# Determine the set of model fields, and the fields that they map to.
896913
# We actually only need this to deal with the slightly awkward case
@@ -1024,17 +1041,6 @@ def get_fields(self):
10241041
(field_name, model.__class__.__name__)
10251042
)
10261043

1027-
# Check that any fields declared on the class are
1028-
# also explicitly included in `Meta.fields`.
1029-
missing_fields = set(declared_fields.keys()) - set(fields)
1030-
if missing_fields:
1031-
missing_field = list(missing_fields)[0]
1032-
raise ImproperlyConfigured(
1033-
'Field `%s` has been declared on serializer `%s`, but '
1034-
'is missing from `Meta.fields`.' %
1035-
(missing_field, self.__class__.__name__)
1036-
)
1037-
10381044
# Populate any kwargs defined in `Meta.extra_kwargs`
10391045
extras = extra_kwargs.get(field_name, {})
10401046
if extras.get('read_only', False):

rest_framework/utils/serializer_helpers.py

+3
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ def __iter__(self):
105105

106106
def __len__(self):
107107
return len(self.fields)
108+
109+
def __repr__(self):
110+
return dict.__repr__(self.fields)

tests/test_model_serializer.py

+41-11
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
These tests deal with ensuring that we correctly map the model fields onto
66
an appropriate set of serializer fields for each case.
77
"""
8+
from __future__ import unicode_literals
89
from django.core.exceptions import ImproperlyConfigured
910
from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
1011
from django.db import models
1112
from django.test import TestCase
13+
from django.utils import six
1214
from rest_framework import serializers
15+
from rest_framework.compat import unicode_repr
1316

1417

1518
def dedent(blocktext):
@@ -124,7 +127,7 @@ class Meta:
124127
url_field = URLField(max_length=100)
125128
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
126129
""")
127-
self.assertEqual(repr(TestSerializer()), expected)
130+
self.assertEqual(unicode_repr(TestSerializer()), expected)
128131

129132
def test_field_options(self):
130133
class TestSerializer(serializers.ModelSerializer):
@@ -142,7 +145,14 @@ class Meta:
142145
descriptive_field = IntegerField(help_text='Some help text', label='A label')
143146
choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
144147
""")
145-
self.assertEqual(repr(TestSerializer()), expected)
148+
if six.PY2:
149+
# This particular case is too awkward to resolve fully across
150+
# both py2 and py3.
151+
expected = expected.replace(
152+
"('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')",
153+
"(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')"
154+
)
155+
self.assertEqual(unicode_repr(TestSerializer()), expected)
146156

147157
def test_method_field(self):
148158
"""
@@ -221,14 +231,34 @@ class Meta:
221231
model = RegularFieldsModel
222232
fields = ('auto_field',)
223233

224-
with self.assertRaises(ImproperlyConfigured) as excinfo:
234+
with self.assertRaises(AssertionError) as excinfo:
225235
TestSerializer().fields
226236
expected = (
227237
'Field `missing` has been declared on serializer '
228238
'`TestSerializer`, but is missing from `Meta.fields`.'
229239
)
230240
assert str(excinfo.exception) == expected
231241

242+
def test_missing_superclass_field(self):
243+
"""
244+
Fields that have been declared on a parent of the serializer class may
245+
be excluded from the `Meta.fields` option.
246+
"""
247+
class TestSerializer(serializers.ModelSerializer):
248+
missing = serializers.ReadOnlyField()
249+
250+
class Meta:
251+
model = RegularFieldsModel
252+
253+
class ChildSerializer(TestSerializer):
254+
missing = serializers.ReadOnlyField()
255+
256+
class Meta:
257+
model = RegularFieldsModel
258+
fields = ('auto_field',)
259+
260+
ChildSerializer().fields
261+
232262

233263
# Tests for relational field mappings.
234264
# ------------------------------------
@@ -276,7 +306,7 @@ class Meta:
276306
many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
277307
through = PrimaryKeyRelatedField(many=True, read_only=True)
278308
""")
279-
self.assertEqual(repr(TestSerializer()), expected)
309+
self.assertEqual(unicode_repr(TestSerializer()), expected)
280310

281311
def test_nested_relations(self):
282312
class TestSerializer(serializers.ModelSerializer):
@@ -300,7 +330,7 @@ class Meta:
300330
id = IntegerField(label='ID', read_only=True)
301331
name = CharField(max_length=100)
302332
""")
303-
self.assertEqual(repr(TestSerializer()), expected)
333+
self.assertEqual(unicode_repr(TestSerializer()), expected)
304334

305335
def test_hyperlinked_relations(self):
306336
class TestSerializer(serializers.HyperlinkedModelSerializer):
@@ -315,7 +345,7 @@ class Meta:
315345
many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
316346
through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
317347
""")
318-
self.assertEqual(repr(TestSerializer()), expected)
348+
self.assertEqual(unicode_repr(TestSerializer()), expected)
319349

320350
def test_nested_hyperlinked_relations(self):
321351
class TestSerializer(serializers.HyperlinkedModelSerializer):
@@ -339,7 +369,7 @@ class Meta:
339369
url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
340370
name = CharField(max_length=100)
341371
""")
342-
self.assertEqual(repr(TestSerializer()), expected)
372+
self.assertEqual(unicode_repr(TestSerializer()), expected)
343373

344374
def test_pk_reverse_foreign_key(self):
345375
class TestSerializer(serializers.ModelSerializer):
@@ -353,7 +383,7 @@ class Meta:
353383
name = CharField(max_length=100)
354384
reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
355385
""")
356-
self.assertEqual(repr(TestSerializer()), expected)
386+
self.assertEqual(unicode_repr(TestSerializer()), expected)
357387

358388
def test_pk_reverse_one_to_one(self):
359389
class TestSerializer(serializers.ModelSerializer):
@@ -367,7 +397,7 @@ class Meta:
367397
name = CharField(max_length=100)
368398
reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
369399
""")
370-
self.assertEqual(repr(TestSerializer()), expected)
400+
self.assertEqual(unicode_repr(TestSerializer()), expected)
371401

372402
def test_pk_reverse_many_to_many(self):
373403
class TestSerializer(serializers.ModelSerializer):
@@ -381,7 +411,7 @@ class Meta:
381411
name = CharField(max_length=100)
382412
reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
383413
""")
384-
self.assertEqual(repr(TestSerializer()), expected)
414+
self.assertEqual(unicode_repr(TestSerializer()), expected)
385415

386416
def test_pk_reverse_through(self):
387417
class TestSerializer(serializers.ModelSerializer):
@@ -395,7 +425,7 @@ class Meta:
395425
name = CharField(max_length=100)
396426
reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
397427
""")
398-
self.assertEqual(repr(TestSerializer()), expected)
428+
self.assertEqual(unicode_repr(TestSerializer()), expected)
399429

400430

401431
class TestIntegration(TestCase):

0 commit comments

Comments
 (0)