Commit 2a1485e0 by Tom Christie

Final bits of docs for ModelSerializer fields API

parent 48d15f6f
......@@ -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
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_standard_field(self, field_name, model_field)`
Called to generate a serializer field that maps to a standard model field.
The default implementation returns a serializer class based on the `serializer_field_mapping` attribute.
### `.build_relational_field(self, field_name, relation_info)`
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)`
#### `build_standard_field(**kwargs)`
Called to generate a serializer field that maps to a property or zero-argument method on the model class.
#### `build_relational_field(**kwargs)`
The default implementation returns a `ReadOnlyField` class.
#### `build_nested_field(**kwargs)`
### `.build_url_field(self, field_name, model_class)`
#### `build_property_field(**kwargs)`
Called to generate a serializer field for the serializer's own `url` field. The default implementation returns a `HyperlinkedIdentityField` class.
#### `build_url_field(**kwargs)`
### `.build_unknown_field(self, field_name, model_class)`
#### `build_unknown_field(**kwargs)`
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.
---
......
......@@ -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%;
......
......@@ -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
......@@ -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)
......
......@@ -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):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment