aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2014-12-19 21:44:24 +0000
committerTom Christie2014-12-19 21:44:24 +0000
commit889a0bdeca942995ab32bf19c3d9fdbfaeec58ec (patch)
tree6b0a43326c6541662862de2b21d290c8c0b86007
parent435aef77384759d5a29129cdb0cbe47b3de6df93 (diff)
parent2a1485e00943b8280245d19e1e1f8514b1ef18ea (diff)
downloaddjango-rest-framework-889a0bdeca942995ab32bf19c3d9fdbfaeec58ec.tar.bz2
Merge pull request #2322 from tomchristie/model-serializer-api
ModelSerializer API.
-rw-r--r--docs/api-guide/serializers.md63
-rw-r--r--docs_theme/css/default.css4
-rw-r--r--rest_framework/serializers.py678
-rw-r--r--rest_framework/utils/model_meta.py10
-rw-r--r--tests/test_model_serializer.py10
5 files changed, 516 insertions, 249 deletions
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index b9f0e7bc..dcbbd5f2 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -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,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.
+## 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
diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css
index 8c9cd536..48d00366 100644
--- a/docs_theme/css/default.css
+++ b/docs_theme/css/default.css
@@ -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%;
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ed709d84..623ed586 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -327,7 +327,9 @@ class Serializer(BaseSerializer):
Returns a list of validator callables.
"""
# 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):
if hasattr(self, 'initial_data'):
@@ -696,7 +698,7 @@ class ModelSerializer(Serializer):
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a `Serializer` class.
"""
- _field_mapping = ClassLookupDict({
+ serializer_field_mapping = {
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
@@ -719,8 +721,10 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
- })
- _related_class = PrimaryKeyRelatedField
+ }
+ serializer_related_class = PrimaryKeyRelatedField
+
+ # Default `create` and `update` behavior...
def create(self, validated_data):
"""
@@ -791,69 +795,81 @@ class ModelSerializer(Serializer):
return instance
- def get_validators(self):
- # 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 fields to apply...
- # 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)
- ])
+ def get_fields(self):
+ """
+ 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__
+ )
+ )
- # 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)
+ declared_fields = copy.deepcopy(self._declared_fields)
+ model = getattr(self.Meta, 'model')
+ depth = getattr(self.Meta, 'depth', 0)
- # 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 depth is not None:
+ assert depth >= 0, "'depth' may not be negative."
+ assert depth <= 10, "'depth' may not be greater than 10."
- 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)
+ # 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)
- 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)
+ # 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
+ )
- return validators
+ # Determine the fields that should be included on the serializer.
+ fields = OrderedDict()
- def get_fields(self):
- declared_fields = copy.deepcopy(self._declared_fields)
+ 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
- ret = OrderedDict()
- model = getattr(self.Meta, 'model')
+ # 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)
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)):
raise TypeError(
@@ -867,192 +883,191 @@ class ModelSerializer(Serializer):
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
+ # `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)
- # Retrieve metadata about fields & relationships on the model class.
- info = model_meta.get_field_info(model)
+ return 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)
- exclude = getattr(self.Meta, 'exclude', None)
- if exclude is not None:
- 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)
+ 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())
+ )
- # 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
+ # Methods for constructing serializer fields...
+
+ 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:
+ 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, relation_info)
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 self.build_nested_field(field_name, relation_info, nested_depth)
- # 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()
+ elif hasattr(model_class, field_name):
+ return self.build_property_field(field_name, model_class)
- for model_field_name, field_name in model_field_mapping.items():
- try:
- model_field = model._meta.get_field(model_field_name)
- except FieldDoesNotExist:
- continue
+ elif field_name == api_settings.URL_FIELD_NAME:
+ return self.build_url_field(field_name, model_class)
- # 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
- ])
+ return self.build_unknown_field(field_name, model_class)
- unique_constraint_names -= set([None])
+ 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)
- # 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)
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_class, HyperlinkedRelatedField):
+ field_kwargs.pop('view_name', None)
- # 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)
+ return field_class, field_kwargs
- 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
+ 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
- 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)
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- # 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
+ return field_class, field_kwargs
- 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)
+ def build_property_field(self, field_name, model_class):
+ """
+ Create a read only field for model methods and properties.
+ """
+ field_class = ReadOnlyField
+ field_kwargs = {}
- else:
- raise ImproperlyConfigured(
- 'Field name `%s` is not valid for model `%s`.' %
- (field_name, model.__class__.__name__)
- )
+ return field_class, field_kwargs
- # 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__)
- )
+ 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)
- # 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)
+ return field_class, field_kwargs
- if extras.get('default') and kwargs.get('required') is False:
- kwargs.pop('required')
+ 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, {})
- kwargs.update(extras)
+ 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)
- # Create the serializer field.
- ret[field_name] = field_cls(**kwargs)
+ if extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
- for field_name, field in hidden_fields.items():
- ret[field_name] = field
+ kwargs.update(extras)
- return ret
+ 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)
if read_only_fields is not None:
for field_name in read_only_fields:
@@ -1100,21 +1115,202 @@ class ModelSerializer(Serializer):
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 any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('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
+ )
+
+ # 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_constraint_names = set()
+
+ 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 (
- [model_info.pk.name] +
- list(declared_fields.keys()) +
- list(model_info.fields.keys()) +
- list(model_info.forward_relations.keys())
+ self.get_unique_together_validators() +
+ self.get_unique_for_date_validators()
)
- def _get_nested_class(self, nested_depth, relation_info):
- class NestedSerializer(ModelSerializer):
- class Meta:
- model = relation_info.related
- depth = nested_depth
+ 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())
+ )
- return NestedSerializer
+ # 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):
@@ -1125,9 +1321,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
* A 'url' field is included instead of the 'id' field.
* 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 (
[api_settings.URL_FIELD_NAME] +
list(declared_fields.keys()) +
@@ -1135,10 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer):
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 Meta:
- model = relation_info.related
- depth = nested_depth
+ model = relation_info.related_model
+ depth = nested_depth - 1
+
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- return NestedSerializer
+ return field_class, field_kwargs
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index c98725c6..dfc387ca 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -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)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index da79164a..603faf47 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -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):
@@ -221,11 +221,11 @@ class TestRegularFieldMappings(TestCase):
model = RegularFieldsModel
fields = ('auto_field',)
- with self.assertRaises(ImproperlyConfigured) as excinfo:
+ with self.assertRaises(AssertionError) as excinfo:
TestSerializer().fields
expected = (
- 'Field `missing` has been declared on serializer '
- '`TestSerializer`, but is missing from `Meta.fields`.'
+ "The field 'missing' was declared on serializer TestSerializer, "
+ "but has not been included in the 'fields' option."
)
assert str(excinfo.exception) == expected
@@ -607,5 +607,5 @@ class TestSerializerMetaClass(TestCase):
exception = result.exception
self.assertEqual(
str(exception),
- "Cannot set both 'fields' and 'exclude'."
+ "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer."
)