From 2a1485e00943b8280245d19e1e1f8514b1ef18ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 21:32:43 +0000 Subject: [PATCH] Final bits of docs for ModelSerializer fields API --- docs/api-guide/serializers.md | 57 +++++++++--- docs_theme/css/default.css | 4 + rest_framework/serializers.py | 140 ++++++++++++++++------------- rest_framework/utils/model_meta.py | 10 +-- tests/test_model_serializer.py | 2 +- 5 files changed, 132 insertions(+), 81 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4d3dfa31b..dcbbd5f21 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -457,7 +457,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the name = CharField(allow_blank=True, max_length=100, required=False) owner = PrimaryKeyRelatedField(queryset=User.objects.all()) -## Specifying which fields should be included +## Specifying which fields to include If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. @@ -499,7 +499,7 @@ You can add extra fields to a `ModelSerializer` or override the default fields b Extra fields can correspond to any property or callable on the model. -## Specifying which fields should be read-only +## Specifying read only fields You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the shortcut Meta option, `read_only_fields`. @@ -528,7 +528,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details --- -## Specifying additional keyword arguments for fields. +## Additional keyword arguments There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer. @@ -567,31 +567,62 @@ The inner `Meta` class on serializers is not inherited from parent classes by de Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly. -## Advanced `ModelSerializer` usage +## Customizing field mappings The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer. -#### `.serializer_field_mapping` +Normally if a `ModelSerializer` does not generate the fields you need by default the you should either add them to the class explicitly, or simply use a regular `Serializer` class instead. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model. + +### `.serializer_field_mapping` A mapping of Django model classes to REST framework serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class. -#### `.serializer_relational_field` +### `.serializer_relational_field` This property should be the serializer field class, that is used for relational fields by default. For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `HyperlinkedModelSerializer` this defaults to `HyperlinkedRelatedField`. -#### The build field methods +### The field_class and field_kwargs API -#### `build_standard_field(**kwargs)` +The following methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of `(field_class, field_kwargs)`. -#### `build_relational_field(**kwargs)` +### `.build_standard_field(self, field_name, model_field)` -#### `build_nested_field(**kwargs)` +Called to generate a serializer field that maps to a standard model field. -#### `build_property_field(**kwargs)` +The default implementation returns a serializer class based on the `serializer_field_mapping` attribute. -#### `build_url_field(**kwargs)` +### `.build_relational_field(self, field_name, relation_info)` -#### `build_unknown_field(**kwargs)` +Called to generate a serializer field that maps to a relational model field. + +The default implementation returns a serializer class based on the `serializer_relational_field` attribute. + +The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. + +### `.build_nested_field(self, field_name, relation_info, nested_depth)` + +Called to generate a serializer field that maps to a relational model field, when the `depth` option has been set. + +The default implementation dynamically creates a nested serializer class based on either `ModelSerializer` or `HyperlinkedModelSerializer`. + +The `nested_depth` will be the value of the `depth` option, minus one. + +The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. + +### `.build_property_field(self, field_name, model_class)` + +Called to generate a serializer field that maps to a property or zero-argument method on the model class. + +The default implementation returns a `ReadOnlyField` class. + +### `.build_url_field(self, field_name, model_class)` + +Called to generate a serializer field for the serializer's own `url` field. The default implementation returns a `HyperlinkedIdentityField` class. + +### `.build_unknown_field(self, field_name, model_class)` + +Called when the field name did not map to any model field or model property. +The default implementation raises an error, although subclasses may customize this behavior. --- diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 8c9cd5363..48d00366b 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -239,6 +239,10 @@ body a:hover{ } } +h1 code, h2 code, h3 code, h4 code, h5 code { + color: #333; +} + /* sticky footer and footer */ html, body { height: 100%; diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8adbafe45..623ed5865 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -802,10 +802,25 @@ class ModelSerializer(Serializer): Return the dict of field names -> field instances that should be used for `self.fields` when instantiating the serializer. """ + assert hasattr(self, 'Meta'), ( + 'Class {serializer_class} missing "Meta" attribute'.format( + serializer_class=self.__class__.__name__ + ) + ) + assert hasattr(self.Meta, 'model'), ( + 'Class {serializer_class} missing "Meta.model" attribute'.format( + serializer_class=self.__class__.__name__ + ) + ) + declared_fields = copy.deepcopy(self._declared_fields) model = getattr(self.Meta, 'model') depth = getattr(self.Meta, 'depth', 0) + if depth is not None: + assert depth >= 0, "'depth' may not be negative." + assert depth <= 10, "'depth' may not be greater than 10." + # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) field_names = self.get_field_names(declared_fields, info) @@ -817,27 +832,32 @@ class ModelSerializer(Serializer): field_names, declared_fields, extra_kwargs ) - # Now determine the fields that should be included on the serializer. - ret = OrderedDict() + # Determine the fields that should be included on the serializer. + fields = OrderedDict() + for field_name in field_names: + # If the field is explicitly declared on the class then use that. if field_name in declared_fields: - # Field is explicitly declared on the class, use that. - ret[field_name] = declared_fields[field_name] + fields[field_name] = declared_fields[field_name] continue # Determine the serializer field class and keyword arguments. - field_cls, kwargs = self.build_field(field_name, info, model, depth) + field_class, field_kwargs = self.build_field( + field_name, info, model, depth + ) - # Populate any kwargs defined in `Meta.extra_kwargs` - kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name) + # Include any kwargs defined in `Meta.extra_kwargs` + field_kwargs = self.build_field_kwargs( + field_kwargs, extra_kwargs, field_name + ) # Create the serializer field. - ret[field_name] = field_cls(**kwargs) + fields[field_name] = field_class(**field_kwargs) # Add in any hidden fields. - ret.update(hidden_fields) + fields.update(hidden_fields) - return ret + return fields # Methods for determining the set of field names to include... @@ -916,108 +936,105 @@ class ModelSerializer(Serializer): # Methods for constructing serializer fields... - def build_field(self, field_name, info, model, nested_depth): + def build_field(self, field_name, info, model_class, nested_depth): """ Return a two tuple of (cls, kwargs) to build a serializer field with. """ if field_name in info.fields_and_pk: - return self.build_standard_field(field_name, info, model) + model_field = info.fields_and_pk[field_name] + return self.build_standard_field(field_name, model_field) elif field_name in info.relations: + relation_info = info.relations[field_name] if not nested_depth: - return self.build_relational_field(field_name, info, model) + return self.build_relational_field(field_name, relation_info) else: - return self.build_nested_field(field_name, info, model, nested_depth) + return self.build_nested_field(field_name, relation_info, nested_depth) - elif hasattr(model, field_name): - return self.build_property_field(field_name, info, model) + elif hasattr(model_class, field_name): + return self.build_property_field(field_name, model_class) elif field_name == api_settings.URL_FIELD_NAME: - return self.build_url_field(field_name, info, model) + return self.build_url_field(field_name, model_class) - return self.build_unknown_field(field_name, info, model) + return self.build_unknown_field(field_name, model_class) - def build_standard_field(self, field_name, info, model): + def build_standard_field(self, field_name, model_field): """ Create regular model fields. """ field_mapping = ClassLookupDict(self.serializer_field_mapping) - model_field = info.fields_and_pk[field_name] - field_cls = field_mapping[model_field] - kwargs = get_field_kwargs(field_name, model_field) + field_class = field_mapping[model_field] + field_kwargs = get_field_kwargs(field_name, model_field) - if 'choices' in kwargs: + if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. - field_cls = ChoiceField - if not issubclass(field_cls, ModelField): + field_class = ChoiceField + if not issubclass(field_class, ModelField): # `model_field` is only valid for the fallback case of # `ModelField`, which is used when no other typed field # matched to the model field. - kwargs.pop('model_field', None) - if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField): + field_kwargs.pop('model_field', None) + if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField): # `allow_blank` is only valid for textual fields. - kwargs.pop('allow_blank', None) + field_kwargs.pop('allow_blank', None) - return field_cls, kwargs + return field_class, field_kwargs - def build_relational_field(self, field_name, info, model): + def build_relational_field(self, field_name, relation_info): """ Create fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - - field_cls = self.serializer_related_class - kwargs = get_relation_kwargs(field_name, relation_info) + field_class = self.serializer_related_class + field_kwargs = get_relation_kwargs(field_name, relation_info) # `view_name` is only valid for hyperlinked relationships. - if not issubclass(field_cls, HyperlinkedRelatedField): - kwargs.pop('view_name', None) + if not issubclass(field_class, HyperlinkedRelatedField): + field_kwargs.pop('view_name', None) - return field_cls, kwargs + return field_class, field_kwargs - def build_nested_field(self, field_name, info, model, nested_depth): + def build_nested_field(self, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - class NestedSerializer(ModelSerializer): class Meta: - model = relation_info.related - depth = nested_depth - 1 + model = relation_info.related_model + depth = nested_depth - field_cls = NestedSerializer - kwargs = get_nested_relation_kwargs(relation_info) + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) - return field_cls, kwargs + return field_class, field_kwargs - def build_property_field(self, field_name, info, model): + def build_property_field(self, field_name, model_class): """ Create a read only field for model methods and properties. """ - field_cls = ReadOnlyField - kwargs = {} + field_class = ReadOnlyField + field_kwargs = {} - return field_cls, kwargs + return field_class, field_kwargs - def build_url_field(self, field_name, info, model): + def build_url_field(self, field_name, model_class): """ Create a field representing the object's own URL. """ - field_cls = HyperlinkedIdentityField - kwargs = get_url_kwargs(model) + field_class = HyperlinkedIdentityField + field_kwargs = get_url_kwargs(model_class) - return field_cls, kwargs + return field_class, field_kwargs - def build_unknown_field(self, field_name, info, model): + def build_unknown_field(self, field_name, model_class): """ Raise an error on any unknown fields. """ raise ImproperlyConfigured( 'Field name `%s` is not valid for model `%s`.' % - (field_name, model.__class__.__name__) + (field_name, model_class.__name__) ) def build_field_kwargs(self, kwargs, extra_kwargs, field_name): @@ -1318,17 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer): list(model_info.forward_relations.keys()) ) - def build_nested_field(self, field_name, info, model, nested_depth): + def build_nested_field(self, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - class NestedSerializer(HyperlinkedModelSerializer): class Meta: - model = relation_info.related + model = relation_info.related_model depth = nested_depth - 1 - field_cls = NestedSerializer - kwargs = get_nested_relation_kwargs(relation_info) - return field_cls, kwargs + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) + + return field_class, field_kwargs diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index c98725c66..dfc387ca5 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -24,7 +24,7 @@ FieldInfo = namedtuple('FieldResult', [ RelationInfo = namedtuple('RelationInfo', [ 'model_field', - 'related', + 'related_model', 'to_many', 'has_through_model' ]) @@ -77,7 +77,7 @@ def get_field_info(model): for field in [field for field in opts.fields if field.serialize and field.rel]: forward_relations[field.name] = RelationInfo( model_field=field, - related=_resolve_model(field.rel.to), + related_model=_resolve_model(field.rel.to), to_many=False, has_through_model=False ) @@ -86,7 +86,7 @@ def get_field_info(model): for field in [field for field in opts.many_to_many if field.serialize]: forward_relations[field.name] = RelationInfo( model_field=field, - related=_resolve_model(field.rel.to), + related_model=_resolve_model(field.rel.to), to_many=True, has_through_model=( not field.rel.through._meta.auto_created @@ -99,7 +99,7 @@ def get_field_info(model): accessor_name = relation.get_accessor_name() reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related_model=relation.model, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -109,7 +109,7 @@ def get_field_info(model): accessor_name = relation.get_accessor_name() reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related_model=relation.model, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 5c56c8dbb..603faf477 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -206,7 +206,7 @@ class TestRegularFieldMappings(TestCase): with self.assertRaises(ImproperlyConfigured) as excinfo: TestSerializer().fields - expected = 'Field name `invalid` is not valid for model `ModelBase`.' + expected = 'Field name `invalid` is not valid for model `RegularFieldsModel`.' assert str(excinfo.exception) == expected def test_missing_field(self):