Commit 75e81b82 by Tom Christie

build_*_field methods

parent f72928ea
...@@ -696,7 +696,7 @@ class ModelSerializer(Serializer): ...@@ -696,7 +696,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 +719,8 @@ class ModelSerializer(Serializer): ...@@ -719,8 +719,8 @@ 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... # Default `create` and `update` behavior...
...@@ -793,98 +793,13 @@ class ModelSerializer(Serializer): ...@@ -793,98 +793,13 @@ class ModelSerializer(Serializer):
return instance return instance
# 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
# Determine the fields to apply... # Determine the fields to apply...
def get_fields(self): def get_fields(self):
"""
Return the dict of field names -> field instances that should be
used for `self.fields` when instantiating the serializer.
"""
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)
...@@ -912,7 +827,7 @@ class ModelSerializer(Serializer): ...@@ -912,7 +827,7 @@ class ModelSerializer(Serializer):
field_cls, kwargs = self.build_field(field_name, info, model, depth) field_cls, kwargs = self.build_field(field_name, info, model, depth)
# Populate any kwargs defined in `Meta.extra_kwargs` # Populate any kwargs defined in `Meta.extra_kwargs`
kwargs = self.build_final_kwargs(kwargs, extra_kwargs, field_name) kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name)
# Create the serializer field. # Create the serializer field.
ret[field_name] = field_cls(**kwargs) ret[field_name] = field_cls(**kwargs)
...@@ -922,57 +837,188 @@ class ModelSerializer(Serializer): ...@@ -922,57 +837,188 @@ class ModelSerializer(Serializer):
return ret return ret
def build_field(self, field_name, info, model, depth): # 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)
exclude = getattr(self.Meta, 'exclude', None)
if fields and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple. Got %s.' %
type(fields).__name__
)
if exclude and not isinstance(exclude, (list, tuple)):
raise TypeError(
'The `exclude` option must be a list or tuple. Got %s.' %
type(exclude).__name__
)
assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on "
"serializer {serializer_class}.".format(
serializer_class=self.__class__.__name__
)
)
if fields is not None:
# Ensure that all declared fields have also been included in the
# `Meta.fields` option.
for field_name in declared_fields:
assert field_name in fields, (
"The field '{field_name}' was declared on serializer "
"{serializer_class}, but has not been included in the "
"'fields' option.".format(
field_name=field_name,
serializer_class=self.__class__.__name__
)
)
return fields
# Use the default set of field names if `Meta.fields` is not specified.
fields = self.get_default_field_names(declared_fields, info)
if exclude is not None:
# If `Meta.exclude` is included, then remove those fields.
for field_name in exclude:
assert field_name in fields, (
"The field '{field_name}' was include on serializer "
"{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)
return fields
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 (
[model_info.pk.name] +
list(declared_fields.keys()) +
list(model_info.fields.keys()) +
list(model_info.forward_relations.keys())
)
# Methods for constructing serializer fields...
def build_field(self, field_name, info, model, nested_depth):
"""
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:
# Create regular model fields. return self.build_standard_field(field_name, info, model)
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: elif field_name in info.relations:
# Create forward and reverse relationships. if not nested_depth:
relation_info = info.relations[field_name] return self.build_relational_field(field_name, info, model)
if depth:
field_cls = self._get_nested_class(depth, relation_info)
kwargs = get_nested_relation_kwargs(relation_info)
else: else:
field_cls = self._related_class return self.build_nested_field(field_name, info, model, nested_depth)
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): elif hasattr(model, field_name):
# Create a read only field for model methods and properties. return self.build_property_field(field_name, info, model)
field_cls = ReadOnlyField
kwargs = {}
elif field_name == api_settings.URL_FIELD_NAME: elif field_name == api_settings.URL_FIELD_NAME:
# Create the URL field. return self.build_url_field(field_name, info, model)
field_cls = HyperlinkedIdentityField
kwargs = get_url_kwargs(model)
else: return self.build_unknown_field(field_name, info, model)
raise ImproperlyConfigured(
'Field name `%s` is not valid for model `%s`.' % def build_standard_field(self, field_name, info, model):
(field_name, model.__class__.__name__) """
) 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)
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)
return field_cls, kwargs
def build_relational_field(self, field_name, info, model):
"""
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)
# `view_name` is only valid for hyperlinked relationships.
if not issubclass(field_cls, HyperlinkedRelatedField):
kwargs.pop('view_name', None)
return field_cls, kwargs
def build_nested_field(self, field_name, info, model, 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
field_cls = NestedSerializer
kwargs = get_nested_relation_kwargs(relation_info)
return field_cls, kwargs
def build_property_field(self, field_name, info, model):
"""
Create a read only field for model methods and properties.
"""
field_cls = ReadOnlyField
kwargs = {}
return field_cls, kwargs
def build_url_field(self, field_name, info, model):
"""
Create a field representing the object's own URL.
"""
field_cls = HyperlinkedIdentityField
kwargs = get_url_kwargs(model)
return field_cls, kwargs return field_cls, kwargs
def build_final_kwargs(self, kwargs, extra_kwargs, field_name): def build_unknown_field(self, field_name, info, model):
"""
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, Include an 'extra_kwargs' that have been included for this field,
possibly removing any incompatible existing keyword arguments. possibly removing any incompatible existing keyword arguments.
...@@ -994,38 +1040,61 @@ class ModelSerializer(Serializer): ...@@ -994,38 +1040,61 @@ class ModelSerializer(Serializer):
return kwargs return kwargs
def _get_model_fields(self, field_names, declared_fields, extra_kwargs): # Methods for determining additional keyword arguments to apply...
def get_extra_kwargs(self):
""" """
Returns all the model fields that are being mapped to by fields Return a dictionary mapping field names to a dictionary of
on the serializer class. additional keyword arguments.
Returned as a dict of 'model field name' -> 'model field'.
Used internally by `get_uniqueness_field_options`.
""" """
model = getattr(self.Meta, 'model') extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
model_fields = {}
for field_name in field_names: read_only_fields = getattr(self.Meta, 'read_only_fields', None)
if field_name in declared_fields: if read_only_fields is not None:
# If the field is declared on the serializer for field_name in read_only_fields:
field = declared_fields[field_name] kwargs = extra_kwargs.get(field_name, {})
source = field.source or field_name kwargs['read_only'] = True
else: extra_kwargs[field_name] = kwargs
try:
source = extra_kwargs[field_name]['source']
except KeyError:
source = field_name
if '.' in source or source == '*': # These are all pending deprecation.
# Model fields will always have a simple source mapping, write_only_fields = getattr(self.Meta, 'write_only_fields', None)
# they can't be nested attribute lookups. if write_only_fields is not None:
continue warnings.warn(
"The `Meta.write_only_fields` option is pending deprecation. "
"Use `Meta.extra_kwargs={<field_name>: {'write_only': True}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
for field_name in write_only_fields:
kwargs = extra_kwargs.get(field_name, {})
kwargs['write_only'] = True
extra_kwargs[field_name] = kwargs
try: view_name = getattr(self.Meta, 'view_name', None)
model_fields[source] = model._meta.get_field(source) if view_name is not None:
except FieldDoesNotExist: warnings.warn(
pass "The `Meta.view_name` option is pending deprecation. "
"Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
kwargs['view_name'] = view_name
extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
return model_fields lookup_field = getattr(self.Meta, 'lookup_field', None)
if lookup_field is not None:
warnings.warn(
"The `Meta.lookup_field` option is pending deprecation. "
"Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
kwargs['lookup_field'] = lookup_field
extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
return extra_kwargs
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs): def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
""" """
...@@ -1102,140 +1171,127 @@ class ModelSerializer(Serializer): ...@@ -1102,140 +1171,127 @@ class ModelSerializer(Serializer):
return extra_kwargs, hidden_fields return extra_kwargs, hidden_fields
def get_extra_kwargs(self): def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
""" """
Return a dictionary mapping field names to a dictionary of Returns all the model fields that are being mapped to by fields
additional keyword arguments. on the serializer class.
Returned as a dict of 'model field name' -> 'model field'.
Used internally by `get_uniqueness_field_options`.
""" """
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) model = getattr(self.Meta, 'model')
model_fields = {}
read_only_fields = getattr(self.Meta, 'read_only_fields', None) for field_name in field_names:
if read_only_fields is not None: if field_name in declared_fields:
for field_name in read_only_fields: # If the field is declared on the serializer
kwargs = extra_kwargs.get(field_name, {}) field = declared_fields[field_name]
kwargs['read_only'] = True source = field.source or field_name
extra_kwargs[field_name] = kwargs else:
try:
source = extra_kwargs[field_name]['source']
except KeyError:
source = field_name
# These are all pending deprecation. if '.' in source or source == '*':
write_only_fields = getattr(self.Meta, 'write_only_fields', None) # Model fields will always have a simple source mapping,
if write_only_fields is not None: # they can't be nested attribute lookups.
warnings.warn( continue
"The `Meta.write_only_fields` option is pending deprecation. "
"Use `Meta.extra_kwargs={<field_name>: {'write_only': True}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
for field_name in write_only_fields:
kwargs = extra_kwargs.get(field_name, {})
kwargs['write_only'] = True
extra_kwargs[field_name] = kwargs
view_name = getattr(self.Meta, 'view_name', None) try:
if view_name is not None: model_fields[source] = model._meta.get_field(source)
warnings.warn( except FieldDoesNotExist:
"The `Meta.view_name` option is pending deprecation. " pass
"Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
kwargs['view_name'] = view_name
extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
lookup_field = getattr(self.Meta, 'lookup_field', None) return model_fields
if lookup_field is not None:
warnings.warn(
"The `Meta.lookup_field` option is pending deprecation. "
"Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
PendingDeprecationWarning,
stacklevel=3
)
kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
kwargs['lookup_field'] = lookup_field
extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
return extra_kwargs # Determine the validators to apply...
def get_field_names(self, declared_fields, info): def get_validators(self):
""" """
Returns the list of all field names that should be created when Determine the set of validators to use when instantiating serializer.
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) # If the validators have been declared explicitly then use that.
exclude = getattr(self.Meta, 'exclude', None) validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
if fields and not isinstance(fields, (list, tuple)): return validators
raise TypeError(
'The `fields` option must be a list or tuple. Got %s.' %
type(fields).__name__
)
if exclude and not isinstance(exclude, (list, tuple)):
raise TypeError(
'The `exclude` option must be a list or tuple. Got %s.' %
type(exclude).__name__
)
assert not (fields and exclude), ( # Otherwise use the default set of validators.
"Cannot set both 'fields' and 'exclude' options on " return (
"serializer {serializer_class}.".format( self.get_unique_together_validators() +
serializer_class=self.__class__.__name__ self.get_unique_for_date_validators()
)
) )
if fields is not None: def get_unique_together_validators(self):
# Ensure that all declared fields have also been included in the """
# `Meta.fields` option. Determine a default set of validators for any unique_together contraints.
for field_name in declared_fields: """
assert field_name in fields, ( model_class_inheritance_tree = (
"The field '{field_name}' was declared on serializer " [self.Meta.model] +
"{serializer_class}, but has not been included in the " list(self.Meta.model._meta.parents.keys())
"'fields' option.".format( )
field_name=field_name,
serializer_class=self.__class__.__name__
)
)
return fields
# Use the default set of field names if `Meta.fields` is not specified. # The field names we're passing though here only include fields
fields = self.get_default_field_names(declared_fields, info) # 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)
])
if exclude is not None: # Note that we make sure to check `unique_together` both on the
# If `Meta.exclude` is included, then remove those fields. # base model class, but also on any parent classes.
for field_name in exclude: validators = []
assert field_name in fields, ( for parent_class in model_class_inheritance_tree:
"The field '{field_name}' was include on serializer " for unique_together in parent_class._meta.unique_together:
"{serializer_class} in the 'exclude' option, but does " if field_names.issuperset(set(unique_together)):
"not match any model field.".format( validator = UniqueTogetherValidator(
field_name=field_name, queryset=parent_class._default_manager,
serializer_class=self.__class__.__name__ fields=unique_together
) )
) validators.append(validator)
fields.remove(field_name) return validators
return fields
def get_default_field_names(self, declared_fields, model_info): def get_unique_for_date_validators(self):
""" """
Return the default list of field names that will be used if the Determine a default set of validators for the following contraints:
`Meta.fields` option is not specified.
* unique_for_date
* unique_for_month
* unique_for_year
""" """
return ( info = model_meta.get_field_info(self.Meta.model)
[model_info.pk.name] + default_manager = self.Meta.model._default_manager
list(declared_fields.keys()) + field_names = [field.source for field in self.fields.values()]
list(model_info.fields.keys()) +
list(model_info.forward_relations.keys())
)
def _get_nested_class(self, nested_depth, relation_info): validators = []
class NestedSerializer(ModelSerializer):
class Meta:
model = relation_info.related
depth = nested_depth - 1
return NestedSerializer 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):
...@@ -1246,7 +1302,7 @@ class HyperlinkedModelSerializer(ModelSerializer): ...@@ -1246,7 +1302,7 @@ 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):
""" """
...@@ -1260,10 +1316,17 @@ class HyperlinkedModelSerializer(ModelSerializer): ...@@ -1260,10 +1316,17 @@ 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, info, model, nested_depth):
"""
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
depth = nested_depth - 1 depth = nested_depth - 1
return NestedSerializer field_cls = NestedSerializer
kwargs = get_nested_relation_kwargs(relation_info)
return field_cls, kwargs
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