diff options
| author | Tom Christie | 2014-12-19 21:44:24 +0000 | 
|---|---|---|
| committer | Tom Christie | 2014-12-19 21:44:24 +0000 | 
| commit | 889a0bdeca942995ab32bf19c3d9fdbfaeec58ec (patch) | |
| tree | 6b0a43326c6541662862de2b21d290c8c0b86007 | |
| parent | 435aef77384759d5a29129cdb0cbe47b3de6df93 (diff) | |
| parent | 2a1485e00943b8280245d19e1e1f8514b1ef18ea (diff) | |
| download | django-rest-framework-889a0bdeca942995ab32bf19c3d9fdbfaeec58ec.tar.bz2 | |
Merge pull request #2322 from tomchristie/model-serializer-api
ModelSerializer API.
| -rw-r--r-- | docs/api-guide/serializers.md | 63 | ||||
| -rw-r--r-- | docs_theme/css/default.css | 4 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 678 | ||||
| -rw-r--r-- | rest_framework/utils/model_meta.py | 10 | ||||
| -rw-r--r-- | tests/test_model_serializer.py | 10 | 
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."          ) | 
