Final bits of docs for ModelSerializer fields API

This commit is contained in:
Tom Christie 2014-12-19 21:32:43 +00:00
parent 48d15f6ff8
commit 2a1485e009
5 changed files with 132 additions and 81 deletions

View File

@ -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) name = CharField(allow_blank=True, max_length=100, required=False)
owner = PrimaryKeyRelatedField(queryset=User.objects.all()) 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`. 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. 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`. 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. 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. 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. 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. 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`. 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.
--- ---

View File

@ -239,6 +239,10 @@ body a:hover{
} }
} }
h1 code, h2 code, h3 code, h4 code, h5 code {
color: #333;
}
/* sticky footer and footer */ /* sticky footer and footer */
html, body { html, body {
height: 100%; height: 100%;

View File

@ -802,10 +802,25 @@ class ModelSerializer(Serializer):
Return the dict of field names -> field instances that should be Return the dict of field names -> field instances that should be
used for `self.fields` when instantiating the serializer. 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) declared_fields = copy.deepcopy(self._declared_fields)
model = getattr(self.Meta, 'model') model = getattr(self.Meta, 'model')
depth = getattr(self.Meta, 'depth', 0) 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. # Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model) info = model_meta.get_field_info(model)
field_names = self.get_field_names(declared_fields, info) field_names = self.get_field_names(declared_fields, info)
@ -817,27 +832,32 @@ class ModelSerializer(Serializer):
field_names, declared_fields, extra_kwargs field_names, declared_fields, extra_kwargs
) )
# Now determine the fields that should be included on the serializer. # Determine the fields that should be included on the serializer.
ret = OrderedDict() fields = OrderedDict()
for field_name in field_names: for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields: if field_name in declared_fields:
# Field is explicitly declared on the class, use that. fields[field_name] = declared_fields[field_name]
ret[field_name] = declared_fields[field_name]
continue continue
# Determine the serializer field class and keyword arguments. # 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` # Include any kwargs defined in `Meta.extra_kwargs`
kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name) field_kwargs = self.build_field_kwargs(
field_kwargs, extra_kwargs, field_name
)
# Create the serializer field. # Create the serializer field.
ret[field_name] = field_cls(**kwargs) fields[field_name] = field_class(**field_kwargs)
# Add in any hidden fields. # 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... # Methods for determining the set of field names to include...
@ -916,108 +936,105 @@ class ModelSerializer(Serializer):
# Methods for constructing serializer fields... # 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. Return a two tuple of (cls, kwargs) to build a serializer field with.
""" """
if field_name in info.fields_and_pk: 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: elif field_name in info.relations:
relation_info = info.relations[field_name]
if not nested_depth: if not nested_depth:
return self.build_relational_field(field_name, info, model) return self.build_relational_field(field_name, relation_info)
else: 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): elif hasattr(model_class, field_name):
return self.build_property_field(field_name, info, model) return self.build_property_field(field_name, model_class)
elif field_name == api_settings.URL_FIELD_NAME: 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. Create regular model fields.
""" """
field_mapping = ClassLookupDict(self.serializer_field_mapping) field_mapping = ClassLookupDict(self.serializer_field_mapping)
model_field = info.fields_and_pk[field_name]
field_cls = field_mapping[model_field] field_class = field_mapping[model_field]
kwargs = get_field_kwargs(field_name, 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` # Fields with choices get coerced into `ChoiceField`
# instead of using their regular typed field. # instead of using their regular typed field.
field_cls = ChoiceField field_class = ChoiceField
if not issubclass(field_cls, ModelField): if not issubclass(field_class, ModelField):
# `model_field` is only valid for the fallback case of # `model_field` is only valid for the fallback case of
# `ModelField`, which is used when no other typed field # `ModelField`, which is used when no other typed field
# matched to the model field. # matched to the model field.
kwargs.pop('model_field', None) field_kwargs.pop('model_field', None)
if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField): if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField):
# `allow_blank` is only valid for textual fields. # `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. Create fields for forward and reverse relationships.
""" """
relation_info = info.relations[field_name] field_class = self.serializer_related_class
field_kwargs = get_relation_kwargs(field_name, relation_info)
field_cls = self.serializer_related_class
kwargs = get_relation_kwargs(field_name, relation_info)
# `view_name` is only valid for hyperlinked relationships. # `view_name` is only valid for hyperlinked relationships.
if not issubclass(field_cls, HyperlinkedRelatedField): if not issubclass(field_class, HyperlinkedRelatedField):
kwargs.pop('view_name', None) 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. Create nested fields for forward and reverse relationships.
""" """
relation_info = info.relations[field_name]
class NestedSerializer(ModelSerializer): class NestedSerializer(ModelSerializer):
class Meta: class Meta:
model = relation_info.related model = relation_info.related_model
depth = nested_depth - 1 depth = nested_depth
field_cls = NestedSerializer field_class = NestedSerializer
kwargs = get_nested_relation_kwargs(relation_info) 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. Create a read only field for model methods and properties.
""" """
field_cls = ReadOnlyField field_class = ReadOnlyField
kwargs = {} 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. Create a field representing the object's own URL.
""" """
field_cls = HyperlinkedIdentityField field_class = HyperlinkedIdentityField
kwargs = get_url_kwargs(model) 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 an error on any unknown fields.
""" """
raise ImproperlyConfigured( raise ImproperlyConfigured(
'Field name `%s` is not valid for model `%s`.' % '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): def build_field_kwargs(self, kwargs, extra_kwargs, field_name):
@ -1318,17 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer):
list(model_info.forward_relations.keys()) 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. Create nested fields for forward and reverse relationships.
""" """
relation_info = info.relations[field_name]
class NestedSerializer(HyperlinkedModelSerializer): class NestedSerializer(HyperlinkedModelSerializer):
class Meta: class Meta:
model = relation_info.related model = relation_info.related_model
depth = nested_depth - 1 depth = nested_depth - 1
field_cls = NestedSerializer field_class = NestedSerializer
kwargs = get_nested_relation_kwargs(relation_info) field_kwargs = get_nested_relation_kwargs(relation_info)
return field_cls, kwargs
return field_class, field_kwargs

View File

@ -24,7 +24,7 @@ FieldInfo = namedtuple('FieldResult', [
RelationInfo = namedtuple('RelationInfo', [ RelationInfo = namedtuple('RelationInfo', [
'model_field', 'model_field',
'related', 'related_model',
'to_many', 'to_many',
'has_through_model' '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]: for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo( forward_relations[field.name] = RelationInfo(
model_field=field, model_field=field,
related=_resolve_model(field.rel.to), related_model=_resolve_model(field.rel.to),
to_many=False, to_many=False,
has_through_model=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]: for field in [field for field in opts.many_to_many if field.serialize]:
forward_relations[field.name] = RelationInfo( forward_relations[field.name] = RelationInfo(
model_field=field, model_field=field,
related=_resolve_model(field.rel.to), related_model=_resolve_model(field.rel.to),
to_many=True, to_many=True,
has_through_model=( has_through_model=(
not field.rel.through._meta.auto_created not field.rel.through._meta.auto_created
@ -99,7 +99,7 @@ def get_field_info(model):
accessor_name = relation.get_accessor_name() accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo( reverse_relations[accessor_name] = RelationInfo(
model_field=None, model_field=None,
related=relation.model, related_model=relation.model,
to_many=relation.field.rel.multiple, to_many=relation.field.rel.multiple,
has_through_model=False has_through_model=False
) )
@ -109,7 +109,7 @@ def get_field_info(model):
accessor_name = relation.get_accessor_name() accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo( reverse_relations[accessor_name] = RelationInfo(
model_field=None, model_field=None,
related=relation.model, related_model=relation.model,
to_many=True, to_many=True,
has_through_model=( has_through_model=(
(getattr(relation.field.rel, 'through', None) is not None) (getattr(relation.field.rel, 'through', None) is not None)

View File

@ -206,7 +206,7 @@ class TestRegularFieldMappings(TestCase):
with self.assertRaises(ImproperlyConfigured) as excinfo: with self.assertRaises(ImproperlyConfigured) as excinfo:
TestSerializer().fields 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 assert str(excinfo.exception) == expected
def test_missing_field(self): def test_missing_field(self):