Cleaning up serializers.ModelSerializer.get_fields and model_meta.get_field_info

This commit is contained in:
Kyle Valade 2014-11-12 23:17:11 -08:00
parent 3c6d84d0dd
commit d31e339a6b
2 changed files with 248 additions and 172 deletions

View File

@ -203,7 +203,7 @@ class Serializer(BaseSerializer):
"""
A dictionary of {field_name: field_instance}.
"""
# `fields` is evalutated lazily. We do this to ensure that we don't
# `fields` is evaluated lazily. We do this to ensure that we don't
# have issues importing modules that use ModelSerializers as fields,
# even if Django's app-loading stage has not yet run.
if not hasattr(self, '_fields'):
@ -631,177 +631,55 @@ class ModelSerializer(Serializer):
def get_fields(self):
declared_fields = copy.deepcopy(self._declared_fields)
meta_options = self._get_meta_options()
model = meta_options['model']
extra_kwargs = meta_options['extra_kwargs']
ret = OrderedDict()
model = getattr(self.Meta, 'model')
# Retrieve metadata about fields & relationships on the model class.
fields_info = model_meta.get_field_info(model)
fields = self._get_fields_to_serialize(declared_fields, fields_info, meta_options)
unique_fields = self._get_unique_fields(
model, fields, declared_fields, extra_kwargs)
serializer_fields = self._get_serializer_fields(
fields, declared_fields, fields_info, meta_options)
for field_name, field in unique_fields.items():
serializer_fields[field_name] = field
return serializer_fields
def _get_meta_options(self):
"""This should only be called the one time from `get_fields`"""
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
extra_kwargs = self._include_additional_options(extra_kwargs)
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
return {
'model': getattr(self.Meta, 'model'),
'fields': fields,
'exclude': exclude,
'extra_kwargs': extra_kwargs,
'depth': getattr(self.Meta, 'depth', 0),
}
def _get_fields_to_serialize(self, declared_fields, fields_info, meta_options):
fields = meta_options['fields']
# Use the default set of field names if none is supplied explicitly.
if fields is None:
fields = self._get_default_field_names(declared_fields, info)
fields = self._get_default_field_names(declared_fields, fields_info)
exclude = getattr(self.Meta, 'exclude', None)
if exclude is not None:
for field_name in exclude:
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.
unique_fields = {}
for model_field_name, field_name in model_field_mapping.items():
try:
model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist:
continue
# Deal with each of the `unique_for_*` cases.
for date_field_name in (
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
):
if date_field_name is None:
continue
# Get the model field that is refered too.
date_field = model._meta.get_field(date_field_name)
if date_field.auto_now_add:
default = CreateOnlyDefault(timezone.now)
elif date_field.auto_now:
default = timezone.now
elif date_field.has_default():
default = model_field.default
else:
default = empty
if date_field_name in model_field_mapping:
# The corresponding date field is present in the serializer
if date_field_name not in extra_kwargs:
extra_kwargs[date_field_name] = {}
if default is empty:
if 'required' not in extra_kwargs[date_field_name]:
extra_kwargs[date_field_name]['required'] = True
else:
if 'default' not in extra_kwargs[date_field_name]:
extra_kwargs[date_field_name]['default'] = default
else:
# The corresponding date field is not present in the,
# serializer. We have a default to use for the date, so
# add in a hidden field that populates it.
unique_fields[date_field_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):
# `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__)
)
# Check that any fields declared on the class are
# also explicity included in `Meta.fields`.
missing_fields = set(declared_fields.keys()) - set(fields)
if missing_fields:
missing_field = list(missing_fields)[0]
raise ImproperlyConfigured(
'Field `%s` has been declared on serializer `%s`, but '
'is missing from `Meta.fields`.' %
(missing_field, self.__class__.__name__)
)
# Populate any kwargs defined in `Meta.extra_kwargs`
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)
kwargs.update(extras)
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
for field_name, field in unique_fields.items():
ret[field_name] = field
return ret
return fields
def _include_additional_options(self, extra_kwargs):
read_only_fields = getattr(self.Meta, 'read_only_fields', None)
@ -859,6 +737,172 @@ class ModelSerializer(Serializer):
list(model_info.forward_relations.keys())
)
def _get_unique_fields(self, model, fields, declared_fields, extra_kwargs):
"""
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.
"""
model_field_mapping = self._get_model_field_mapping(
fields, declared_fields, extra_kwargs)
unique_fields = {}
for model_field_name, field_name in model_field_mapping.items():
try:
model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist:
continue
# Deal with each of the `unique_for_*` cases.
for date_field_name in (
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
):
if date_field_name is None:
continue
# Get the model field that is referred to.
date_field = model._meta.get_field(date_field_name)
if date_field.auto_now_add:
default = CreateOnlyDefault(timezone.now)
elif date_field.auto_now:
default = timezone.now
elif date_field.has_default():
default = model_field.default
else:
default = empty
if date_field_name in model_field_mapping:
# The corresponding date field is present in the serializer
if date_field_name not in extra_kwargs:
extra_kwargs[date_field_name] = {}
if default is empty:
if 'required' not in extra_kwargs[date_field_name]:
extra_kwargs[date_field_name]['required'] = True
else:
if 'default' not in extra_kwargs[date_field_name]:
extra_kwargs[date_field_name]['default'] = default
else:
# The corresponding date field is not present in the,
# serializer. We have a default to use for the date, so
# add in a hidden field that populates it.
unique_fields[date_field_name] = HiddenField(default=default)
return unique_fields
def _get_model_field_mapping(self, fields, declared_fields, extra_kwargs):
"""
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
return model_field_mapping
def _get_serializer_fields(self, fields, declared_fields, fields_info, meta_options):
"""Now determine the fields that should be included on the serializer"""
depth = meta_options['depth']
model = meta_options['model']
extra_kwargs = meta_options['extra_kwargs']
serializer_fields = OrderedDict()
for field_name in fields:
if field_name in declared_fields:
# Field is explicitly declared on the class, use that.
serializer_fields[field_name] = declared_fields[field_name]
continue
elif field_name in fields_info.fields_and_pk:
# Create regular model fields.
model_field = fields_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):
# `allow_blank` is only valid for textual fields.
kwargs.pop('allow_blank', None)
elif field_name in fields_info.relations:
# Create forward and reverse relationships.
relation_info = fields_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__)
)
# Check that any fields declared on the class are
# also explicitly included in `Meta.fields`.
missing_fields = set(declared_fields.keys()) - set(fields)
if missing_fields:
missing_field = list(missing_fields)[0]
raise ImproperlyConfigured(
'Field `%s` has been declared on serializer `%s`, but '
'is missing from `Meta.fields`.' %
(missing_field, self.__class__.__name__)
)
# Populate any kwargs defined in `Meta.extra_kwargs`
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)
kwargs.update(extras)
# Create the serializer field.
serializer_fields[field_name] = field_cls(**kwargs)
return serializer_fields
def _get_nested_class(self, nested_depth, relation_info):
class NestedSerializer(ModelSerializer):
class Meta:

View File

@ -34,7 +34,7 @@ def _resolve_model(obj):
Resolve supplied `obj` to a Django model class.
`obj` must be a Django model class itself, or a string
representation of one. Useful in situtations like GH #1225 where
representation of one. Useful in situations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
@ -51,15 +51,16 @@ def _resolve_model(obj):
def get_field_info(model):
"""
Given a model class, returns a `FieldInfo` instance containing metadata
about the various field types on the model.
Given a model class, returns a `FieldInfo` instance, which is a
`namedtuple`, containing metadata about the various field types on the
model including information about their relationships.
"""
opts = model._meta.concrete_model._meta
# Deal with the primary key.
pk = opts.pk
while pk.rel and pk.rel.parent_link:
# If model is a child via multitable inheritance, use parent's pk.
# If model is a child via multi-table inheritance, use parent's pk.
pk = pk.rel.to._meta.pk
# Deal with regular fields.
@ -67,9 +68,32 @@ def get_field_info(model):
for field in [field for field in opts.fields if field.serialize and not field.rel]:
fields[field.name] = field
# Deal with forward relationships.
forward_relations = _get_forward_relationships(opts)
reverse_relations = _get_reverse_relationships(opts)
fields_and_pk = _merge_fields_and_pk(pk, fields)
# Shortcut that merges both forward and reverse relationships
relations = OrderedDict(
list(forward_relations.items()) +
list(reverse_relations.items())
)
return FieldInfo(
pk, fields, forward_relations, reverse_relations, fields_and_pk,
relations
)
def _get_forward_relationships(opts):
"""
Deal with forward relationships.
:return OrderedDict: an ordered dictionary of field names mapped to
`RelationInfo`.
"""
forward_relations = OrderedDict()
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(
model_field=field,
related=_resolve_model(field.rel.to),
@ -88,7 +112,16 @@ def get_field_info(model):
)
)
# Deal with reverse relationships.
return forward_relations
def _get_reverse_relationships(opts):
"""
Deal with reverse relationships.
:return OrderedDict: an ordered dictionary of fields names mapped to
`RelationInfo`.
"""
reverse_relations = OrderedDict()
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
@ -112,18 +145,17 @@ def get_field_info(model):
)
)
# Shortcut that merges both regular fields and the pk,
# for simplifying regular field lookup.
return reverse_relations
def _merge_fields_and_pk(pk, fields):
"""
Shortcut that merges both regular fields and the pk, for simplifying
regular field lookup.
"""
fields_and_pk = OrderedDict()
fields_and_pk['pk'] = pk
fields_and_pk[pk.name] = pk
fields_and_pk.update(fields)
# Shortcut that merges both forward and reverse relationships
relations = OrderedDict(
list(forward_relations.items()) +
list(reverse_relations.items())
)
return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
return fields_and_pk