diff options
Diffstat (limited to 'rest_framework/serializers.py')
| -rw-r--r-- | rest_framework/serializers.py | 701 |
1 files changed, 587 insertions, 114 deletions
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d2740fc2..f7aa3a7d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,20 +10,31 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -from django.core.exceptions import ImproperlyConfigured, ValidationError +from django.core.exceptions import ImproperlyConfigured from django.db import models +from django.db.models.fields import FieldDoesNotExist from django.utils import six -from django.utils.datastructures import SortedDict -from collections import namedtuple +from django.utils.translation import ugettext_lazy as _ +from rest_framework.compat import OrderedDict +from rest_framework.exceptions import ValidationError from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings from rest_framework.utils import html, model_meta, representation from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, get_relation_kwargs, get_nested_relation_kwargs, - lookup_class + ClassLookupDict +) +from rest_framework.utils.serializer_helpers import ( + ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict +) +from rest_framework.validators import ( + UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, + UniqueTogetherValidator ) import copy +import inspect +import warnings # Note: We do the following so that users of the framework can use this style: # @@ -36,19 +47,64 @@ from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA -FieldResult = namedtuple('FieldResult', ['field', 'value', 'error']) +# We assume that 'validators' are intended for the child serializer, +# rather than the parent serializer. +LIST_SERIALIZER_KWARGS = ( + 'read_only', 'write_only', 'required', 'default', 'initial', 'source', + 'label', 'help_text', 'style', 'error_messages', + 'instance', 'data', 'partial', 'context' +) +# BaseSerializer +# -------------- + class BaseSerializer(Field): """ The BaseSerializer class provides a minimal class which may be used for writing custom serializer implementations. """ - def __init__(self, instance=None, data=None, **kwargs): - super(BaseSerializer, self).__init__(**kwargs) self.instance = instance self._initial_data = data + self.partial = kwargs.pop('partial', False) + self._context = kwargs.pop('context', {}) + kwargs.pop('many', None) + super(BaseSerializer, self).__init__(**kwargs) + + def __new__(cls, *args, **kwargs): + # We override this method in order to automagically create + # `ListSerializer` classes instead when `many=True` is set. + if kwargs.pop('many', False): + return cls.many_init(*args, **kwargs) + return super(BaseSerializer, cls).__new__(cls, *args, **kwargs) + + @classmethod + def many_init(cls, *args, **kwargs): + """ + This method implements the creation of a `ListSerializer` parent + class when `many=True` is used. You can customize it if you need to + control which keyword arguments are passed to the parent, and + which are passed to the child. + + Note that we're over-cautious in passing most arguments to both parent + and child classes in order to try to cover the general case. If you're + overriding this method you'll probably want something much simpler, eg: + + @classmethod + def many_init(cls, *args, **kwargs): + kwargs['child'] = cls() + return CustomListSerializer(*args, **kwargs) + """ + child_serializer = cls(*args, **kwargs) + list_kwargs = {'child': child_serializer} + list_kwargs.update(dict([ + (key, value) for key, value in kwargs.items() + if key in LIST_SERIALIZER_KWARGS + ])) + meta = getattr(cls, 'Meta', None) + list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer) + return list_serializer_class(*args, **list_kwargs) def to_internal_value(self, data): raise NotImplementedError('`to_internal_value()` must be implemented.') @@ -56,31 +112,52 @@ class BaseSerializer(Field): def to_representation(self, instance): raise NotImplementedError('`to_representation()` must be implemented.') - def update(self, instance, attrs): + def update(self, instance, validated_data): raise NotImplementedError('`update()` must be implemented.') - def create(self, attrs): + def create(self, validated_data): raise NotImplementedError('`create()` must be implemented.') - def save(self, extras=None): - attrs = self.validated_data - if extras is not None: - attrs = dict(list(attrs.items()) + list(extras.items())) + def save(self, **kwargs): + assert not hasattr(self, 'save_object'), ( + 'Serializer `%s.%s` has old-style version 2 `.save_object()` ' + 'that is no longer compatible with REST framework 3. ' + 'Use the new-style `.create()` and `.update()` methods instead.' % + (self.__class__.__module__, self.__class__.__name__) + ) + + validated_data = dict( + list(self.validated_data.items()) + + list(kwargs.items()) + ) if self.instance is not None: - self.update(self.instance, attrs) + self.instance = self.update(self.instance, validated_data) + assert self.instance is not None, ( + '`update()` did not return an object instance.' + ) else: - self.instance = self.create(attrs) + self.instance = self.create(validated_data) + assert self.instance is not None, ( + '`create()` did not return an object instance.' + ) return self.instance def is_valid(self, raise_exception=False): + assert not hasattr(self, 'restore_object'), ( + 'Serializer `%s.%s` has old-style version 2 `.restore_object()` ' + 'that is no longer compatible with REST framework 3. ' + 'Use the new-style `.create()` and `.update()` methods instead.' % + (self.__class__.__module__, self.__class__.__name__) + ) + if not hasattr(self, '_validated_data'): try: - self._validated_data = self.to_internal_value(self._initial_data) + self._validated_data = self.run_validation(self._initial_data) except ValidationError as exc: self._validated_data = {} - self._errors = exc.message_dict + self._errors = exc.detail else: self._errors = {} @@ -92,13 +169,10 @@ class BaseSerializer(Field): @property def data(self): if not hasattr(self, '_data'): - if self.instance is not None: + if self.instance is not None and not getattr(self, '_errors', None): self._data = self.to_representation(self.instance) - elif self._initial_data is not None: - self._data = dict([ - (field_name, field.get_value(self._initial_data)) - for field_name, field in self.fields.items() - ]) + elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None): + self._data = self.to_representation(self.validated_data) else: self._data = self.get_initial() return self._data @@ -118,6 +192,9 @@ class BaseSerializer(Field): return self._validated_data +# Serializer & ListSerializer classes +# ----------------------------------- + class SerializerMetaclass(type): """ This metaclass sets a dictionary named `base_fields` on the class. @@ -141,7 +218,7 @@ class SerializerMetaclass(type): if hasattr(base, '_declared_fields'): fields = list(base._declared_fields.items()) + fields - return SortedDict(fields) + return OrderedDict(fields) def __new__(cls, name, bases, attrs): attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) @@ -150,44 +227,53 @@ class SerializerMetaclass(type): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): - def __init__(self, *args, **kwargs): - self.context = kwargs.pop('context', {}) - kwargs.pop('partial', None) - kwargs.pop('many', None) - - super(Serializer, self).__init__(*args, **kwargs) + default_error_messages = { + 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') + } + @property + def fields(self): + """ + A dictionary of {field_name: field_instance}. + """ + # `fields` is evalutated lazily. We do this to ensure that we don't + # have issues importing modules that use ModelSerializers as fields, + # even if Django's app-loading stage has not yet run. + if not hasattr(self, '_fields'): + self._fields = BindingDict(self) + for key, value in self.get_fields().items(): + self._fields[key] = value + return self._fields + + def get_fields(self): + """ + Returns a dictionary of {field_name: field_instance}. + """ # Every new serializer is created with a clone of the field instances. # This allows users to dynamically modify the fields on a serializer # instance without affecting every other serializer class. - self.fields = self._get_base_fields() - - # Setup all the child fields, to provide them with the current context. - for field_name, field in self.fields.items(): - field.bind(field_name, self, self) - - def __new__(cls, *args, **kwargs): - # We override this method in order to automagically create - # `ListSerializer` classes instead when `many=True` is set. - if kwargs.pop('many', False): - kwargs['child'] = cls() - return ListSerializer(*args, **kwargs) - return super(Serializer, cls).__new__(cls, *args, **kwargs) - - def _get_base_fields(self): return copy.deepcopy(self._declared_fields) - def bind(self, field_name, parent, root): - # If the serializer is used as a field then when it becomes bound - # it also needs to bind all its child fields. - super(Serializer, self).bind(field_name, parent, root) - for field_name, field in self.fields.items(): - field.bind(field_name, self, root) + def get_validators(self): + """ + Returns a list of validator callables. + """ + # Used by the lazily-evaluated `validators` property. + return getattr(getattr(self, 'Meta', None), 'validators', []) def get_initial(self): - return dict([ + if self._initial_data is not None: + return OrderedDict([ + (field_name, field.get_value(self._initial_data)) + for field_name, field in self.fields.items() + if field.get_value(self._initial_data) is not empty + and not field.read_only + ]) + + return OrderedDict([ (field.field_name, field.get_initial()) for field in self.fields.values() + if not field.read_only ]) def get_value(self, dictionary): @@ -197,18 +283,66 @@ class Serializer(BaseSerializer): return html.parse_html_dict(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) - def to_internal_value(self, data): + def run_validation(self, data=empty): """ - Dict of native values <- Dict of primitive datatypes. + We override the default `run_validation`, because the validation + performed by validators and the `.validate()` method should + be coerced into an error dictionary with a 'non_fields_error' key. """ + if data is empty: + if getattr(self.root, 'partial', False): + raise SkipField() + if self.required: + self.fail('required') + return self.get_default() + + if data is None: + if not self.allow_null: + self.fail('null') + return None + if not isinstance(data, dict): + message = self.error_messages['invalid'].format( + datatype=type(data).__name__ + ) raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data'] + api_settings.NON_FIELD_ERRORS_KEY: [message] }) - ret = {} - errors = {} - fields = [field for field in self.fields.values() if not field.read_only] + value = self.to_internal_value(data) + try: + self.run_validators(value) + value = self.validate(value) + assert value is not None, '.validate() should return the validated data' + except ValidationError as exc: + if isinstance(exc.detail, dict): + # .validate() errors may be a dict, in which case, use + # standard {key: list of values} style. + raise ValidationError(dict([ + (key, value if isinstance(value, list) else [value]) + for key, value in exc.detail.items() + ])) + elif isinstance(exc.detail, list): + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: exc.detail + }) + else: + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] + }) + + return value + + def to_internal_value(self, data): + """ + Dict of native values <- Dict of primitive datatypes. + """ + ret = OrderedDict() + errors = OrderedDict() + fields = [ + field for field in self.fields.values() + if (not field.read_only) or (field.default is not empty) + ] for field in fields: validate_method = getattr(self, 'validate_' + field.field_name, None) @@ -218,7 +352,7 @@ class Serializer(BaseSerializer): if validate_method is not None: validated_value = validate_method(validated_value) except ValidationError as exc: - errors[field.field_name] = exc.messages + errors[field.field_name] = exc.detail except SkipField: pass else: @@ -227,63 +361,90 @@ class Serializer(BaseSerializer): if errors: raise ValidationError(errors) - try: - return self.validate(ret) - except ValidationError as exc: - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: exc.messages - }) + return ret def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ - ret = SortedDict() + ret = OrderedDict() fields = [field for field in self.fields.values() if not field.write_only] for field in fields: - native_value = field.get_attribute(instance) - ret[field.field_name] = field.to_representation(native_value) + attribute = field.get_attribute(instance) + if attribute is None: + ret[field.field_name] = None + else: + ret[field.field_name] = field.to_representation(attribute) return ret def validate(self, attrs): return attrs + def __repr__(self): + return representation.serializer_repr(self, indent=1) + + # The following are used for accessing `BoundField` instances on the + # serializer, for the purposes of presenting a form-like API onto the + # field values and field errors. + def __iter__(self): - errors = self.errors if hasattr(self, '_errors') else {} for field in self.fields.values(): - value = self.data.get(field.field_name) if self.data else None - error = errors.get(field.field_name) - yield FieldResult(field, value, error) + yield self[field.field_name] - def __repr__(self): - return representation.serializer_repr(self, indent=1) + def __getitem__(self, key): + field = self.fields[key] + value = self.data.get(key) + error = self.errors.get(key) if hasattr(self, '_errors') else None + if isinstance(field, Serializer): + return NestedBoundField(field, value, error) + return BoundField(field, value, error) + # Include a backlink to the serializer class on return objects. + # Allows renderers such as HTMLFormRenderer to get the full field info. + + @property + def data(self): + ret = super(Serializer, self).data + return ReturnDict(ret, serializer=self) + + @property + def errors(self): + ret = super(Serializer, self).errors + return ReturnDict(ret, serializer=self) + + +# There's some replication of `ListField` here, +# but that's probably better than obfuscating the call hierarchy. class ListSerializer(BaseSerializer): child = None - initial = [] + many = True + + default_error_messages = { + 'not_a_list': _('Expected a list of items but got type `{input_type}`.') + } def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) assert self.child is not None, '`child` is a required argument.' - self.context = kwargs.pop('context', {}) - kwargs.pop('partial', None) - + assert not inspect.isclass(self.child), '`child` has not been instantiated.' super(ListSerializer, self).__init__(*args, **kwargs) - self.child.bind('', self, self) + self.child.bind(field_name='', parent=self) - def bind(self, field_name, parent, root): - # If the list is used as a field then it needs to provide - # the current context to the child serializer. - super(ListSerializer, self).bind(field_name, parent, root) - self.child.bind(field_name, self, root) + def get_initial(self): + if self._initial_data is not None: + return self.to_representation(self._initial_data) + return [] def get_value(self, dictionary): + """ + Given the input dictionary, return the field value. + """ # We override the default field access in order to support # lists in HTML forms. - if is_html_input(dictionary): + if html.is_html_input(dictionary): return html.parse_html_list(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) @@ -294,29 +455,107 @@ class ListSerializer(BaseSerializer): if html.is_html_input(data): data = html.parse_html_list(data) - return [self.child.run_validation(item) for item in data] + if not isinstance(data, list): + message = self.error_messages['not_a_list'].format( + input_type=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }) + + ret = [] + errors = [] + + for item in data: + try: + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret def to_representation(self, data): """ List of object instances -> List of dicts of primitive datatypes. """ - return [self.child.to_representation(item) for item in data] + iterable = data.all() if (hasattr(data, 'all')) else data + return [ + self.child.to_representation(item) for item in iterable + ] + + def update(self, instance, validated_data): + raise NotImplementedError( + "Serializers with many=True do not support multiple update by " + "default, only multiple create. For updates it is unclear how to " + "deal with insertions and deletions. If you need to support " + "multiple update, use a `ListSerializer` class and override " + "`.update()` so you can specify the behavior exactly." + ) - def create(self, attrs_list): - return [self.child.create(attrs) for attrs in attrs_list] + def create(self, validated_data): + return [ + self.child.create(attrs) for attrs in validated_data + ] + + def save(self, **kwargs): + """ + Save and return a list of object instances. + """ + validated_data = [ + dict(list(attrs.items()) + list(kwargs.items())) + for attrs in self.validated_data + ] - def save(self): if self.instance is not None: - self.update(self.instance, self.validated_data) - self.instance = self.create(self.validated_data) + self.instance = self.update(self.instance, validated_data) + assert self.instance is not None, ( + '`update()` did not return an object instance.' + ) + else: + self.instance = self.create(validated_data) + assert self.instance is not None, ( + '`create()` did not return an object instance.' + ) + return self.instance def __repr__(self): return representation.list_repr(self, indent=1) + # Include a backlink to the serializer class on return objects. + # Allows renderers such as HTMLFormRenderer to get the full field info. + + @property + def data(self): + ret = super(ListSerializer, self).data + return ReturnList(ret, serializer=self) + + @property + def errors(self): + ret = super(ListSerializer, self).errors + if isinstance(ret, dict): + return ReturnDict(ret, serializer=self) + return ReturnList(ret, serializer=self) + + +# ModelSerializer & HyperlinkedModelSerializer +# -------------------------------------------- class ModelSerializer(Serializer): - _field_mapping = { + """ + A `ModelSerializer` is just a regular `Serializer`, except that: + + * A set of default fields are automatically populated. + * A set of default validators are automatically populated. + * Default `.create()` and `.update()` implementations are provided. + """ + _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, models.BigIntegerField: IntegerField, models.BooleanField: BooleanField, @@ -331,7 +570,7 @@ class ModelSerializer(Serializer): models.FloatField: FloatField, models.ImageField: ImageField, models.IntegerField: IntegerField, - models.NullBooleanField: BooleanField, + models.NullBooleanField: NullBooleanField, models.PositiveIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, models.SlugField: SlugField, @@ -339,22 +578,35 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, - } + }) _related_class = PrimaryKeyRelatedField - def create(self, attrs): + def create(self, validated_attrs): + # Check that the user isn't trying to handle a writable nested field. + # If we don't do this explicitly they'd likely get a confusing + # error at the point of calling `Model.objects.create()`. + assert not any( + isinstance(field, BaseSerializer) and not field.read_only + for field in self.fields.values() + ), ( + 'The `.create()` method does not suport nested writable fields ' + 'by default. Write an explicit `.create()` method for serializer ' + '`%s.%s`, or set `read_only=True` on nested serializer fields.' % + (self.__class__.__module__, self.__class__.__name__) + ) + ModelClass = self.Meta.model - # Remove many-to-many relationships from attrs. + # Remove many-to-many relationships from validated_attrs. # They are not valid arguments to the default `.create()` method, # as they require that the instance has already been saved. info = model_meta.get_field_info(ModelClass) many_to_many = {} for field_name, relation_info in info.relations.items(): - if relation_info.to_many and (field_name in attrs): - many_to_many[field_name] = attrs.pop(field_name) + if relation_info.to_many and (field_name in validated_attrs): + many_to_many[field_name] = validated_attrs.pop(field_name) - instance = ModelClass.objects.create(**attrs) + instance = ModelClass.objects.create(**validated_attrs) # Save many-to-many relationships after the instance is created. if many_to_many: @@ -363,42 +615,185 @@ class ModelSerializer(Serializer): return instance - def update(self, obj, attrs): - for attr, value in attrs.items(): - setattr(obj, attr, value) - obj.save() + def update(self, instance, validated_attrs): + assert not any( + isinstance(field, BaseSerializer) and not field.read_only + for field in self.fields.values() + ), ( + 'The `.update()` method does not suport nested writable fields ' + 'by default. Write an explicit `.update()` method for serializer ' + '`%s.%s`, or set `read_only=True` on nested serializer fields.' % + (self.__class__.__module__, self.__class__.__name__) + ) + + for attr, value in validated_attrs.items(): + setattr(instance, attr, value) + instance.save() + return instance + + def get_validators(self): + field_names = set([ + field.source for field in self.fields.values() + if (field.source != '*') and ('.' not in field.source) + ]) - def _get_base_fields(self): + validators = getattr(getattr(self, 'Meta', None), 'validators', []) + model_class = self.Meta.model + + # 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) + + # 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 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) + + 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) + + return validators + + def get_fields(self): declared_fields = copy.deepcopy(self._declared_fields) - ret = SortedDict() + 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', {}) + 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 fields if none is supplied explicitly. + # 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: + fields.remove(field_name) + + # Determine the set of model fields, and the fields that they map to. + # We actually only need this to deal with the slightly awkward case + # of supporting `unique_for_date`/`unique_for_month`/`unique_for_year`. + model_field_mapping = {} + for field_name in fields: + if field_name in declared_fields: + field = declared_fields[field_name] + source = field.source or field_name + else: + try: + source = extra_kwargs[field_name]['source'] + except KeyError: + source = field_name + # Model fields will always have a simple source mapping, + # they can't be nested attribute lookups. + if '.' not in source and source != '*': + model_field_mapping[source] = field_name + + # Determine if we need any additional `HiddenField` or extra keyword + # arguments to deal with `unique_for` dates that are required to + # be in the input data in order to validate it. + 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 refered 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) + + # Now determine the fields that should be included on the serializer. for field_name in fields: if field_name in declared_fields: # Field is explicitly declared on the class, use that. ret[field_name] = declared_fields[field_name] continue - elif field_name == api_settings.URL_FIELD_NAME: - # Create the URL field. - field_cls = HyperlinkedIdentityField - kwargs = get_url_kwargs(model) - elif field_name in info.fields_and_pk: # Create regular model fields. model_field = info.fields_and_pk[field_name] - field_cls = lookup_class(self._field_mapping, model_field) + 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` @@ -409,6 +804,9 @@ class ModelSerializer(Serializer): # `ModelField`, which is used when no other typed field # matched to the model field. kwargs.pop('model_field', None) + if not issubclass(field_cls, CharField): + # `allow_blank` is only valid for textual fields. + kwargs.pop('allow_blank', None) elif field_name in info.relations: # Create forward and reverse relationships. @@ -428,6 +826,11 @@ class ModelSerializer(Serializer): 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`.' % @@ -446,13 +849,76 @@ class ModelSerializer(Serializer): ) # Populate any kwargs defined in `Meta.extra_kwargs` - kwargs.update(extra_kwargs.get(field_name, {})) + 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) # Create the serializer field. ret[field_name] = field_cls(**kwargs) + for field_name, field in hidden_fields.items(): + ret[field_name] = field + return ret + 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: + 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={<field_name>: {'write_only': True}}` instead.", + PendingDeprecationWarning, + stacklevel=3 + ) + for field_name in write_only_fields: + kwargs = extra_kwargs.get(field_name, {}) + kwargs['write_only'] = True + extra_kwargs[field_name] = kwargs + + view_name = getattr(self.Meta, 'view_name', None) + 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_default_field_names(self, declared_fields, model_info): return ( [model_info.pk.name] + @@ -470,6 +936,13 @@ class ModelSerializer(Serializer): class HyperlinkedModelSerializer(ModelSerializer): + """ + A type of `ModelSerializer` that uses hyperlinked relationships instead + of primary key relationships. Specifically: + + * A 'url' field is included instead of the 'id' field. + * Relationships to other instances are hyperlinks, instead of primary keys. + """ _related_class = HyperlinkedRelatedField def _get_default_field_names(self, declared_fields, model_info): |
