From 5830f7e13817210f5c6d955ad4fedfaa492aa209 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 10:15:36 +0000 Subject: [PATCH 01/11] get_unique_together_validators and get_unique_for_date_validators --- rest_framework/serializers.py | 51 ++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8de22f4b9..55828b03e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -792,14 +792,33 @@ class ModelSerializer(Serializer): return instance 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 - # Determine the default set of validators. - validators = [] - model_class = self.Meta.model + # 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) @@ -807,7 +826,8 @@ class ModelSerializer(Serializer): # 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()): + 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( @@ -815,13 +835,26 @@ class ModelSerializer(Serializer): 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 = [] - # 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, + queryset=default_manager, field=field_name, date_field=field.unique_for_date ) @@ -829,7 +862,7 @@ class ModelSerializer(Serializer): if field.unique_for_month and field_name in field_names: validator = UniqueForMonthValidator( - queryset=model_class._default_manager, + queryset=default_manager, field=field_name, date_field=field.unique_for_month ) @@ -837,7 +870,7 @@ class ModelSerializer(Serializer): if field.unique_for_year and field_name in field_names: validator = UniqueForYearValidator( - queryset=model_class._default_manager, + queryset=default_manager, field=field_name, date_field=field.unique_for_year ) From 6d907cde9a90aad76acb00482a1d70550bb95ccd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 12:18:40 +0000 Subject: [PATCH 02/11] get_field_names, get_default_field_names --- rest_framework/serializers.py | 102 +++++++++++++++++++++++---------- tests/test_model_serializer.py | 8 +-- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 52ea5b0b6..b391a94eb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -883,41 +883,14 @@ class ModelSerializer(Serializer): ret = OrderedDict() model = getattr(self.Meta, 'model') - 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( - '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'." - extra_kwargs = self._include_additional_options(extra_kwargs) # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) - # 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) + fields = self.get_field_names(declared_fields, info) # 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 @@ -1133,7 +1106,72 @@ class ModelSerializer(Serializer): return extra_kwargs - def _get_default_field_names(self, declared_fields, model_info): + 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()) + @@ -1160,7 +1198,11 @@ class HyperlinkedModelSerializer(ModelSerializer): """ _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()) + diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index da79164af..5c56c8dbb 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -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." ) From 1a84943a006abffb7e1b3b3ff55441c7a1132fa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 12:27:50 +0000 Subject: [PATCH 03/11] get_extra_kwargs --- rest_framework/serializers.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b391a94eb..d4b0926e6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -884,13 +884,12 @@ class ModelSerializer(Serializer): ret = OrderedDict() model = getattr(self.Meta, 'model') depth = getattr(self.Meta, 'depth', 0) - extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) - extra_kwargs = self._include_additional_options(extra_kwargs) # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) fields = self.get_field_names(declared_fields, info) + extra_kwargs = self.get_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 @@ -1024,17 +1023,6 @@ class ModelSerializer(Serializer): (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): @@ -1058,7 +1046,13 @@ class ModelSerializer(Serializer): return ret - def _include_additional_options(self, extra_kwargs): + 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', {}) + read_only_fields = getattr(self.Meta, 'read_only_fields', None) if read_only_fields is not None: for field_name in read_only_fields: From caa13181244ce3c074f647510bb38d7b0c8b4c70 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 13:13:20 +0000 Subject: [PATCH 04/11] get_uniqueness_field_options first pass --- rest_framework/serializers.py | 174 +++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 78 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d4b0926e6..5e9cbe361 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -888,89 +888,19 @@ class ModelSerializer(Serializer): # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) - fields = self.get_field_names(declared_fields, info) + field_names = self.get_field_names(declared_fields, info) extra_kwargs = self.get_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 + model_fields = self.get_model_fields(field_names, declared_fields, extra_kwargs) + uniqueness_extra_kwargs, hidden_fields = self.get_uniqueness_field_options(field_names, model_fields) + for key, value in uniqueness_extra_kwargs.items(): + if key in extra_kwargs: + extra_kwargs[key].update(value) 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) + extra_kwargs[key] = value # Now determine the fields that should be included on the serializer. - for field_name in fields: + for field_name in field_names: if field_name in declared_fields: # Field is explicitly declared on the class, use that. ret[field_name] = declared_fields[field_name] @@ -1046,6 +976,94 @@ class ModelSerializer(Serializer): return ret + 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' + 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 + + def get_uniqueness_field_options(self, field_names, model_fields): + model = getattr(self.Meta, 'model') + + # 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 = {} + 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: + extra_kwargs[unique_constraint_name] = {'required': True} + else: + 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) + + return extra_kwargs, hidden_fields + def get_extra_kwargs(self): """ Return a dictionary mapping field names to a dictionary of From 4a112fc3a616238b7995b3a442ae236116364ceb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 14:51:45 +0000 Subject: [PATCH 05/11] Clean up --- rest_framework/serializers.py | 63 +++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5e9cbe361..093b0eb51 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -722,6 +722,8 @@ class ModelSerializer(Serializer): }) _related_class = PrimaryKeyRelatedField + # Default `create` and `update` behavior... + def create(self, validated_data): """ We have a bit of extra checking around this in order to provide @@ -791,6 +793,8 @@ class ModelSerializer(Serializer): return instance + # Determine the validators to apply... + def get_validators(self): """ Determine the set of validators to use when instantiating serializer. @@ -878,28 +882,26 @@ class ModelSerializer(Serializer): return validators + # Determine the fields to apply... + def get_fields(self): declared_fields = copy.deepcopy(self._declared_fields) - - ret = OrderedDict() model = getattr(self.Meta, 'model') depth = getattr(self.Meta, 'depth', 0) # 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) - extra_kwargs = self.get_extra_kwargs() - model_fields = self.get_model_fields(field_names, declared_fields, extra_kwargs) - uniqueness_extra_kwargs, hidden_fields = self.get_uniqueness_field_options(field_names, model_fields) - for key, value in uniqueness_extra_kwargs.items(): - if key in extra_kwargs: - extra_kwargs[key].update(value) - else: - extra_kwargs[key] = value + # 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 + ) # Now determine the fields that should be included on the serializer. + ret = OrderedDict() for field_name in field_names: if field_name in declared_fields: # Field is explicitly declared on the class, use that. @@ -971,15 +973,17 @@ class ModelSerializer(Serializer): # Create the serializer field. ret[field_name] = field_cls(**kwargs) - for field_name, field in hidden_fields.items(): - ret[field_name] = field + ret.update(hidden_fields) return ret - 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' + 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 = {} @@ -1006,8 +1010,18 @@ class ModelSerializer(Serializer): return model_fields - def get_uniqueness_field_options(self, field_names, model_fields): + 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 @@ -1035,7 +1049,7 @@ class ModelSerializer(Serializer): # applied, we can add the extra 'required=...' or 'default=...' # arguments that are appropriate to these fields, or add a `HiddenField` for it. hidden_fields = {} - extra_kwargs = {} + uniqueness_extra_kwargs = {} for unique_constraint_name in unique_constraint_names: # Get the model field that is referred too. @@ -1053,15 +1067,22 @@ class ModelSerializer(Serializer): if unique_constraint_name in model_fields: # The corresponding field is present in the serializer if default is empty: - extra_kwargs[unique_constraint_name] = {'required': True} + uniqueness_extra_kwargs[unique_constraint_name] = {'required': True} else: - extra_kwargs[unique_constraint_name] = {'default': default} + 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_extra_kwargs(self): From 80bacc5fb00682b589b3280c7082af73e3aaa8f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 14:52:53 +0000 Subject: [PATCH 06/11] depth should reduce by one on each nesting level. Closes #2287. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ed709d848..6f89df0db 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1112,7 +1112,7 @@ class ModelSerializer(Serializer): class NestedSerializer(ModelSerializer): class Meta: model = relation_info.related - depth = nested_depth + depth = nested_depth - 1 return NestedSerializer @@ -1139,6 +1139,6 @@ class HyperlinkedModelSerializer(ModelSerializer): class NestedSerializer(HyperlinkedModelSerializer): class Meta: model = relation_info.related - depth = nested_depth + depth = nested_depth - 1 return NestedSerializer From f72928ea982cfe2127288dd6dc52f8006638b0c3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:09:57 +0000 Subject: [PATCH 07/11] build_field, build_final_kwargs --- rest_framework/serializers.py | 135 +++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1f76c4c19..80ad10f0a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -908,75 +908,92 @@ class ModelSerializer(Serializer): 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__) - ) + # Determine the serializer field class and keyword arguments. + field_cls, kwargs = self.build_field(field_name, info, model, depth) # 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) - - if extras.get('default') and kwargs.get('required') is False: - kwargs.pop('required') - - kwargs.update(extras) + kwargs = self.build_final_kwargs(kwargs, extra_kwargs, field_name) # Create the serializer field. ret[field_name] = field_cls(**kwargs) + # Add in any hidden fields. ret.update(hidden_fields) return ret + def build_field(self, field_name, info, model, depth): + if 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 field_cls, kwargs + + def build_final_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 + def _get_model_fields(self, field_names, declared_fields, extra_kwargs): """ Returns all the model fields that are being mapped to by fields From 75e81b82545704bac8afdf3270ba9f6c8da09c27 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:35:52 +0000 Subject: [PATCH 08/11] build_*_field methods --- rest_framework/serializers.py | 785 ++++++++++++++++++---------------- 1 file changed, 424 insertions(+), 361 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 80ad10f0a..a983d3fc7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -696,7 +696,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 +719,8 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, - }) - _related_class = PrimaryKeyRelatedField + } + serializer_related_class = PrimaryKeyRelatedField # Default `create` and `update` behavior... @@ -793,6 +793,417 @@ class ModelSerializer(Serializer): return instance + # Determine the fields to apply... + + 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) + model = getattr(self.Meta, 'model') + depth = getattr(self.Meta, 'depth', 0) + + # 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 + ) + + # Now determine the fields that should be included on the serializer. + ret = OrderedDict() + for field_name in field_names: + if field_name in declared_fields: + # Field is explicitly declared on the class, use that. + ret[field_name] = declared_fields[field_name] + continue + + # Determine the serializer field class and keyword arguments. + field_cls, kwargs = self.build_field(field_name, info, model, depth) + + # Populate any kwargs defined in `Meta.extra_kwargs` + kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name) + + # Create the serializer field. + ret[field_name] = field_cls(**kwargs) + + # Add in any hidden fields. + ret.update(hidden_fields) + + return ret + + # 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: + return self.build_standard_field(field_name, info, model) + + elif field_name in info.relations: + if not nested_depth: + return self.build_relational_field(field_name, info, model) + else: + return self.build_nested_field(field_name, info, model, nested_depth) + + elif hasattr(model, field_name): + return self.build_property_field(field_name, info, model) + + elif field_name == api_settings.URL_FIELD_NAME: + return self.build_url_field(field_name, info, model) + + return self.build_unknown_field(field_name, info, model) + + def build_standard_field(self, field_name, info, model): + """ + 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 + + 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, + 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', {}) + + read_only_fields = getattr(self.Meta, 'read_only_fields', None) + if read_only_fields is not None: + for field_name in read_only_fields: + kwargs = extra_kwargs.get(field_name, {}) + kwargs['read_only'] = True + extra_kwargs[field_name] = kwargs + + # These are all pending deprecation. + write_only_fields = getattr(self.Meta, 'write_only_fields', None) + if write_only_fields is not None: + warnings.warn( + "The `Meta.write_only_fields` option is pending deprecation. " + "Use `Meta.extra_kwargs={: {'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) + if view_name is not None: + warnings.warn( + "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 + + 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): + """ + 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): @@ -882,361 +1293,6 @@ class ModelSerializer(Serializer): return validators - # Determine the fields to apply... - - def get_fields(self): - declared_fields = copy.deepcopy(self._declared_fields) - model = getattr(self.Meta, 'model') - depth = getattr(self.Meta, 'depth', 0) - - # 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 - ) - - # Now determine the fields that should be included on the serializer. - ret = OrderedDict() - for field_name in field_names: - if field_name in declared_fields: - # Field is explicitly declared on the class, use that. - ret[field_name] = declared_fields[field_name] - continue - - # Determine the serializer field class and keyword arguments. - field_cls, kwargs = self.build_field(field_name, info, model, depth) - - # Populate any kwargs defined in `Meta.extra_kwargs` - kwargs = self.build_final_kwargs(kwargs, extra_kwargs, field_name) - - # Create the serializer field. - ret[field_name] = field_cls(**kwargs) - - # Add in any hidden fields. - ret.update(hidden_fields) - - return ret - - def build_field(self, field_name, info, model, depth): - if 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 field_cls, kwargs - - def build_final_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 - - 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 - - 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_extra_kwargs(self): - """ - Return a dictionary mapping field names to a dictionary of - additional keyword arguments. - """ - extra_kwargs = getattr(self.Meta, '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: - kwargs = extra_kwargs.get(field_name, {}) - kwargs['read_only'] = True - extra_kwargs[field_name] = kwargs - - # These are all pending deprecation. - write_only_fields = getattr(self.Meta, 'write_only_fields', None) - if write_only_fields is not None: - warnings.warn( - "The `Meta.write_only_fields` option is pending deprecation. " - "Use `Meta.extra_kwargs={: {'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) - if view_name is not None: - warnings.warn( - "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 - - 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_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()) - ) - - def _get_nested_class(self, nested_depth, relation_info): - class NestedSerializer(ModelSerializer): - class Meta: - model = relation_info.related - depth = nested_depth - 1 - - return NestedSerializer - class HyperlinkedModelSerializer(ModelSerializer): """ @@ -1246,7 +1302,7 @@ 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): """ @@ -1260,10 +1316,17 @@ 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, info, model, nested_depth): + """ + Create nested fields for forward and reverse relationships. + """ + relation_info = info.relations[field_name] + class NestedSerializer(HyperlinkedModelSerializer): class Meta: model = relation_info.related depth = nested_depth - 1 - return NestedSerializer + field_cls = NestedSerializer + kwargs = get_nested_relation_kwargs(relation_info) + return field_cls, kwargs From 62f78dfbf1b1dfa2d6406a4be5b83bc69267e851 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:50:29 +0000 Subject: [PATCH 09/11] Copy validators lists on instantiation. --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a983d3fc7..8adbafe45 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'): @@ -1213,7 +1215,7 @@ class ModelSerializer(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 + return validators[:] # Otherwise use the default set of validators. return ( From 48d15f6ff8a13aafd5b4977c8d1b4b7fe70b4f6a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 16:58:35 +0000 Subject: [PATCH 10/11] Stub out the documentation --- docs/api-guide/serializers.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index b9f0e7bc0..4d3dfa31b 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -567,6 +567,32 @@ The inner `Meta` class on serializers is not inherited from parent classes by de Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly. +## Advanced `ModelSerializer` usage + +The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer. + +#### `.serializer_field_mapping` + +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 build field methods + +#### `build_standard_field(**kwargs)` + +#### `build_relational_field(**kwargs)` + +#### `build_nested_field(**kwargs)` + +#### `build_property_field(**kwargs)` + +#### `build_url_field(**kwargs)` + +#### `build_unknown_field(**kwargs)` + --- # HyperlinkedModelSerializer From 2a1485e00943b8280245d19e1e1f8514b1ef18ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 21:32:43 +0000 Subject: [PATCH 11/11] Final bits of docs for ModelSerializer fields API --- docs/api-guide/serializers.md | 57 +++++++++--- docs_theme/css/default.css | 4 + rest_framework/serializers.py | 140 ++++++++++++++++------------- rest_framework/utils/model_meta.py | 10 +-- tests/test_model_serializer.py | 2 +- 5 files changed, 132 insertions(+), 81 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4d3dfa31b..dcbbd5f21 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,31 +567,62 @@ The inner `Meta` class on serializers is not inherited from parent classes by de Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly. -## Advanced `ModelSerializer` usage +## Customizing field mappings The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer. -#### `.serializer_field_mapping` +Normally if a `ModelSerializer` does not generate the fields you need by default the you should either add them to the class explicitly, or simply use a regular `Serializer` class instead. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model. + +### `.serializer_field_mapping` A mapping of Django model classes to REST framework serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class. -#### `.serializer_relational_field` +### `.serializer_relational_field` This property should be the serializer field class, that is used for relational fields by default. For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `HyperlinkedModelSerializer` this defaults to `HyperlinkedRelatedField`. -#### The build field methods +### The field_class and field_kwargs API -#### `build_standard_field(**kwargs)` +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_relational_field(**kwargs)` +### `.build_standard_field(self, field_name, model_field)` -#### `build_nested_field(**kwargs)` +Called to generate a serializer field that maps to a standard model field. -#### `build_property_field(**kwargs)` +The default implementation returns a serializer class based on the `serializer_field_mapping` attribute. -#### `build_url_field(**kwargs)` +### `.build_relational_field(self, field_name, relation_info)` -#### `build_unknown_field(**kwargs)` +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. --- diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 8c9cd5363..48d00366b 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 8adbafe45..623ed5865 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -802,10 +802,25 @@ class ModelSerializer(Serializer): Return the dict of field names -> field instances that should be used for `self.fields` when instantiating the serializer. """ + assert hasattr(self, 'Meta'), ( + 'Class {serializer_class} missing "Meta" attribute'.format( + serializer_class=self.__class__.__name__ + ) + ) + assert hasattr(self.Meta, 'model'), ( + 'Class {serializer_class} missing "Meta.model" attribute'.format( + serializer_class=self.__class__.__name__ + ) + ) + declared_fields = copy.deepcopy(self._declared_fields) model = getattr(self.Meta, 'model') depth = getattr(self.Meta, 'depth', 0) + if depth is not None: + assert depth >= 0, "'depth' may not be negative." + assert depth <= 10, "'depth' may not be greater than 10." + # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) field_names = self.get_field_names(declared_fields, info) @@ -817,27 +832,32 @@ class ModelSerializer(Serializer): field_names, declared_fields, extra_kwargs ) - # Now determine the fields that should be included on the serializer. - ret = OrderedDict() + # Determine the fields that should be included on the serializer. + fields = OrderedDict() + for field_name in field_names: + # If the field is explicitly declared on the class then use that. if field_name in declared_fields: - # Field is explicitly declared on the class, use that. - ret[field_name] = declared_fields[field_name] + fields[field_name] = declared_fields[field_name] continue # Determine the serializer field class and keyword arguments. - field_cls, kwargs = self.build_field(field_name, info, model, depth) + field_class, field_kwargs = self.build_field( + field_name, info, model, depth + ) - # Populate any kwargs defined in `Meta.extra_kwargs` - kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name) + # Include any kwargs defined in `Meta.extra_kwargs` + field_kwargs = self.build_field_kwargs( + field_kwargs, extra_kwargs, field_name + ) # Create the serializer field. - ret[field_name] = field_cls(**kwargs) + fields[field_name] = field_class(**field_kwargs) # Add in any hidden fields. - ret.update(hidden_fields) + fields.update(hidden_fields) - return ret + return fields # Methods for determining the set of field names to include... @@ -916,108 +936,105 @@ class ModelSerializer(Serializer): # Methods for constructing serializer fields... - def build_field(self, field_name, info, model, nested_depth): + def build_field(self, field_name, info, model_class, nested_depth): """ Return a two tuple of (cls, kwargs) to build a serializer field with. """ if field_name in info.fields_and_pk: - return self.build_standard_field(field_name, info, model) + model_field = info.fields_and_pk[field_name] + return self.build_standard_field(field_name, model_field) elif field_name in info.relations: + relation_info = info.relations[field_name] if not nested_depth: - return self.build_relational_field(field_name, info, model) + return self.build_relational_field(field_name, relation_info) else: - return self.build_nested_field(field_name, info, model, nested_depth) + return self.build_nested_field(field_name, relation_info, nested_depth) - elif hasattr(model, field_name): - return self.build_property_field(field_name, info, model) + elif hasattr(model_class, field_name): + return self.build_property_field(field_name, model_class) elif field_name == api_settings.URL_FIELD_NAME: - return self.build_url_field(field_name, info, model) + return self.build_url_field(field_name, model_class) - return self.build_unknown_field(field_name, info, model) + return self.build_unknown_field(field_name, model_class) - def build_standard_field(self, field_name, info, model): + def build_standard_field(self, field_name, model_field): """ Create regular model fields. """ field_mapping = ClassLookupDict(self.serializer_field_mapping) - model_field = info.fields_and_pk[field_name] - field_cls = field_mapping[model_field] - kwargs = get_field_kwargs(field_name, model_field) + field_class = field_mapping[model_field] + field_kwargs = get_field_kwargs(field_name, model_field) - if 'choices' in kwargs: + if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. - field_cls = ChoiceField - if not issubclass(field_cls, ModelField): + field_class = ChoiceField + if not issubclass(field_class, ModelField): # `model_field` is only valid for the fallback case of # `ModelField`, which is used when no other typed field # matched to the model field. - kwargs.pop('model_field', None) - if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField): + field_kwargs.pop('model_field', None) + if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField): # `allow_blank` is only valid for textual fields. - kwargs.pop('allow_blank', None) + field_kwargs.pop('allow_blank', None) - return field_cls, kwargs + return field_class, field_kwargs - def build_relational_field(self, field_name, info, model): + def build_relational_field(self, field_name, relation_info): """ Create fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - - field_cls = self.serializer_related_class - kwargs = get_relation_kwargs(field_name, relation_info) + field_class = self.serializer_related_class + field_kwargs = get_relation_kwargs(field_name, relation_info) # `view_name` is only valid for hyperlinked relationships. - if not issubclass(field_cls, HyperlinkedRelatedField): - kwargs.pop('view_name', None) + if not issubclass(field_class, HyperlinkedRelatedField): + field_kwargs.pop('view_name', None) - return field_cls, kwargs + return field_class, field_kwargs - def build_nested_field(self, field_name, info, model, nested_depth): + def build_nested_field(self, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - class NestedSerializer(ModelSerializer): class Meta: - model = relation_info.related - depth = nested_depth - 1 + model = relation_info.related_model + depth = nested_depth - field_cls = NestedSerializer - kwargs = get_nested_relation_kwargs(relation_info) + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) - return field_cls, kwargs + return field_class, field_kwargs - def build_property_field(self, field_name, info, model): + def build_property_field(self, field_name, model_class): """ Create a read only field for model methods and properties. """ - field_cls = ReadOnlyField - kwargs = {} + field_class = ReadOnlyField + field_kwargs = {} - return field_cls, kwargs + return field_class, field_kwargs - def build_url_field(self, field_name, info, model): + def build_url_field(self, field_name, model_class): """ Create a field representing the object's own URL. """ - field_cls = HyperlinkedIdentityField - kwargs = get_url_kwargs(model) + field_class = HyperlinkedIdentityField + field_kwargs = get_url_kwargs(model_class) - return field_cls, kwargs + return field_class, field_kwargs - def build_unknown_field(self, field_name, info, model): + def build_unknown_field(self, field_name, model_class): """ Raise an error on any unknown fields. """ raise ImproperlyConfigured( 'Field name `%s` is not valid for model `%s`.' % - (field_name, model.__class__.__name__) + (field_name, model_class.__name__) ) def build_field_kwargs(self, kwargs, extra_kwargs, field_name): @@ -1318,17 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer): list(model_info.forward_relations.keys()) ) - def build_nested_field(self, field_name, info, model, nested_depth): + def build_nested_field(self, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """ - relation_info = info.relations[field_name] - class NestedSerializer(HyperlinkedModelSerializer): class Meta: - model = relation_info.related + model = relation_info.related_model depth = nested_depth - 1 - field_cls = NestedSerializer - kwargs = get_nested_relation_kwargs(relation_info) - return field_cls, kwargs + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) + + return field_class, field_kwargs diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index c98725c66..dfc387ca5 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 5c56c8dbb..603faf477 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):