Merge pull request #2322 from tomchristie/model-serializer-api

ModelSerializer API.
This commit is contained in:
Tom Christie 2014-12-19 21:44:24 +00:00
commit 889a0bdeca
5 changed files with 533 additions and 266 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,6 +567,63 @@ 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.
## 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.
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`
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 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)`
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.
--- ---
# HyperlinkedModelSerializer # HyperlinkedModelSerializer

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

@ -327,7 +327,9 @@ class Serializer(BaseSerializer):
Returns a list of validator callables. Returns a list of validator callables.
""" """
# Used by the lazily-evaluated `validators` property. # Used by the lazily-evaluated `validators` property.
return getattr(getattr(self, 'Meta', None), 'validators', []) meta = getattr(self, 'Meta', None)
validators = getattr(meta, 'validators', None)
return validators[:] if validators else []
def get_initial(self): def get_initial(self):
if hasattr(self, 'initial_data'): if hasattr(self, 'initial_data'):
@ -696,7 +698,7 @@ class ModelSerializer(Serializer):
you need you should either declare the extra/differing fields explicitly on you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a `Serializer` class. the serializer class, or simply use a `Serializer` class.
""" """
_field_mapping = ClassLookupDict({ serializer_field_mapping = {
models.AutoField: IntegerField, models.AutoField: IntegerField,
models.BigIntegerField: IntegerField, models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField, models.BooleanField: BooleanField,
@ -719,8 +721,10 @@ class ModelSerializer(Serializer):
models.TextField: CharField, models.TextField: CharField,
models.TimeField: TimeField, models.TimeField: TimeField,
models.URLField: URLField, models.URLField: URLField,
}) }
_related_class = PrimaryKeyRelatedField serializer_related_class = PrimaryKeyRelatedField
# Default `create` and `update` behavior...
def create(self, validated_data): def create(self, validated_data):
""" """
@ -791,69 +795,81 @@ class ModelSerializer(Serializer):
return instance return instance
def get_validators(self): # Determine the fields to apply...
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return validators
# Determine the default set of validators.
validators = []
model_class = self.Meta.model
field_names = set([
field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source)
])
# Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes.
for parent_class in [model_class] + list(model_class._meta.parents.keys()):
for unique_together in parent_class._meta.unique_together:
if field_names.issuperset(set(unique_together)):
validator = UniqueTogetherValidator(
queryset=parent_class._default_manager,
fields=unique_together
)
validators.append(validator)
# Add any unique_for_date/unique_for_month/unique_for_year constraints.
info = model_meta.get_field_info(model_class)
for field_name, field in info.fields_and_pk.items():
if field.unique_for_date and field_name in field_names:
validator = UniqueForDateValidator(
queryset=model_class._default_manager,
field=field_name,
date_field=field.unique_for_date
)
validators.append(validator)
if field.unique_for_month and field_name in field_names:
validator = UniqueForMonthValidator(
queryset=model_class._default_manager,
field=field_name,
date_field=field.unique_for_month
)
validators.append(validator)
if field.unique_for_year and field_name in field_names:
validator = UniqueForYearValidator(
queryset=model_class._default_manager,
field=field_name,
date_field=field.unique_for_year
)
validators.append(validator)
return validators
def get_fields(self): def get_fields(self):
declared_fields = copy.deepcopy(self._declared_fields) """
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__
)
)
ret = OrderedDict() declared_fields = copy.deepcopy(self._declared_fields)
model = getattr(self.Meta, 'model') 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)
# Determine any extra field arguments and hidden fields that
# should be included
extra_kwargs = self.get_extra_kwargs()
extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
field_names, declared_fields, extra_kwargs
)
# 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:
fields[field_name] = declared_fields[field_name]
continue
# Determine the serializer field class and keyword arguments.
field_class, field_kwargs = self.build_field(
field_name, info, model, depth
)
# Include any kwargs defined in `Meta.extra_kwargs`
field_kwargs = self.build_field_kwargs(
field_kwargs, extra_kwargs, field_name
)
# Create the serializer field.
fields[field_name] = field_class(**field_kwargs)
# Add in any hidden fields.
fields.update(hidden_fields)
return fields
# Methods for determining the set of field names to include...
def get_field_names(self, declared_fields, info):
"""
Returns the list of all field names that should be created when
instantiating this serializer class. This is based on the default
set of fields, but also takes into account the `Meta.fields` or
`Meta.exclude` options if they have been specified.
"""
fields = getattr(self.Meta, 'fields', None) fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None) exclude = getattr(self.Meta, 'exclude', None)
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
if fields and not isinstance(fields, (list, tuple)): if fields and not isinstance(fields, (list, tuple)):
raise TypeError( raise TypeError(
@ -867,192 +883,191 @@ class ModelSerializer(Serializer):
type(exclude).__name__ type(exclude).__name__
) )
assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'." assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on "
"serializer {serializer_class}.".format(
serializer_class=self.__class__.__name__
)
)
extra_kwargs = self._include_additional_options(extra_kwargs) if fields is not None:
# Ensure that all declared fields have also been included in the
# Retrieve metadata about fields & relationships on the model class. # `Meta.fields` option.
info = model_meta.get_field_info(model) for field_name in declared_fields:
assert field_name in fields, (
# Use the default set of field names if none is supplied explicitly. "The field '{field_name}' was declared on serializer "
if fields is None: "{serializer_class}, but has not been included in the "
fields = self._get_default_field_names(declared_fields, info) "'fields' option.".format(
exclude = getattr(self.Meta, 'exclude', None) field_name=field_name,
if exclude is not None: serializer_class=self.__class__.__name__
for field_name in exclude:
assert field_name in fields, (
'The field in the `exclude` option must be a model field. Got %s.' %
field_name
) )
fields.remove(field_name)
# Determine the set of model fields, and the fields that they map to.
# We actually only need this to deal with the slightly awkward case
# of supporting `unique_for_date`/`unique_for_month`/`unique_for_year`.
model_field_mapping = {}
for field_name in fields:
if field_name in declared_fields:
field = declared_fields[field_name]
source = field.source or field_name
else:
try:
source = extra_kwargs[field_name]['source']
except KeyError:
source = field_name
# Model fields will always have a simple source mapping,
# they can't be nested attribute lookups.
if '.' not in source and source != '*':
model_field_mapping[source] = field_name
# Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to
# be in the input data in order to validate it.
hidden_fields = {}
unique_constraint_names = set()
for model_field_name, field_name in model_field_mapping.items():
try:
model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist:
continue
# Include each of the `unique_for_*` field names.
unique_constraint_names |= set([
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
])
unique_constraint_names -= set([None])
# Include each of the `unique_together` field names,
# so long as all the field names are included on the serializer.
for parent_class in [model] + list(model._meta.parents.keys()):
for unique_together_list in parent_class._meta.unique_together:
if set(fields).issuperset(set(unique_together_list)):
unique_constraint_names |= set(unique_together_list)
# Now we have all the field names that have uniqueness constraints
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
for unique_constraint_name in unique_constraint_names:
# Get the model field that is referred too.
unique_constraint_field = model._meta.get_field(unique_constraint_name)
if getattr(unique_constraint_field, 'auto_now_add', None):
default = CreateOnlyDefault(timezone.now)
elif getattr(unique_constraint_field, 'auto_now', None):
default = timezone.now
elif unique_constraint_field.has_default():
default = unique_constraint_field.default
else:
default = empty
if unique_constraint_name in model_field_mapping:
# The corresponding field is present in the serializer
if unique_constraint_name not in extra_kwargs:
extra_kwargs[unique_constraint_name] = {}
if default is empty:
if 'required' not in extra_kwargs[unique_constraint_name]:
extra_kwargs[unique_constraint_name]['required'] = True
else:
if 'default' not in extra_kwargs[unique_constraint_name]:
extra_kwargs[unique_constraint_name]['default'] = default
elif default is not empty:
# The corresponding field is not present in the,
# serializer. We have a default to use for it, so
# add in a hidden field that populates it.
hidden_fields[unique_constraint_name] = HiddenField(default=default)
# Now determine the fields that should be included on the serializer.
for field_name in fields:
if field_name in declared_fields:
# Field is explicitly declared on the class, use that.
ret[field_name] = declared_fields[field_name]
continue
elif field_name in info.fields_and_pk:
# Create regular model fields.
model_field = info.fields_and_pk[field_name]
field_cls = self._field_mapping[model_field]
kwargs = get_field_kwargs(field_name, model_field)
if 'choices' in kwargs:
# Fields with choices get coerced into `ChoiceField`
# instead of using their regular typed field.
field_cls = ChoiceField
if not issubclass(field_cls, 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):
# `allow_blank` is only valid for textual fields.
kwargs.pop('allow_blank', None)
elif field_name in info.relations:
# Create forward and reverse relationships.
relation_info = info.relations[field_name]
if depth:
field_cls = self._get_nested_class(depth, relation_info)
kwargs = get_nested_relation_kwargs(relation_info)
else:
field_cls = self._related_class
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)
elif hasattr(model, field_name):
# Create a read only field for model methods and properties.
field_cls = ReadOnlyField
kwargs = {}
elif field_name == api_settings.URL_FIELD_NAME:
# Create the URL field.
field_cls = HyperlinkedIdentityField
kwargs = get_url_kwargs(model)
else:
raise ImproperlyConfigured(
'Field name `%s` is not valid for model `%s`.' %
(field_name, model.__class__.__name__)
) )
return fields
# Check that any fields declared on the class are # Use the default set of field names if `Meta.fields` is not specified.
# also explicitly included in `Meta.fields`. fields = self.get_default_field_names(declared_fields, info)
missing_fields = set(declared_fields.keys()) - set(fields)
if missing_fields: if exclude is not None:
missing_field = list(missing_fields)[0] # If `Meta.exclude` is included, then remove those fields.
raise ImproperlyConfigured( for field_name in exclude:
'Field `%s` has been declared on serializer `%s`, but ' assert field_name in fields, (
'is missing from `Meta.fields`.' % "The field '{field_name}' was include on serializer "
(missing_field, self.__class__.__name__) "{serializer_class} in the 'exclude' option, but does "
"not match any model field.".format(
field_name=field_name,
serializer_class=self.__class__.__name__
)
) )
fields.remove(field_name)
# Populate any kwargs defined in `Meta.extra_kwargs` return fields
extras = extra_kwargs.get(field_name, {})
if extras.get('read_only', False):
for attr in [
'required', 'default', 'allow_blank', 'allow_null',
'min_length', 'max_length', 'min_value', 'max_value',
'validators', 'queryset'
]:
kwargs.pop(attr, None)
if extras.get('default') and kwargs.get('required') is False: def get_default_field_names(self, declared_fields, model_info):
kwargs.pop('required') """
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
return (
[model_info.pk.name] +
list(declared_fields.keys()) +
list(model_info.fields.keys()) +
list(model_info.forward_relations.keys())
)
kwargs.update(extras) # Methods for constructing serializer fields...
# Create the serializer field. def build_field(self, field_name, info, model_class, nested_depth):
ret[field_name] = field_cls(**kwargs) """
Return a two tuple of (cls, kwargs) to build a serializer field with.
"""
if field_name in info.fields_and_pk:
model_field = info.fields_and_pk[field_name]
return self.build_standard_field(field_name, model_field)
for field_name, field in hidden_fields.items(): elif field_name in info.relations:
ret[field_name] = field relation_info = info.relations[field_name]
if not nested_depth:
return self.build_relational_field(field_name, relation_info)
else:
return self.build_nested_field(field_name, relation_info, nested_depth)
return ret 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, model_class)
return self.build_unknown_field(field_name, model_class)
def build_standard_field(self, field_name, model_field):
"""
Create regular model fields.
"""
field_mapping = ClassLookupDict(self.serializer_field_mapping)
field_class = field_mapping[model_field]
field_kwargs = get_field_kwargs(field_name, model_field)
if 'choices' in field_kwargs:
# Fields with choices get coerced into `ChoiceField`
# instead of using their regular typed field.
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.
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.
field_kwargs.pop('allow_blank', None)
return field_class, field_kwargs
def build_relational_field(self, field_name, relation_info):
"""
Create fields for forward and reverse relationships.
"""
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_class, HyperlinkedRelatedField):
field_kwargs.pop('view_name', None)
return field_class, field_kwargs
def build_nested_field(self, field_name, relation_info, nested_depth):
"""
Create nested fields for forward and reverse relationships.
"""
class NestedSerializer(ModelSerializer):
class Meta:
model = relation_info.related_model
depth = nested_depth
field_class = NestedSerializer
field_kwargs = get_nested_relation_kwargs(relation_info)
return field_class, field_kwargs
def build_property_field(self, field_name, model_class):
"""
Create a read only field for model methods and properties.
"""
field_class = ReadOnlyField
field_kwargs = {}
return field_class, field_kwargs
def build_url_field(self, field_name, model_class):
"""
Create a field representing the object's own URL.
"""
field_class = HyperlinkedIdentityField
field_kwargs = get_url_kwargs(model_class)
return field_class, field_kwargs
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__)
)
def build_field_kwargs(self, kwargs, extra_kwargs, field_name):
"""
Include an 'extra_kwargs' that have been included for this field,
possibly removing any incompatible existing keyword arguments.
"""
extras = extra_kwargs.get(field_name, {})
if extras.get('read_only', False):
for attr in [
'required', 'default', 'allow_blank', 'allow_null',
'min_length', 'max_length', 'min_value', 'max_value',
'validators', 'queryset'
]:
kwargs.pop(attr, None)
if extras.get('default') and kwargs.get('required') is False:
kwargs.pop('required')
kwargs.update(extras)
return kwargs
# Methods for determining additional keyword arguments to apply...
def get_extra_kwargs(self):
"""
Return a dictionary mapping field names to a dictionary of
additional keyword arguments.
"""
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
def _include_additional_options(self, extra_kwargs):
read_only_fields = getattr(self.Meta, 'read_only_fields', None) read_only_fields = getattr(self.Meta, 'read_only_fields', None)
if read_only_fields is not None: if read_only_fields is not None:
for field_name in read_only_fields: for field_name in read_only_fields:
@ -1100,21 +1115,202 @@ class ModelSerializer(Serializer):
return extra_kwargs return extra_kwargs
def _get_default_field_names(self, declared_fields, model_info): def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
return ( """
[model_info.pk.name] + Return any additional field options that need to be included as a
list(declared_fields.keys()) + result of uniqueness constraints on the model. This is returned as
list(model_info.fields.keys()) + a two-tuple of:
list(model_info.forward_relations.keys())
('dict of updated extra kwargs', 'mapping of hidden fields')
"""
model = getattr(self.Meta, 'model')
model_fields = self._get_model_fields(
field_names, declared_fields, extra_kwargs
) )
def _get_nested_class(self, nested_depth, relation_info): # Determine if we need any additional `HiddenField` or extra keyword
class NestedSerializer(ModelSerializer): # arguments to deal with `unique_for` dates that are required to
class Meta: # be in the input data in order to validate it.
model = relation_info.related unique_constraint_names = set()
depth = nested_depth
return NestedSerializer for model_field in model_fields.values():
# Include each of the `unique_for_*` field names.
unique_constraint_names |= set([
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
])
unique_constraint_names -= set([None])
# Include each of the `unique_together` field names,
# so long as all the field names are included on the serializer.
for parent_class in [model] + list(model._meta.parents.keys()):
for unique_together_list in parent_class._meta.unique_together:
if set(field_names).issuperset(set(unique_together_list)):
unique_constraint_names |= set(unique_together_list)
# Now we have all the field names that have uniqueness constraints
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
hidden_fields = {}
uniqueness_extra_kwargs = {}
for unique_constraint_name in unique_constraint_names:
# Get the model field that is referred too.
unique_constraint_field = model._meta.get_field(unique_constraint_name)
if getattr(unique_constraint_field, 'auto_now_add', None):
default = CreateOnlyDefault(timezone.now)
elif getattr(unique_constraint_field, 'auto_now', None):
default = timezone.now
elif unique_constraint_field.has_default():
default = unique_constraint_field.default
else:
default = empty
if unique_constraint_name in model_fields:
# The corresponding field is present in the serializer
if default is empty:
uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
else:
uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
elif default is not empty:
# The corresponding field is not present in the,
# serializer. We have a default to use for it, so
# add in a hidden field that populates it.
hidden_fields[unique_constraint_name] = HiddenField(default=default)
# Update `extra_kwargs` with any new options.
for key, value in uniqueness_extra_kwargs.items():
if key in extra_kwargs:
extra_kwargs[key].update(value)
else:
extra_kwargs[key] = value
return extra_kwargs, hidden_fields
def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
"""
Returns all the model fields that are being mapped to by fields
on the serializer class.
Returned as a dict of 'model field name' -> 'model field'.
Used internally by `get_uniqueness_field_options`.
"""
model = getattr(self.Meta, 'model')
model_fields = {}
for field_name in field_names:
if field_name in declared_fields:
# If the field is declared on the serializer
field = declared_fields[field_name]
source = field.source or field_name
else:
try:
source = extra_kwargs[field_name]['source']
except KeyError:
source = field_name
if '.' in source or source == '*':
# Model fields will always have a simple source mapping,
# they can't be nested attribute lookups.
continue
try:
model_fields[source] = model._meta.get_field(source)
except FieldDoesNotExist:
pass
return model_fields
# Determine the validators to apply...
def get_validators(self):
"""
Determine the set of validators to use when instantiating serializer.
"""
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return validators[:]
# Otherwise use the default set of validators.
return (
self.get_unique_together_validators() +
self.get_unique_for_date_validators()
)
def get_unique_together_validators(self):
"""
Determine a default set of validators for any unique_together contraints.
"""
model_class_inheritance_tree = (
[self.Meta.model] +
list(self.Meta.model._meta.parents.keys())
)
# The field names we're passing though here only include fields
# which may map onto a model field. Any dotted field name lookups
# cannot map to a field, and must be a traversal, so we're not
# including those.
field_names = set([
field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source)
])
# Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes.
validators = []
for parent_class in model_class_inheritance_tree:
for unique_together in parent_class._meta.unique_together:
if field_names.issuperset(set(unique_together)):
validator = UniqueTogetherValidator(
queryset=parent_class._default_manager,
fields=unique_together
)
validators.append(validator)
return validators
def get_unique_for_date_validators(self):
"""
Determine a default set of validators for the following contraints:
* unique_for_date
* unique_for_month
* unique_for_year
"""
info = model_meta.get_field_info(self.Meta.model)
default_manager = self.Meta.model._default_manager
field_names = [field.source for field in self.fields.values()]
validators = []
for field_name, field in info.fields_and_pk.items():
if field.unique_for_date and field_name in field_names:
validator = UniqueForDateValidator(
queryset=default_manager,
field=field_name,
date_field=field.unique_for_date
)
validators.append(validator)
if field.unique_for_month and field_name in field_names:
validator = UniqueForMonthValidator(
queryset=default_manager,
field=field_name,
date_field=field.unique_for_month
)
validators.append(validator)
if field.unique_for_year and field_name in field_names:
validator = UniqueForYearValidator(
queryset=default_manager,
field=field_name,
date_field=field.unique_for_year
)
validators.append(validator)
return validators
class HyperlinkedModelSerializer(ModelSerializer): class HyperlinkedModelSerializer(ModelSerializer):
@ -1125,9 +1321,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
* A 'url' field is included instead of the 'id' field. * A 'url' field is included instead of the 'id' field.
* Relationships to other instances are hyperlinks, instead of primary keys. * Relationships to other instances are hyperlinks, instead of primary keys.
""" """
_related_class = HyperlinkedRelatedField serializer_related_class = HyperlinkedRelatedField
def _get_default_field_names(self, declared_fields, model_info): def get_default_field_names(self, declared_fields, model_info):
"""
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
return ( return (
[api_settings.URL_FIELD_NAME] + [api_settings.URL_FIELD_NAME] +
list(declared_fields.keys()) + list(declared_fields.keys()) +
@ -1135,10 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer):
list(model_info.forward_relations.keys()) list(model_info.forward_relations.keys())
) )
def _get_nested_class(self, nested_depth, relation_info): def build_nested_field(self, field_name, relation_info, nested_depth):
"""
Create nested fields for forward and reverse relationships.
"""
class NestedSerializer(HyperlinkedModelSerializer): class NestedSerializer(HyperlinkedModelSerializer):
class Meta: class Meta:
model = relation_info.related model = relation_info.related_model
depth = nested_depth depth = nested_depth - 1
return NestedSerializer field_class = NestedSerializer
field_kwargs = get_nested_relation_kwargs(relation_info)
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):
@ -221,11 +221,11 @@ class TestRegularFieldMappings(TestCase):
model = RegularFieldsModel model = RegularFieldsModel
fields = ('auto_field',) fields = ('auto_field',)
with self.assertRaises(ImproperlyConfigured) as excinfo: with self.assertRaises(AssertionError) as excinfo:
TestSerializer().fields TestSerializer().fields
expected = ( expected = (
'Field `missing` has been declared on serializer ' "The field 'missing' was declared on serializer TestSerializer, "
'`TestSerializer`, but is missing from `Meta.fields`.' "but has not been included in the 'fields' option."
) )
assert str(excinfo.exception) == expected assert str(excinfo.exception) == expected
@ -607,5 +607,5 @@ class TestSerializerMetaClass(TestCase):
exception = result.exception exception = result.exception
self.assertEqual( self.assertEqual(
str(exception), str(exception),
"Cannot set both 'fields' and 'exclude'." "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer."
) )