From 3f79a9a3d3e7692d90476f8a6907957b47aab821 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2013 22:39:45 +0000 Subject: one-one writable nested modelserializers --- rest_framework/serializers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6aca2f57..26c34044 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -753,7 +753,16 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - setattr(obj, accessor_name, related) + if related is None: + previous = getattr(obj, accessor_name, related) + if previous: + previous.delete() + elif isinstance(related, models.Model): + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) + else: + setattr(obj, accessor_name, related) del(obj._related_data) -- cgit v1.2.3 From d97e72cdb2f4fcc5aa2c19527a2b2ff11cf784bb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 25 Mar 2013 17:28:23 +0000 Subject: Cleanup one-one nested tests and implementation --- rest_framework/serializers.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 26c34044..668bcc49 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -667,9 +667,12 @@ class ModelSerializer(Serializer): cls = self.opts.model opts = get_concrete_model(cls)._meta exclusions = [field.name for field in opts.fields + opts.many_to_many] + for field_name, field in self.fields.items(): field_name = field.source or field_name - if field_name in exclusions and not field.read_only: + if field_name in exclusions \ + and not field.read_only \ + and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -695,6 +698,7 @@ class ModelSerializer(Serializer): """ m2m_data = {} related_data = {} + nested_forward_relations = {} meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -714,6 +718,12 @@ class ModelSerializer(Serializer): if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) + # Nested forward relations - These need to be marked so we can save + # them before saving the parent model instance. + for field_name in attrs.keys(): + if isinstance(self.fields.get(field_name, None), Serializer): + nested_forward_relations[field_name] = attrs[field_name] + # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -729,6 +739,7 @@ class ModelSerializer(Serializer): # at the point of save. instance._related_data = related_data instance._m2m_data = m2m_data + instance._nested_forward_relations = nested_forward_relations return instance @@ -744,6 +755,13 @@ class ModelSerializer(Serializer): """ Save the deserialized object and return it. """ + if getattr(obj, '_nested_forward_relations', None): + # Nested relationships need to be saved before we can save the + # parent instance. + for field_name, sub_object in obj._nested_forward_relations.items(): + self.save_object(sub_object) + setattr(obj, field_name, sub_object) + obj.save(**kwargs) if getattr(obj, '_m2m_data', None): @@ -753,15 +771,22 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - if related is None: - previous = getattr(obj, accessor_name, related) - if previous: - previous.delete() - elif isinstance(related, models.Model): + field = self.fields.get(accessor_name, None) + if isinstance(field, Serializer): + # TODO: Following will be needed for reverse FK + # if field.many: + # # Nested reverse fk relationship + # for related_item in related: + # fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + # setattr(related_item, fk_field, obj) + # self.save_object(related_item) + # else: + # Nested reverse one-one relationship fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name setattr(related, fk_field, obj) self.save_object(related) else: + # Reverse FK or reverse one-one setattr(obj, accessor_name, related) del(obj._related_data) -- cgit v1.2.3 From 73efa96de983fc644328d2fc498651aa917a2272 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sat, 6 Apr 2013 08:43:21 -0700 Subject: one-many writable nested modelserializer support --- rest_framework/serializers.py | 56 +++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 668bcc49..73cad00f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -130,14 +130,14 @@ class BaseSerializer(WritableField): def __init__(self, instance=None, data=None, files=None, context=None, partial=False, many=None, - allow_delete=False, **kwargs): + allow_add_remove=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial self.many = many - self.allow_delete = allow_delete + self.allow_add_remove = allow_add_remove self.context = context or {} @@ -154,8 +154,8 @@ class BaseSerializer(WritableField): if many and instance is not None and not hasattr(instance, '__iter__'): raise ValueError('instance should be a queryset or other iterable with many=True') - if allow_delete and not many: - raise ValueError('allow_delete should only be used for bulk updates, but you have not set many=True') + if allow_add_remove and not many: + raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True') ##### # Methods to determine which fields to use when (de)serializing objects. @@ -288,8 +288,15 @@ class BaseSerializer(WritableField): You should override this method to control how deserialized objects are instantiated. """ + removed_relations = [] + + # Deleted related objects + if self._deleted: + removed_relations = list(self._deleted) + if instance is not None: instance.update(attrs) + instance._removed_relations = removed_relations return instance return attrs @@ -377,6 +384,7 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj if value in (None, ''): into[(self.source or field_name)] = None @@ -386,7 +394,8 @@ class BaseSerializer(WritableField): 'data': value, 'context': self.context, 'partial': self.partial, - 'many': self.many + 'many': self.many, + 'allow_add_remove': self.allow_add_remove } serializer = self.__class__(**kwargs) @@ -496,6 +505,9 @@ class BaseSerializer(WritableField): def save_object(self, obj, **kwargs): obj.save(**kwargs) + if self.allow_add_remove and hasattr(obj, '_removed_relations'): + [self.delete_object(item) for item in obj._removed_relations] + def delete_object(self, obj): obj.delete() @@ -508,7 +520,7 @@ class BaseSerializer(WritableField): else: self.save_object(self.object, **kwargs) - if self.allow_delete and self._deleted: + if self.allow_add_remove and self._deleted: [self.delete_object(item) for item in self._deleted] return self.object @@ -699,6 +711,7 @@ class ModelSerializer(Serializer): m2m_data = {} related_data = {} nested_forward_relations = {} + removed_relations = [] meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -724,6 +737,10 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] + # Deleted related objects + if self._deleted: + removed_relations = list(self._deleted) + # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -740,6 +757,7 @@ class ModelSerializer(Serializer): instance._related_data = related_data instance._m2m_data = m2m_data instance._nested_forward_relations = nested_forward_relations + instance._removed_relations = removed_relations return instance @@ -764,6 +782,9 @@ class ModelSerializer(Serializer): obj.save(**kwargs) + if self.allow_add_remove and hasattr(obj, '_removed_relations'): + [self.delete_object(item) for item in obj._removed_relations] + if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): setattr(obj, accessor_name, object_list) @@ -773,18 +794,17 @@ class ModelSerializer(Serializer): for accessor_name, related in obj._related_data.items(): field = self.fields.get(accessor_name, None) if isinstance(field, Serializer): - # TODO: Following will be needed for reverse FK - # if field.many: - # # Nested reverse fk relationship - # for related_item in related: - # fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - # setattr(related_item, fk_field, obj) - # self.save_object(related_item) - # else: - # Nested reverse one-one relationship - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related, fk_field, obj) - self.save_object(related) + if field.many: + # Nested reverse fk relationship + for related_item in related: + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related_item, fk_field, obj) + self.save_object(related_item) + else: + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) else: # Reverse FK or reverse one-one setattr(obj, accessor_name, related) -- cgit v1.2.3 From fdc5cc3d81679d30cd20acf063dc7dc74ad17d7a Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Thu, 18 Apr 2013 10:28:20 -0700 Subject: Fix model serializer nestesd delete behavior --- rest_framework/serializers.py | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f0f11a4..78c45548 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,6 +20,9 @@ from rest_framework.relations import * from rest_framework.fields import * +class RelationsList(list): + _deleted = [] + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -149,7 +152,6 @@ class BaseSerializer(WritableField): self._data = None self._files = None self._errors = None - self._deleted = None if many and instance is not None and not hasattr(instance, '__iter__'): raise ValueError('instance should be a queryset or other iterable with many=True') @@ -288,15 +290,8 @@ class BaseSerializer(WritableField): You should override this method to control how deserialized objects are instantiated. """ - removed_relations = [] - - # Deleted related objects - if self._deleted: - removed_relations = list(self._deleted) - if instance is not None: instance.update(attrs) - instance._removed_relations = removed_relations return instance return attrs @@ -438,7 +433,7 @@ class BaseSerializer(WritableField): PendingDeprecationWarning, stacklevel=3) if many: - ret = [] + ret = RelationsList() errors = [] update = self.object is not None @@ -466,7 +461,7 @@ class BaseSerializer(WritableField): errors.append(self._errors) if update: - self._deleted = identity_to_objects.values() + ret._deleted = identity_to_objects.values() self._errors = any(errors) and errors or [] else: @@ -509,9 +504,6 @@ class BaseSerializer(WritableField): def save_object(self, obj, **kwargs): obj.save(**kwargs) - if self.allow_add_remove and hasattr(obj, '_removed_relations'): - [self.delete_object(item) for item in obj._removed_relations] - def delete_object(self, obj): obj.delete() @@ -521,11 +513,11 @@ class BaseSerializer(WritableField): """ if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] - else: - self.save_object(self.object, **kwargs) - if self.allow_add_remove and self._deleted: - [self.delete_object(item) for item in self._deleted] + if self.allow_add_remove and self.object._deleted: + [self.delete_object(item) for item in self.object._deleted] + else: + self.save_object(self.object, **kwargs) return self.object @@ -715,7 +707,6 @@ class ModelSerializer(Serializer): m2m_data = {} related_data = {} nested_forward_relations = {} - removed_relations = [] meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -741,10 +732,6 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] - # Deleted related objects - if self._deleted: - removed_relations = list(self._deleted) - # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -761,7 +748,6 @@ class ModelSerializer(Serializer): instance._related_data = related_data instance._m2m_data = m2m_data instance._nested_forward_relations = nested_forward_relations - instance._removed_relations = removed_relations return instance @@ -786,9 +772,6 @@ class ModelSerializer(Serializer): obj.save(**kwargs) - if self.allow_add_remove and hasattr(obj, '_removed_relations'): - [self.delete_object(item) for item in obj._removed_relations] - if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): setattr(obj, accessor_name, object_list) @@ -804,6 +787,11 @@ class ModelSerializer(Serializer): fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) + + # Delete any removed objects + if field.allow_add_remove and related._deleted: + [self.delete_object(item) for item in related._deleted] + else: # Nested reverse one-one relationship fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name -- cgit v1.2.3 From 7e0a93f0eefead25f0e9b6615675f394af3a4ba0 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 19 Apr 2013 10:46:57 -0700 Subject: Don't use field when saving related data --- rest_framework/serializers.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 78c45548..b39cb810 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -460,7 +460,7 @@ class BaseSerializer(WritableField): ret.append(self.from_native(item, None)) errors.append(self._errors) - if update: + if update and self.allow_add_remove: ret._deleted = identity_to_objects.values() self._errors = any(errors) and errors or [] @@ -514,7 +514,7 @@ class BaseSerializer(WritableField): if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] - if self.allow_add_remove and self.object._deleted: + if self.object._deleted: [self.delete_object(item) for item in self.object._deleted] else: self.save_object(self.object, **kwargs) @@ -779,24 +779,22 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - field = self.fields.get(accessor_name, None) - if isinstance(field, Serializer): - if field.many: - # Nested reverse fk relationship - for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related_item, fk_field, obj) - self.save_object(related_item) - - # Delete any removed objects - if field.allow_add_remove and related._deleted: - [self.delete_object(item) for item in related._deleted] - - else: - # Nested reverse one-one relationship + if isinstance(related, RelationsList): + # Nested reverse fk relationship + for related_item in related: fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related, fk_field, obj) - self.save_object(related) + setattr(related_item, fk_field, obj) + self.save_object(related_item) + + # Delete any removed objects + if related._deleted: + [self.delete_object(item) for item in related._deleted] + + elif isinstance(related, models.Model): + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) else: # Reverse FK or reverse one-one setattr(obj, accessor_name, related) -- cgit v1.2.3 From 3fcc01273c5efef26d911e50c02a4a43f89b34eb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 20:29:52 +0100 Subject: Remove deprecated code --- rest_framework/serializers.py | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 023f7ccf..ae39cce8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,7 +15,6 @@ import copy import datetime import types from decimal import Decimal -from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict @@ -141,7 +140,7 @@ class BaseSerializer(WritableField): _dict_class = SortedDictWithMetadata def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, many=None, + context=None, partial=False, many=False, allow_add_remove=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) @@ -348,12 +347,7 @@ class BaseSerializer(WritableField): if value is None: return None - if self.many is not None: - many = self.many - else: - many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type)) - - if many: + if self.many: return [self.to_native(item) for item in value] return self.to_native(value) @@ -424,16 +418,7 @@ class BaseSerializer(WritableField): if self._errors is None: data, files = self.init_data, self.init_files - if self.many is not None: - many = self.many - else: - many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) - if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=3) - - if many: + if self.many: ret = [] errors = [] update = self.object is not None @@ -486,16 +471,7 @@ class BaseSerializer(WritableField): if self._data is None: obj = self.object - if self.many is not None: - many = self.many - else: - many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) - if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=2) - - if many: + if self.many: self._data = [self.to_native(item) for item in obj] else: self._data = self.to_native(obj) -- cgit v1.2.3 From 379ad8a82485e61b180ee823ba49799d39446aeb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 20:36:14 +0100 Subject: pending deprecations -> deprecated --- rest_framework/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ae39cce8..dd9e14ad 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -593,10 +593,10 @@ class ModelSerializer(Serializer): if len(inspect.getargspec(self.get_nested_field).args) == 2: warnings.warn( 'The `get_nested_field(model_field)` call signature ' - 'is due to be deprecated. ' + 'is deprecated. ' 'Use `get_nested_field(model_field, related_model, ' 'to_many) instead', - PendingDeprecationWarning + DeprecationWarning ) field = self.get_nested_field(model_field) else: @@ -605,10 +605,10 @@ class ModelSerializer(Serializer): if len(inspect.getargspec(self.get_nested_field).args) == 3: warnings.warn( 'The `get_related_field(model_field, to_many)` call ' - 'signature is due to be deprecated. ' + 'signature is deprecated. ' 'Use `get_related_field(model_field, related_model, ' 'to_many) instead', - PendingDeprecationWarning + DeprecationWarning ) field = self.get_related_field(model_field, to_many=to_many) else: -- cgit v1.2.3 From edd696292ca50cb9c10b50b0124a79135d007ea4 Mon Sep 17 00:00:00 2001 From: Dan Stephenson Date: Sat, 10 Aug 2013 01:12:36 +0100 Subject: Spelling correction on read_only_fields err msg --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 682a99a4..25825df4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -683,7 +683,7 @@ class ModelSerializer(Serializer): # in the `read_only_fields` option for field_name in self.opts.read_only_fields: assert field_name not in self.base_fields.keys(), \ - "field '%s' on serializer '%s' specfied in " \ + "field '%s' on serializer '%s' specified in " \ "`read_only_fields`, but also added " \ "as an explict field. Remove it from `read_only_fields`." % \ (field_name, self.__class__.__name__) -- cgit v1.2.3 From bbdcbe945248c5448540a1ab746b8a4b8bc8f95b Mon Sep 17 00:00:00 2001 From: Dan Stephenson Date: Sat, 10 Aug 2013 01:22:47 +0100 Subject: Spelling correction on read_only_fields err msg just spotted extras--- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 682a99a4..9e85cb46 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -685,10 +685,10 @@ class ModelSerializer(Serializer): assert field_name not in self.base_fields.keys(), \ "field '%s' on serializer '%s' specfied in " \ "`read_only_fields`, but also added " \ - "as an explict field. Remove it from `read_only_fields`." % \ + "as an explicit field. Remove it from `read_only_fields`." % \ (field_name, self.__class__.__name__) assert field_name in ret, \ - "Noexistant field '%s' specified in `read_only_fields` " \ + "Non-existant field '%s' specified in `read_only_fields` " \ "on serializer '%s'." % \ (field_name, self.__class__.__name__) ret[field_name].read_only = True -- cgit v1.2.3 From ff1efcf60f0a9b66cdb736f8c0b2cfe2fc84cdf5 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 12 Aug 2013 18:08:23 +0300 Subject: If null or blank - don't save the nested object --- rest_framework/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d8f9145e..2b260c25 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -522,7 +522,7 @@ class BaseSerializer(WritableField): if self.object._deleted: [self.delete_object(item) for item in self.object._deleted] else: - self.save_object(self.object, **kwargs) + self.save_object(self.object, **kwargs) return self.object @@ -891,7 +891,8 @@ class ModelSerializer(Serializer): # Nested relationships need to be saved before we can save the # parent instance. for field_name, sub_object in obj._nested_forward_relations.items(): - self.save_object(sub_object) + if sub_object: + self.save_object(sub_object) setattr(obj, field_name, sub_object) obj.save(**kwargs) -- cgit v1.2.3 From e03854ba6a74428675c40d469a7768cc5131035f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 14:06:14 +0100 Subject: Tweaks to display nested data in empty serializers --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2b260c25..22525964 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -338,9 +338,9 @@ class BaseSerializer(WritableField): value = obj for component in source.split('.'): - value = get_component(value, component) if value is None: - break + return self.to_native(None) + value = get_component(value, component) except ObjectDoesNotExist: return None -- cgit v1.2.3 From 10d386ec6a4822402b5ffea46bdd9e7d72db519b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 16:10:20 +0100 Subject: Cleanup and dealing with empty form data. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fde06d83..97e0a005 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -300,7 +300,8 @@ class BaseSerializer(WritableField): Serialize objects -> primitives. """ ret = self._dict_class() - ret.fields = {} + ret.fields = self._dict_class() + ret.empty = obj is None for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) -- cgit v1.2.3 From 9d3fae27fd9c3236dfd9c26ae9b830deb6fa4e9b Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Fri, 23 Aug 2013 16:48:32 +0000 Subject: parameterize identity field class to allow for easier subclassing --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa344..abb96941 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -903,6 +903,7 @@ class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' _hyperlink_field_class = HyperlinkedRelatedField + _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() @@ -911,7 +912,7 @@ class HyperlinkedModelSerializer(ModelSerializer): self.opts.view_name = self._get_default_view_name(self.opts.model) if 'url' not in fields: - url_field = HyperlinkedIdentityField( + url_field = self._hyperlink_identify_field_class( view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) -- cgit v1.2.3 From 8d590ebfded0968e458f8e3a87efabec8384586e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 11:22:19 +0100 Subject: First hacky pass at displaying raw data --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index abff6898..202d3a09 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,6 +304,8 @@ class BaseSerializer(WritableField): ret.empty = obj is None for field_name, field in self.fields.items(): + if obj is None and field.read_only: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) -- cgit v1.2.3 From 18007d68464b0cfab970e2a60aed0d41c4de4dac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 21:52:56 +0100 Subject: Simplifying raw data renderering support --- rest_framework/serializers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 202d3a09..abff6898 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,8 +304,6 @@ class BaseSerializer(WritableField): ret.empty = obj is None for field_name, field in self.fields.items(): - if obj is None and field.read_only: - continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) -- cgit v1.2.3 From 94cd1369437f84adefb46462439b46dc5208ab1d Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 29 Aug 2013 17:35:15 +1200 Subject: add transform_ methods to serializers, which basically do the opposite of validate_ on a per-field basis. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa344..8ba1b195 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,6 +304,9 @@ class BaseSerializer(WritableField): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) + method = getattr(self, 'transform_%s' % field_name, None) + if callable(method): + value = method(obj, value) ret[key] = value ret.fields[key] = field return ret -- cgit v1.2.3 From 3fba60e99c75dda4e14f7fe4f941d6fc84e4c986 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Aug 2013 09:02:54 +0100 Subject: Drop broken placeholder serializations. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index abff6898..a63c7f6c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -334,13 +334,14 @@ class BaseSerializer(WritableField): if self.source == '*': return self.to_native(obj) + # Get the raw field value try: source = self.source or field_name value = obj for component in source.split('.'): if value is None: - return self.to_native(None) + break value = get_component(value, component) except ObjectDoesNotExist: return None -- cgit v1.2.3 From f07a4f4ca3812fbe45d698e4ba0f9ff9099b6887 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:10:16 +0200 Subject: Clear cached serializer data on `save()` + test. Fixes #1116. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c..8d2e0feb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -518,6 +518,9 @@ class BaseSerializer(WritableField): """ Save the deserialized object and return it. """ + # Clear cached _data, which may be invalidated by `save()` + self._data = None + if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] -- cgit v1.2.3 From e441f85109e64345a12e65062fc0e51c5787e67f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Sep 2013 10:30:04 +0100 Subject: Drop 1.3 support --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f1775762..9e3881a2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -18,7 +18,7 @@ from decimal import Decimal from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict -from rest_framework.compat import get_concrete_model, six +from rest_framework.compat import six # Note: We do the following so that users of the framework can use this style: # @@ -575,7 +575,7 @@ class ModelSerializer(Serializer): cls = self.opts.model assert cls is not None, \ "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ - opts = get_concrete_model(cls)._meta + opts = cls._meta.concrete_model._meta ret = SortedDict() nested = bool(self.opts.depth) @@ -784,7 +784,7 @@ class ModelSerializer(Serializer): Return a list of field names to exclude from model validation. """ cls = self.opts.model - opts = get_concrete_model(cls)._meta + opts = cls._meta.concrete_model._meta exclusions = [field.name for field in opts.fields + opts.many_to_many] for field_name, field in self.fields.items(): -- cgit v1.2.3 From a14f1e886402b8d0f742fdbb912b03a4004e1735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 13:45:35 +0100 Subject: Serializers can now be rendered directly to HTML --- rest_framework/serializers.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8d2e0feb..206a8123 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -32,6 +32,13 @@ from rest_framework.relations import * from rest_framework.fields import * +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return '' + return name.replace('_', ' ').capitalize() + + class RelationsList(list): _deleted = [] @@ -306,7 +313,17 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) + if self._errors: + value = self.init_data.get(field_name) + else: + value = field.field_to_native(obj, field_name) + + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = value + if not field.label: + field.label = pretty_name(key) + ret[key] = value ret.fields[key] = field return ret -- cgit v1.2.3 From 8d4ba478cc5725b4de6ab86b4825b1ea94cb4c7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 16:13:34 +0100 Subject: Fix rendering of forms and add error rendering on HTML form --- rest_framework/serializers.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 206a8123..bc9f73d1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -308,24 +308,14 @@ class BaseSerializer(WritableField): """ ret = self._dict_class() ret.fields = self._dict_class() - ret.empty = obj is None for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - if self._errors: - value = self.init_data.get(field_name) - else: - value = field.field_to_native(obj, field_name) - - field._errors = self._errors.get(key) if self._errors else None - field._name = field_name - field._value = value - if not field.label: - field.label = pretty_name(key) - + value = field.field_to_native(obj, field_name) ret[key] = value - ret.fields[key] = field + ret.fields[key] = self.augment_field(field, field_name, key, value) + return ret def from_native(self, data, files): @@ -333,6 +323,7 @@ class BaseSerializer(WritableField): Deserialize primitives -> objects. """ self._errors = {} + if data is not None or files is not None: attrs = self.restore_fields(data, files) if attrs is not None: @@ -343,6 +334,15 @@ class BaseSerializer(WritableField): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + def field_to_native(self, obj, field_name): """ Override default so that the serializer can be used as a nested field -- cgit v1.2.3 From 9e29c6389529210978d58cee78e437b901f9daa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Oct 2013 17:33:22 +0100 Subject: Ensure read-only fields don't break with current HTML renderer behavior --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bc9f73d1..8e945688 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -310,6 +310,8 @@ class BaseSerializer(WritableField): ret.fields = self._dict_class() for field_name, field in self.fields.items(): + if field.read_only and obj is None: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) -- cgit v1.2.3 From 7c3769f04b5ec2cd14dcbd7e3601d59092255906 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:31:55 +1300 Subject: fix writing into foreign key with non-null source --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee..fa5ac143 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -403,7 +403,7 @@ class BaseSerializer(WritableField): return # Set the serializer object if it exists - obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj if self.source == '*': -- cgit v1.2.3 From c6be12f02b5e07e412c8c91b368566a85364b907 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 18:03:52 -0700 Subject: [Fix]: Error with partial=True and validate_ The error occurs when serializer is set with partial=True and a field-level validation is defined on a field, for which there's no corresponding update value in .data --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c..0b5ae042 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -255,10 +255,13 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field_name in self._errors: continue + + source = field.source or field_name + if self.partial and source not in attrs: + continue try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: - source = field.source or field_name attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) -- cgit v1.2.3 From cc3c16eaa09c7dc63592ae8bf4ee30f1af263be1 Mon Sep 17 00:00:00 2001 From: Bruno Renié Date: Mon, 14 Oct 2013 16:28:32 +0200 Subject: Fix a docstring to reflect what the method does --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee..6801e24d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -912,7 +912,7 @@ class ModelSerializer(Serializer): def save_object(self, obj, **kwargs): """ - Save the deserialized object and return it. + Save the deserialized object. """ if getattr(obj, '_nested_forward_relations', None): # Nested relationships need to be saved before we can save the -- cgit v1.2.3 From 17a00be830a3b8861be5014e94850f7a8f4ecd25 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Fri, 18 Oct 2013 21:13:20 -0400 Subject: This fix results in models.NullBooleanField rendering as a checkbox in the browsable API --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058..f30f1fd8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -606,6 +606,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, models.FileField: FileField, models.ImageField: ImageField, } -- cgit v1.2.3 From 53258908210b1eabd0ee204653a589d6579ac772 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 5 Nov 2013 17:21:18 +0100 Subject: Improve handling of 'empty' values for ChoiceField The empty value defaults back to '' (for backwards-compatibility) but is changed automatically to None for ModelSerializers if the `null` property is set on the db field. --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058..5240dbf6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -794,6 +794,8 @@ class ModelSerializer(Serializer): # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices kwargs['choices'] = model_field.flatchoices + if model_field.null: + kwargs['empty'] = None return ChoiceField(**kwargs) # put this below the ChoiceField because min_value isn't a valid initializer -- cgit v1.2.3 From d4a50429b098656e7a0855c6acf12f0aa4bc434f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 8 Nov 2013 13:12:40 +0100 Subject: Fixed a regression with ValidationError under Django 1.6 --- rest_framework/serializers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5240dbf6..7cdb55c8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -42,6 +42,7 @@ def pretty_name(name): class RelationsList(list): _deleted = [] + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -56,9 +57,13 @@ class NestedValidationError(ValidationError): def __init__(self, message): if isinstance(message, dict): - self.messages = [message] + self._messages = [message] else: - self.messages = message + self._messages = message + + @property + def messages(self): + return self._messages class DictWithMetadata(dict): -- cgit v1.2.3 From b7b57adee2cc5a785a5df492424969c8ba311aa8 Mon Sep 17 00:00:00 2001 From: Ben Pietravalle Date: Fri, 8 Nov 2013 13:19:40 +0000 Subject: Fix object creation with reverse M2M when related_name unspecified It seems that field.related_query_name() does not return the related_name for reverse M2M relations when related_name is not explicitly set in the M2M field definition. So, change to use obj.get_accessor_name(), where obj is an instance of RelatedObject, as are returned by a model's _meta.get_all_related_many_to_many_objects(), or as in the tuples returned by _meta.get_all_m2m_objects_with_model(). --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5240dbf6..244fbe63 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -873,7 +873,7 @@ class ModelSerializer(Serializer): # Reverse m2m relations for (obj, model) in meta.get_all_related_m2m_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: m2m_data[field_name] = attrs.pop(field_name) -- cgit v1.2.3 From fd2c291c4d9243937a31e0e6f523016067824b83 Mon Sep 17 00:00:00 2001 From: Doğan Çeçen Date: Mon, 11 Nov 2013 11:54:30 +0200 Subject: Typo on api-guide/fields.md and serializers.py --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 16095452..163abf4f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -6,8 +6,8 @@ form encoded input. Serialization in REST framework is a two-phase process: 1. Serializers marshal between complex types like model instances, and -python primatives. -2. The process of marshalling between python primatives and request and +python primitives. +2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ from __future__ import unicode_literals -- cgit v1.2.3 From ad7aa8fe485580e1bdff53d39fe3ea282d908a04 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 17 Nov 2013 01:27:16 +0100 Subject: Fixed the nested model serializers in case of the related_name isn’t set. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f..e20e582e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -872,7 +872,7 @@ class ModelSerializer(Serializer): # Reverse fk or one-to-one relations for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: related_data[field_name] = attrs.pop(field_name) -- cgit v1.2.3 From a8b15f4290f4bad17d0dd599b8d5c29c155b89e5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 15:06:39 +0100 Subject: Another fix for nested writable serializers in case of the related_name isn’t set on the ForeignKey. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e20e582e..5059c71b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -940,11 +940,12 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): + related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + fk_field = related_fields[accessor_name].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) -- cgit v1.2.3 From cf6c11bd4b7e7fdaa1de659d69792030e565412a Mon Sep 17 00:00:00 2001 From: Chuck Harmston Date: Fri, 6 Dec 2013 14:00:23 -0600 Subject: Raise appropriate error in serializer when making a partial update to set a required RelatedField to null (issue #1158) --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f..44e4b04b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -896,7 +896,10 @@ class ModelSerializer(Serializer): # Update an existing instance... if instance is not None: for key, val in attrs.items(): - setattr(instance, key, val) + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # ...or create a new instance else: -- cgit v1.2.3 From 3a1c40f81488c241cb64860d6cc510f8e71c0c40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:46:44 +0000 Subject: Refine model manager behavior so as not to use the behavior in incorrect cases. Closes #1205 --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 44e4b04b..0d35fb32 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -412,7 +412,13 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() if self.source == '*': if value: -- cgit v1.2.3 From 0453cbd56bf5c553412b61a2a5e5522a2d44a419 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 11:09:54 +0000 Subject: Clean up implementation --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34f31531..40caa1f3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -949,7 +949,11 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): - related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship -- cgit v1.2.3 From 4a134eefa2c3b71aa1dc6a4ec94716fe41dca8f5 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Mon, 16 Dec 2013 15:51:43 +1300 Subject: Fix expansion of writable nested serializers where the inner fields have source set. --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 40caa1f3..d9313342 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -422,7 +422,9 @@ class BaseSerializer(WritableField): if self.source == '*': if value: - into.update(value) + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None -- cgit v1.2.3 From a439c80cd856f0661cb66003263c454c5108fe0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:21:53 +0000 Subject: Less brittle through relationship testing. Closes #1292. --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 28a09880..8351b3df 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -713,7 +713,9 @@ class ModelSerializer(Serializer): is_m2m = isinstance(relation.field, models.fields.related.ManyToManyField) - if is_m2m and not relation.field.rel.through._meta.auto_created: + if (is_m2m and + hasattr(relation.field.rel, 'through') and + not relation.field.rel.through._meta.auto_created): has_through_model = True if nested: -- cgit v1.2.3 From d30ce2575c6b3901f15eb96eeaf66cc65e1d298b Mon Sep 17 00:00:00 2001 From: S. Andrew Sheppard Date: Mon, 23 Dec 2013 22:31:31 -0600 Subject: fix for genericrelation saving --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8351b3df..c0c810ab 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -894,7 +894,7 @@ class ModelSerializer(Serializer): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many: + for field in meta.many_to_many + meta.virtual_fields: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) -- cgit v1.2.3 From e020c51b44b9acecdb01cc90f2d6da977ba5ea0f Mon Sep 17 00:00:00 2001 From: Steven Cummings Date: Thu, 2 Jan 2014 17:18:08 -0600 Subject: FIX BaseSerializer.from_native has an altered signature * base classes define it with one parameter * BaseSerializer currently defines a second parameter, which we make optional here for method-dispatch passivity --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c0c810ab..b22ca578 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -331,7 +331,7 @@ class BaseSerializer(WritableField): return ret - def from_native(self, data, files): + def from_native(self, data, files=None): """ Deserialize primitives -> objects. """ -- cgit v1.2.3 From e1bbe9d514c95aba596cff64292eb0f0bc7d99fa Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 6 Jan 2014 13:56:57 +0200 Subject: Set `allow_none = True` for CharFields with null=True --- rest_framework/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fa935306..0164965c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -821,10 +821,15 @@ class ModelSerializer(Serializer): kwargs.update({attribute: getattr(model_field, attribute)}) try: - return self.field_mapping[model_field.__class__](**kwargs) + field_class = self.field_mapping[model_field.__class__] except KeyError: return ModelField(model_field=model_field, **kwargs) + if issubclass(field_class, CharField) and model_field.null: + kwargs['allow_none'] = True + + return field_class(**kwargs) + def get_validation_exclusions(self): """ Return a list of field names to exclude from model validation. -- cgit v1.2.3 From cd9a4194ea4f4dc0e43a34485cd8a27eba44a39a Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Sun, 12 Jan 2014 16:30:26 +0200 Subject: Check the modelfield's class instead --- rest_framework/serializers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0164965c..cbf73fc3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -804,6 +804,10 @@ class ModelSerializer(Serializer): issubclass(model_field.__class__, models.PositiveSmallIntegerField): kwargs['min_value'] = 0 + if model_field.null and \ + issubclass(model_field.__class__, (models.CharField, models.TextField)): + kwargs['allow_none'] = True + attribute_dict = { models.CharField: ['max_length'], models.CommaSeparatedIntegerField: ['max_length'], @@ -821,15 +825,10 @@ class ModelSerializer(Serializer): kwargs.update({attribute: getattr(model_field, attribute)}) try: - field_class = self.field_mapping[model_field.__class__] + return self.field_mapping[model_field.__class__](**kwargs) except KeyError: return ModelField(model_field=model_field, **kwargs) - if issubclass(field_class, CharField) and model_field.null: - kwargs['allow_none'] = True - - return field_class(**kwargs) - def get_validation_exclusions(self): """ Return a list of field names to exclude from model validation. -- cgit v1.2.3 From 2332382b5109939238801e7d4c018455d159fe91 Mon Sep 17 00:00:00 2001 From: Dustin Farris Date: Sun, 12 Jan 2014 20:28:19 -0500 Subject: Add a sanity check to avoid running into unresolved related models. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b22ca578..6b31c304 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,6 +20,7 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model, six +from rest_framework.models import resolve_model # Note: We do the following so that users of the framework can use this style: # @@ -656,7 +657,7 @@ class ModelSerializer(Serializer): if model_field.rel: to_many = isinstance(model_field, models.fields.related.ManyToManyField) - related_model = model_field.rel.to + related_model = resolve_model(model_field.rel.to) if to_many and not model_field.rel.through._meta.auto_created: has_through_model = True -- cgit v1.2.3 From 65858428fd7498c1783af0ec13de51521fe29c63 Mon Sep 17 00:00:00 2001 From: S.Prymak Date: Mon, 13 Jan 2014 16:35:20 +0200 Subject: Make HyperlinkedModelSerializer `url` field name customizable --- rest_framework/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b22ca578..0b5c6a05 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -990,6 +990,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) self.lookup_field = getattr(meta, 'lookup_field', None) + self.url_field_name = getattr(meta, 'url_field_name', 'url') class HyperlinkedModelSerializer(ModelSerializer): @@ -1008,13 +1009,13 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - if 'url' not in fields: + if self.opts.url_field_name not in fields: url_field = self._hyperlink_identify_field_class( view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) ret = self._dict_class() - ret['url'] = url_field + ret[self.opts.url_field_name] = url_field ret.update(fields) fields = ret @@ -1050,7 +1051,7 @@ class HyperlinkedModelSerializer(ModelSerializer): We need to override the default, to use the url as the identity. """ try: - return data.get('url', None) + return data.get(self.opts.url_field_name, None) except AttributeError: return None -- cgit v1.2.3 From b1b58762a3d84ac4cdc6553e8ed06983fd3502ca Mon Sep 17 00:00:00 2001 From: Dustin Farris Date: Mon, 13 Jan 2014 11:47:44 -0500 Subject: Move models.resolve_model to serializers._resolve_model --- rest_framework/serializers.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6b31c304..0ea2cadb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,14 +13,15 @@ response content is handled by parsers and renderers. from __future__ import unicode_literals import copy import datetime +import inspect import types from decimal import Decimal +from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model, six -from rest_framework.models import resolve_model # Note: We do the following so that users of the framework can use this style: # @@ -33,6 +34,27 @@ from rest_framework.relations import * from rest_framework.fields import * +def _resolve_model(obj): + """ + Resolve supplied `obj` to a Django model class. + + `obj` must be a Django model class itself, or a string + representation of one. Useful in situtations like GH #1225 where + Django may not have resolved a string-based reference to a model in + another model's foreign key definition. + + String representations should have the format: + 'appname.ModelName' + """ + if type(obj) == str and len(obj.split('.')) == 2: + app_name, model_name = obj.split('.') + return models.get_model(app_name, model_name) + elif inspect.isclass(obj) and issubclass(obj, models.Model): + return obj + else: + raise ValueError("{0} is not a Django model".format(obj)) + + def pretty_name(name): """Converts 'first_name' to 'First name'""" if not name: @@ -657,7 +679,10 @@ class ModelSerializer(Serializer): if model_field.rel: to_many = isinstance(model_field, models.fields.related.ManyToManyField) - related_model = resolve_model(model_field.rel.to) + try: + related_model = _resolve_model(model_field.rel.to) + except ValueError as error_message: + raise ImproperlyConfigured(error_message) if to_many and not model_field.rel.through._meta.auto_created: has_through_model = True -- cgit v1.2.3 From bc6c5df109a35bf76be662a47d9c88a2a3b82351 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 13 Jan 2014 17:39:22 +0000 Subject: Minor tweaks --- rest_framework/serializers.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0ea2cadb..b4087e54 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -16,7 +16,6 @@ import datetime import inspect import types from decimal import Decimal -from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page from django.db import models from django.forms import widgets @@ -679,10 +678,7 @@ class ModelSerializer(Serializer): if model_field.rel: to_many = isinstance(model_field, models.fields.related.ManyToManyField) - try: - related_model = _resolve_model(model_field.rel.to) - except ValueError as error_message: - raise ImproperlyConfigured(error_message) + related_model = _resolve_model(model_field.rel.to) if to_many and not model_field.rel.through._meta.auto_created: has_through_model = True -- cgit v1.2.3 From e9b0d70f5d8eb94d72da677846d5dab5b587e945 Mon Sep 17 00:00:00 2001 From: S.Prymak Date: Tue, 14 Jan 2014 10:38:49 +0200 Subject: Override HyperlinkedModelSerializerOptions url field name with URL_FIELD_NAME setting --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0b5c6a05..73c407c7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,6 +20,8 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model, six +from rest_framework.settings import api_settings + # Note: We do the following so that users of the framework can use this style: # @@ -990,7 +992,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) self.lookup_field = getattr(meta, 'lookup_field', None) - self.url_field_name = getattr(meta, 'url_field_name', 'url') + self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME) class HyperlinkedModelSerializer(ModelSerializer): -- cgit v1.2.3 From 85d74fc86a934309359a437dd487193013055977 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 14 Jan 2014 11:25:44 +0000 Subject: Added write_only and write_only_fields. Refs #1306 --- rest_framework/serializers.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b4087e54..9f047b03 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -344,7 +344,10 @@ class BaseSerializer(WritableField): continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) + try: + value = field.field_to_native(obj, field_name) + except IgnoreFieldException: + continue method = getattr(self, 'transform_%s' % field_name, None) if callable(method): value = method(obj, value) @@ -383,6 +386,9 @@ class BaseSerializer(WritableField): Override default so that the serializer can be used as a nested field across relationships. """ + if self.write_only: + raise IgnoreFieldException() + if self.source == '*': return self.to_native(obj) @@ -615,6 +621,7 @@ class ModelSerializerOptions(SerializerOptions): super(ModelSerializerOptions, self).__init__(meta) self.model = getattr(meta, 'model', None) self.read_only_fields = getattr(meta, 'read_only_fields', ()) + self.write_only_fields = getattr(meta, 'write_only_fields', ()) class ModelSerializer(Serializer): @@ -754,17 +761,29 @@ class ModelSerializer(Serializer): # Add the `read_only` flag to any fields that have bee specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: - assert field_name not in self.base_fields.keys(), \ - "field '%s' on serializer '%s' specified in " \ - "`read_only_fields`, but also added " \ - "as an explicit field. Remove it from `read_only_fields`." % \ - (field_name, self.__class__.__name__) - assert field_name in ret, \ - "Non-existant field '%s' specified in `read_only_fields` " \ - "on serializer '%s'." % \ - (field_name, self.__class__.__name__) + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`read_only_fields`, but also added " + "as an explicit field. Remove it from `read_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `read_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) ret[field_name].read_only = True + for field_name in self.opts.write_only_fields: + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`write_only_fields`, but also added " + "as an explicit field. Remove it from `write_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `write_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) + ret[field_name].write_only = True + return ret def get_pk_field(self, model_field): -- cgit v1.2.3 From e9fda70b4ac86badbd5297f857126121472b7ec6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 15 Jan 2014 08:53:23 +0000 Subject: Nicer write_only fields implementation. Closes #1355 --- rest_framework/serializers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 414a769f..536b040b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -346,14 +346,12 @@ class BaseSerializer(WritableField): continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - try: - value = field.field_to_native(obj, field_name) - except IgnoreFieldException: - continue + value = field.field_to_native(obj, field_name) method = getattr(self, 'transform_%s' % field_name, None) if callable(method): value = method(obj, value) - ret[key] = value + if not getattr(field, 'write_only', False): + ret[key] = value ret.fields[key] = self.augment_field(field, field_name, key, value) return ret @@ -389,7 +387,7 @@ class BaseSerializer(WritableField): across relationships. """ if self.write_only: - raise IgnoreFieldException() + return None if self.source == '*': return self.to_native(obj) -- cgit v1.2.3 From b182b9e246fb0c74801bea46b0d82e7384451165 Mon Sep 17 00:00:00 2001 From: juroe Date: Tue, 4 Feb 2014 11:56:41 +0100 Subject: Fixes typo (Implicit instead of Implict). --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 536b040b..38b5089a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -501,7 +501,7 @@ class BaseSerializer(WritableField): else: many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=3) @@ -563,7 +563,7 @@ class BaseSerializer(WritableField): else: many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=2) -- cgit v1.2.3 From 95670933d7954a99e02f0e19f285d8740ab5e449 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 11 Feb 2014 14:44:56 +0100 Subject: Test and quick fix for #1257 --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 38b5089a..10256d47 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -893,6 +893,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ + and field.required \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions -- cgit v1.2.3 From 94fe03779b8e193a4cfd67be28ab9276e36f4179 Mon Sep 17 00:00:00 2001 From: Rodolfo Carvalho Date: Wed, 5 Mar 2014 17:01:54 +0100 Subject: Fix typo --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d47..c95b0593 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - # Add the `read_only` flag to any fields that have bee specified + # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: assert field_name not in self.base_fields.keys(), ( -- cgit v1.2.3 From 51e6982397cc032d6b3fd66f452713d448eb9084 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 21:18:37 +0100 Subject: Fixed the validation for optional fields that have a value. --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c95b0593..5c726dfc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -881,7 +881,7 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def get_validation_exclusions(self): + def get_validation_exclusions(self, instance=None): """ Return a list of field names to exclude from model validation. """ @@ -893,7 +893,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ - and field.required \ + and (field.required or hasattr(instance, field_name)) \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -908,7 +908,7 @@ class ModelSerializer(Serializer): the full_clean validation checking. """ try: - instance.full_clean(exclude=self.get_validation_exclusions()) + instance.full_clean(exclude=self.get_validation_exclusions(instance)) except ValidationError as err: self._errors = err.message_dict return None -- cgit v1.2.3 From 5c87db96c54aeb8ee62213b3ab2a054546d9756c Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 15:41:25 +0000 Subject: Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string)--- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d47..62ef5eed 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -757,6 +757,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field + + #Ensure that 'read_only_fields is an iterable + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified # in the `read_only_fields` option @@ -771,7 +774,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + + # Ensure that 'write_only_fields' is an iterabe + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " -- cgit v1.2.3 From 03f96988baa6b7fa3a94fd49a5a1631f92b19b4a Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:11:44 +0000 Subject: Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string)--- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 62ef5eed..03db418d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - #Ensure that 'read_only_fields is an iterable + # Ensure that 'read_only_fields' is an iterable assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified @@ -775,7 +775,7 @@ class ModelSerializer(Serializer): (field_name, self.__class__.__name__)) ret[field_name].read_only = True - # Ensure that 'write_only_fields' is an iterabe + # Ensure that 'write_only_fields' is an iterable assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: -- cgit v1.2.3 From 499d3cb8f0cb2f8327050e4fe775ee4bdf288285 Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:23:15 +0000 Subject: Update serializers.py --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 03db418d..4cb2d81c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -776,7 +776,7 @@ class ModelSerializer(Serializer): ret[field_name].read_only = True # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( -- cgit v1.2.3 From c3aa10e589cb524dc3bb39a4fccee8238763d25a Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 01:50:40 +0400 Subject: Moved get component from object after test source is star --- rest_framework/serializers.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c726dfc..cc0e027f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,25 +438,26 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return - # Set the serializer object if it exists - obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - - # If we have a model manager or similar object then we need - # to iterate through each instance. - if (self.many and - not hasattr(obj, '__iter__') and - is_simple_callable(getattr(obj, 'all', None))): - obj = obj.all() - - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None else: + # Set the serializer object if it exists + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() + + if self.source == '*': + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) + kwargs = { 'instance': obj, 'data': value, -- cgit v1.2.3 From e8167f96e6c1a112e76b647ac32164be931b09a8 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 08:53:41 +0400 Subject: Fixed copy-paste --- rest_framework/serializers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cc0e027f..01606e9c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,6 +438,11 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return + if self.source == '*': + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None @@ -452,12 +457,6 @@ class BaseSerializer(WritableField): is_simple_callable(getattr(obj, 'all', None))): obj = obj.all() - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) - kwargs = { 'instance': obj, 'data': value, -- cgit v1.2.3 From 04315c12af09d9b2ee1106ab31af5891833dd2f9 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Mon, 24 Mar 2014 19:25:28 +0100 Subject: Use help_text, verbose_name, editable attributes for related fields --- rest_framework/serializers.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 88972e25..46beb6ac 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,6 +828,15 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if not model_field.editable: + kwargs['read_only'] = True + + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name + + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): -- cgit v1.2.3 From ab5082d15c04866401c6f1bc7d77d21e695f996d Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Fri, 28 Mar 2014 19:42:46 +0100 Subject: Do not check model_field's attributes if it is None --- rest_framework/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46beb6ac..d7941df1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,14 +828,14 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - if not model_field.editable: - kwargs['read_only'] = True + if not model_field.editable: + kwargs['read_only'] = True - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text return PrimaryKeyRelatedField(**kwargs) -- cgit v1.2.3 From 0a0e4f22e72badd1d8700a2b253cb27450a5319f Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Sat, 12 Apr 2014 17:51:02 +0100 Subject: Set GenericForeignKey fields on object before save * A model with a required GenericForeignKey can be saved if the field is set --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cb7539e0..1d6097ed 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -16,6 +16,7 @@ import datetime import inspect import types from decimal import Decimal +from django.contrib.contenttypes.generic import GenericForeignKey from django.core.paginator import Page from django.db import models from django.forms import widgets @@ -943,6 +944,8 @@ class ModelSerializer(Serializer): # Forward m2m relations for field in meta.many_to_many + meta.virtual_fields: + if isinstance(field, GenericForeignKey): + continue if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) -- cgit v1.2.3 From 853c7a16c15c7291561bc4b3dfbcad88ea262a18 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Sun, 13 Apr 2014 17:26:15 +0100 Subject: Use setattr for adding fields to a new instance Add test for restoring a GenericForeignKey --- rest_framework/serializers.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1d6097ed..ea9509bf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -955,17 +955,15 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] - # Update an existing instance... - if instance is not None: - for key, val in attrs.items(): - try: - setattr(instance, key, val) - except ValueError: - self._errors[key] = self.error_messages['required'] + # Create an empty instance of the model + if instance is None: + instance = self.opts.model() - # ...or create a new instance - else: - instance = self.opts.model(**attrs) + for key, val in attrs.items(): + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # Any relations that cannot be set until we've # saved the model get hidden away on these -- cgit v1.2.3 From a6e525cf3a22a01a4f9924e975ea7288d80ac5ef Mon Sep 17 00:00:00 2001 From: Sergey Sinitsyn Date: Thu, 24 Apr 2014 15:58:53 +0600 Subject: Add help_text and verbose_name attribute mapping for related field --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ea9509bf..9cb548a5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,6 +828,10 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name return PrimaryKeyRelatedField(**kwargs) @@ -1088,6 +1092,10 @@ class HyperlinkedModelSerializer(ModelSerializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name if self.opts.lookup_field: kwargs['lookup_field'] = self.opts.lookup_field -- cgit v1.2.3 From 5c12b0768166376783d62632e562f0c1301ee847 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 16 May 2014 19:40:02 +0200 Subject: Added missing import. --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2a0d5263..6dd09f68 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -21,6 +21,7 @@ from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict +from django.core.exceptions import ObjectDoesNotExist from rest_framework.compat import get_concrete_model, six from rest_framework.settings import api_settings -- cgit v1.2.3 From a1a3ad763996b9ab5535bc5d442c2d6fab10b7cc Mon Sep 17 00:00:00 2001 From: allenhu Date: Sat, 17 May 2014 06:05:33 +0800 Subject: fix pep8 --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6dd09f68..87d20cfc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -33,8 +33,8 @@ from rest_framework.settings import api_settings # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. -from rest_framework.relations import * -from rest_framework.fields import * +from rest_framework.relations import * # NOQA +from rest_framework.fields import * # NOQA def _resolve_model(obj): @@ -345,7 +345,7 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field.read_only and obj is None: - continue + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) -- cgit v1.2.3 From 807f7a6bb9e36321f3487b5ac31ef5fdc8f4b3fb Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Thu, 22 May 2014 13:51:20 -0600 Subject: Fix _resolve_model to work with unicode strings --- rest_framework/serializers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 87d20cfc..c2b414d7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -49,7 +49,7 @@ def _resolve_model(obj): String representations should have the format: 'appname.ModelName' """ - if type(obj) == str and len(obj.split('.')) == 2: + if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: app_name, model_name = obj.split('.') return models.get_model(app_name, model_name) elif inspect.isclass(obj) and issubclass(obj, models.Model): @@ -759,9 +759,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field - + # Ensure that 'read_only_fields' is an iterable - assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option @@ -776,10 +776,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' - + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " @@ -790,7 +790,7 @@ class ModelSerializer(Serializer): "Non-existant field '%s' specified in `write_only_fields` " "on serializer '%s'." % (field_name, self.__class__.__name__)) - ret[field_name].write_only = True + ret[field_name].write_only = True return ret -- cgit v1.2.3 From 34c1da3515dbf4e9a0d645ebf81cde6f61254e31 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Wed, 13 Aug 2014 15:31:25 -0500 Subject: ModelSerializer.restore_object - errors as list When a ValueError is raised in ModelSerializer.restore_object, the error is set to a one-element list, rather than a bare string. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c2b414d7..43d339da 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -977,7 +977,7 @@ class ModelSerializer(Serializer): try: setattr(instance, key, val) except ValueError: - self._errors[key] = self.error_messages['required'] + self._errors[key] = [self.error_messages['required']] # Any relations that cannot be set until we've # saved the model get hidden away on these -- cgit v1.2.3 From bf09c32de8f9d528f83e9cb7a2773d1f4c9ab563 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Aug 2014 13:28:07 +0100 Subject: Code linting and added runtests.py --- rest_framework/serializers.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2fdc9b9d..95288671 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -449,9 +449,11 @@ class BaseSerializer(WritableField): # If we have a model manager or similar object then we need # to iterate through each instance. - if (self.many and + if ( + self.many and not hasattr(obj, '__iter__') and - is_simple_callable(getattr(obj, 'all', None))): + is_simple_callable(getattr(obj, 'all', None)) + ): obj = obj.all() kwargs = { @@ -601,8 +603,10 @@ class BaseSerializer(WritableField): API schemas for auto-documentation. """ return SortedDict( - [(field_name, field.metadata()) - for field_name, field in six.iteritems(self.fields)] + [ + (field_name, field.metadata()) + for field_name, field in six.iteritems(self.fields) + ] ) @@ -656,8 +660,10 @@ class ModelSerializer(Serializer): """ cls = self.opts.model - assert cls is not None, \ - "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ + assert cls is not None, ( + "Serializer class '%s' is missing 'model' Meta option" % + self.__class__.__name__ + ) opts = cls._meta.concrete_model._meta ret = SortedDict() nested = bool(self.opts.depth) @@ -668,9 +674,9 @@ class ModelSerializer(Serializer): # If model is a child via multitable inheritance, use parent's pk pk_field = pk_field.rel.to._meta.pk - field = self.get_pk_field(pk_field) - if field: - ret[pk_field.name] = field + serializer_pk_field = self.get_pk_field(pk_field) + if serializer_pk_field: + ret[pk_field.name] = serializer_pk_field # Deal with forward relationships forward_rels = [field for field in opts.fields if field.serialize] @@ -739,9 +745,11 @@ class ModelSerializer(Serializer): is_m2m = isinstance(relation.field, models.fields.related.ManyToManyField) - if (is_m2m and + if ( + is_m2m and hasattr(relation.field.rel, 'through') and - not relation.field.rel.through._meta.auto_created): + not relation.field.rel.through._meta.auto_created + ): has_through_model = True if nested: @@ -911,10 +919,12 @@ class ModelSerializer(Serializer): for field_name, field in self.fields.items(): field_name = field.source or field_name - if field_name in exclusions \ - and not field.read_only \ - and (field.required or hasattr(instance, field_name)) \ - and not isinstance(field, Serializer): + if ( + field_name in exclusions + and not field.read_only + and (field.required or hasattr(instance, field_name)) + and not isinstance(field, Serializer) + ): exclusions.remove(field_name) return exclusions -- cgit v1.2.3 From 63d02dbea855a060ec4cdb194497188e2f40cb66 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Aug 2014 17:06:55 +0100 Subject: Drop six from compat. 1.4.2 is now the lowest supported version. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 95288671..be8ad3f2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,9 +20,9 @@ from django.contrib.contenttypes.generic import GenericForeignKey from django.core.paginator import Page from django.db import models from django.forms import widgets +from django.utils import six from django.utils.datastructures import SortedDict from django.core.exceptions import ObjectDoesNotExist -from rest_framework.compat import six from rest_framework.settings import api_settings -- cgit v1.2.3 From 4ac4676a40b121d27cfd1173ff548d96b8d3de2f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 29 Aug 2014 16:46:26 +0100 Subject: First pass --- rest_framework/serializers.py | 1096 +++++++++++------------------------------ 1 file changed, 291 insertions(+), 805 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index be8ad3f2..d121812d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,21 +10,14 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -from __future__ import unicode_literals -import copy -import datetime -import inspect -import types -from decimal import Decimal -from django.contrib.contenttypes.generic import GenericForeignKey -from django.core.paginator import Page from django.db import models -from django.forms import widgets from django.utils import six -from django.utils.datastructures import SortedDict -from django.core.exceptions import ObjectDoesNotExist +from collections import namedtuple, OrderedDict +from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError from rest_framework.settings import api_settings - +from rest_framework.utils import html +import copy +import inspect # Note: We do the following so that users of the framework can use this style: # @@ -37,635 +30,339 @@ from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA -def _resolve_model(obj): - """ - Resolve supplied `obj` to a Django model class. +FieldResult = namedtuple('FieldResult', ['field', 'value', 'error']) - `obj` must be a Django model class itself, or a string - representation of one. Useful in situtations like GH #1225 where - Django may not have resolved a string-based reference to a model in - another model's foreign key definition. - - String representations should have the format: - 'appname.ModelName' - """ - if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: - app_name, model_name = obj.split('.') - return models.get_model(app_name, model_name) - elif inspect.isclass(obj) and issubclass(obj, models.Model): - return obj - else: - raise ValueError("{0} is not a Django model".format(obj)) - - -def pretty_name(name): - """Converts 'first_name' to 'First name'""" - if not name: - return '' - return name.replace('_', ' ').capitalize() +class BaseSerializer(Field): + def __init__(self, instance=None, data=None, **kwargs): + super(BaseSerializer, self).__init__(**kwargs) + self.instance = instance + self._initial_data = data -class RelationsList(list): - _deleted = [] + def to_native(self, data): + raise NotImplementedError() + def to_primative(self, instance): + raise NotImplementedError() -class NestedValidationError(ValidationError): - """ - The default ValidationError behavior is to stringify each item in the list - if the messages are a list of error messages. + def update(self, instance): + raise NotImplementedError() - In the case of nested serializers, where the parent has many children, - then the child's `serializer.errors` will be a list of dicts. In the case - of a single child, the `serializer.errors` will be a dict. + def create(self): + raise NotImplementedError() - We need to override the default behavior to get properly nested error dicts. - """ + def save(self, extras=None): + if extras is not None: + self._validated_data.update(extras) - def __init__(self, message): - if isinstance(message, dict): - self._messages = [message] + if self.instance is not None: + self.update(self.instance) else: - self._messages = message - - @property - def messages(self): - return self._messages + self.instance = self.create() + return self.instance -class DictWithMetadata(dict): - """ - A dict-like object, that can have additional properties attached. - """ - def __getstate__(self): - """ - Used by pickle (e.g., caching). - Overridden to remove the metadata from the dict, since it shouldn't be - pickled and may in some instances be unpickleable. - """ - return dict(self) - + def is_valid(self): + try: + self._validated_data = self.to_native(self._initial_data) + except ValidationError as exc: + self._validated_data = {} + self._errors = exc.args[0] + return False + self._errors = {} + return True -class SortedDictWithMetadata(SortedDict): - """ - A sorted dict-like object, that can have additional properties attached. - """ - def __getstate__(self): - """ - Used by pickle (e.g., caching). - Overriden to remove the metadata from the dict, since it shouldn't be - pickle and may in some instances be unpickleable. - """ - return SortedDict(self).__dict__ + @property + def data(self): + if not hasattr(self, '_data'): + if self.instance is not None: + self._data = self.to_primative(self.instance) + elif self._initial_data is not None: + self._data = { + field_name: field.get_value(self._initial_data) + for field_name, field in self.fields.items() + } + else: + self._data = self.get_initial() + return self._data + @property + def errors(self): + if not hasattr(self, '_errors'): + msg = 'You must call `.is_valid()` before accessing `.errors`.' + raise AssertionError(msg) + return self._errors -def _is_protected_type(obj): - """ - True if the object is a native datatype that does not need to - be serialized further. - """ - return isinstance(obj, ( - types.NoneType, - int, long, - datetime.datetime, datetime.date, datetime.time, - float, Decimal, - basestring) - ) + @property + def validated_data(self): + if not hasattr(self, '_validated_data'): + msg = 'You must call `.is_valid()` before accessing `.validated_data`.' + raise AssertionError(msg) + return self._validated_data -def _get_declared_fields(bases, attrs): +class SerializerMetaclass(type): """ - Create a list of serializer field instances from the passed in 'attrs', - plus any fields on the base classes (in 'bases'). + This metaclass sets a dictionary named `base_fields` on the class. - Note that all fields from the base classes are used. + Any fields included as attributes on either the class or it's superclasses + will be include in the `base_fields` dictionary. """ - fields = [(field_name, attrs.pop(field_name)) - for field_name, obj in list(six.iteritems(attrs)) - if isinstance(obj, Field)] - fields.sort(key=lambda x: x[1].creation_counter) - # If this class is subclassing another Serializer, add that Serializer's - # fields. Note that we loop over the bases in *reverse*. This is necessary - # in order to maintain the correct order of fields. - for base in bases[::-1]: - if hasattr(base, 'base_fields'): - fields = list(base.base_fields.items()) + fields + @classmethod + def _get_fields(cls, bases, attrs): + fields = [(field_name, attrs.pop(field_name)) + for field_name, obj in list(attrs.items()) + if isinstance(obj, Field)] + fields.sort(key=lambda x: x[1]._creation_counter) - return SortedDict(fields) + # If this class is subclassing another Serializer, add that Serializer's + # fields. Note that we loop over the bases in *reverse*. This is necessary + # in order to maintain the correct order of fields. + for base in bases[::-1]: + if hasattr(base, 'base_fields'): + fields = list(base.base_fields.items()) + fields + return OrderedDict(fields) -class SerializerMetaclass(type): def __new__(cls, name, bases, attrs): - attrs['base_fields'] = _get_declared_fields(bases, attrs) + attrs['base_fields'] = cls._get_fields(bases, attrs) return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) -class SerializerOptions(object): - """ - Meta class options for Serializer - """ - def __init__(self, meta): - self.depth = getattr(meta, 'depth', 0) - self.fields = getattr(meta, 'fields', ()) - self.exclude = getattr(meta, 'exclude', ()) +@six.add_metaclass(SerializerMetaclass) +class Serializer(BaseSerializer): + def __new__(cls, *args, **kwargs): + many = kwargs.pop('many', False) + if many: + class DynamicListSerializer(ListSerializer): + child = cls() + return DynamicListSerializer(*args, **kwargs) + return super(Serializer, cls).__new__(cls) -class BaseSerializer(WritableField): - """ - This is the Serializer implementation. - We need to implement it as `BaseSerializer` due to metaclass magicks. - """ - class Meta(object): - pass - - _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata - - def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, many=False, - allow_add_remove=False, **kwargs): - super(BaseSerializer, self).__init__(**kwargs) - self.opts = self._options_class(self.Meta) - self.parent = None - self.root = None - self.partial = partial - self.many = many - self.allow_add_remove = allow_add_remove + def __init__(self, *args, **kwargs): + kwargs.pop('context', None) + kwargs.pop('partial', None) + kwargs.pop('many', False) - self.context = context or {} + super(Serializer, self).__init__(*args, **kwargs) - self.init_data = data - self.init_files = files - self.object = 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_fields() - self._data = None - self._files = None - self._errors = None - - if many and instance is not None and not hasattr(instance, '__iter__'): - raise ValueError('instance should be a queryset or other iterable with many=True') - - if allow_add_remove and not many: - raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True') - - ##### - # Methods to determine which fields to use when (de)serializing objects. - - def get_default_fields(self): - """ - Return the complete set of default fields for the object, as a dict. - """ - return {} - - def get_fields(self): - """ - Returns the complete set of fields for the object as a dict. - - This will be the set of any explicitly declared fields, - plus the set of fields returned by get_default_fields(). - """ - ret = SortedDict() - - # Get the explicitly declared fields - base_fields = copy.deepcopy(self.base_fields) - for key, field in base_fields.items(): - ret[key] = field - - # Add in the default fields - default_fields = self.get_default_fields() - for key, val in default_fields.items(): - if key not in ret: - ret[key] = val - - # If 'fields' is specified, use those fields, in that order. - if self.opts.fields: - assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple' - new = SortedDict() - for key in self.opts.fields: - new[key] = ret[key] - ret = new - - # Remove anything in 'exclude' - if self.opts.exclude: - assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple' - for key in self.opts.exclude: - ret.pop(key, None) - - for key, field in ret.items(): - field.initialize(parent=self, field_name=key) - - return ret - - ##### - # Methods to convert or revert from objects <--> primitive representations. - - def get_field_key(self, field_name): - """ - Return the key that should be used for a given field. - """ - return field_name - - def restore_fields(self, data, files): - """ - Core of deserialization, together with `restore_object`. - Converts a dictionary of data into a dictionary of deserialized fields. - """ - reverted_data = {} - - if data is not None and not isinstance(data, dict): - self._errors['non_field_errors'] = ['Invalid data'] - return None - + # Setup all the child fields, to provide them with the current context. for field_name, field in self.fields.items(): - field.initialize(parent=self, field_name=field_name) - try: - field.field_from_native(data, files, field_name, reverted_data) - except ValidationError as err: - self._errors[field_name] = list(err.messages) + field.bind(field_name, self, self) - return reverted_data + def get_fields(self): + return copy.deepcopy(self.base_fields) - def perform_validation(self, attrs): - """ - Run `validate_()` and `validate()` methods on the serializer - """ + 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(): - if field_name in self._errors: - continue + field.bind(field_name, self, root) - source = field.source or field_name - if self.partial and source not in attrs: - continue - try: - validate_method = getattr(self, 'validate_%s' % field_name, None) - if validate_method: - attrs = validate_method(attrs, source) - except ValidationError as err: - self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) - - # If there are already errors, we don't run .validate() because - # field-validation failed and thus `attrs` may not be complete. - # which in turn can cause inconsistent validation errors. - if not self._errors: - try: - attrs = self.validate(attrs) - except ValidationError as err: - if hasattr(err, 'message_dict'): - for field_name, error_messages in err.message_dict.items(): - self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) - elif hasattr(err, 'messages'): - self._errors['non_field_errors'] = err.messages - - return attrs + def get_initial(self): + return { + field.field_name: field.get_initial() + for field in self.fields.values() + } - def validate(self, attrs): - """ - Stub method, to be overridden in Serializer subclasses - """ - return attrs + def get_value(self, dictionary): + # We override the default field access in order to support + # nested HTML forms. + if html.is_html_input(dictionary): + return html.parse_html_dict(dictionary, prefix=self.field_name) + return dictionary.get(self.field_name, empty) - def restore_object(self, attrs, instance=None): + def to_native(self, data): """ - Deserialize a dictionary of attributes into an object instance. - You should override this method to control how deserialized objects - are instantiated. + Dict of native values <- Dict of primitive datatypes. """ - if instance is not None: - instance.update(attrs) - return instance - return attrs + ret = {} + errors = {} + fields = [field for field in self.fields.values() if not field.read_only] - def to_native(self, obj): - """ - Serialize objects -> primitives. - """ - ret = self._dict_class() - ret.fields = self._dict_class() + for field in fields: + primitive_value = field.get_value(data) + try: + validated_value = field.validate(primitive_value) + except ValidationError as exc: + errors[field.field_name] = str(exc) + except SkipField: + pass + else: + set_value(ret, field.source_attrs, validated_value) - for field_name, field in self.fields.items(): - if field.read_only and obj is None: - continue - field.initialize(parent=self, field_name=field_name) - key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) - method = getattr(self, 'transform_%s' % field_name, None) - if callable(method): - value = method(obj, value) - if not getattr(field, 'write_only', False): - ret[key] = value - ret.fields[key] = self.augment_field(field, field_name, key, value) + if errors: + raise ValidationError(errors) return ret - def from_native(self, data, files=None): - """ - Deserialize primitives -> objects. - """ - self._errors = {} - - if data is not None or files is not None: - attrs = self.restore_fields(data, files) - if attrs is not None: - attrs = self.perform_validation(attrs) - else: - self._errors['non_field_errors'] = ['No input provided'] - - if not self._errors: - return self.restore_object(attrs, instance=getattr(self, 'object', None)) - - def augment_field(self, field, field_name, key, value): - # This horrible stuff is to manage serializers rendering to HTML - field._errors = self._errors.get(key) if self._errors else None - field._name = field_name - field._value = self.init_data.get(key) if self._errors and self.init_data else value - if not field.label: - field.label = pretty_name(key) - return field - - def field_to_native(self, obj, field_name): + def to_primative(self, instance): """ - Override default so that the serializer can be used as a nested field - across relationships. + Object instance -> Dict of primitive datatypes. """ - if self.write_only: - return None + ret = OrderedDict() + fields = [field for field in self.fields.values() if not field.write_only] - if self.source == '*': - return self.to_native(obj) + for field in fields: + native_value = field.get_attribute(instance) + ret[field.field_name] = field.to_primative(native_value) - # Get the raw field value - try: - source = self.source or field_name - value = obj - - for component in source.split('.'): - if value is None: - break - value = get_component(value, component) - except ObjectDoesNotExist: - return None + return ret - if is_simple_callable(getattr(value, 'all', None)): - return [self.to_native(item) for item in value.all()] + 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) - if value is None: - return None - if self.many: - return [self.to_native(item) for item in value] - return self.to_native(value) +class ListSerializer(BaseSerializer): + child = None + initial = [] - def field_from_native(self, data, files, field_name, into): - """ - Override default so that the serializer can be used as a writable - nested field across relationships. - """ - if self.read_only: - return + 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.' - try: - value = data[field_name] - except KeyError: - if self.default is not None and not self.partial: - # Note: partial updates shouldn't set defaults - value = copy.deepcopy(self.default) - else: - if self.required: - raise ValidationError(self.error_messages['required']) - return - - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) - else: - if value in (None, ''): - into[(self.source or field_name)] = None - else: - # Set the serializer object if it exists - obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - - # If we have a model manager or similar object then we need - # to iterate through each instance. - if ( - self.many and - not hasattr(obj, '__iter__') and - is_simple_callable(getattr(obj, 'all', None)) - ): - obj = obj.all() - - kwargs = { - 'instance': obj, - 'data': value, - 'context': self.context, - 'partial': self.partial, - 'many': self.many, - 'allow_add_remove': self.allow_add_remove - } - serializer = self.__class__(**kwargs) + kwargs.pop('context', None) + kwargs.pop('partial', None) - if serializer.is_valid(): - into[self.source or field_name] = serializer.object - else: - # Propagate errors up to our parent - raise NestedValidationError(serializer.errors) + super(ListSerializer, self).__init__(*args, **kwargs) + self.child.bind('', self, self) - def get_identity(self, data): - """ - This hook is required for bulk update. - It is used to determine the canonical identity of a given object. + 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) - Note that the data has not been validated at this point, so we need - to make sure that we catch any cases of incorrect datatypes being - passed to this method. - """ - try: - return data.get('id', None) - except AttributeError: - return None + def get_value(self, dictionary): + # We override the default field access in order to support + # lists in HTML forms. + if is_html_input(dictionary): + return html.parse_html_list(dictionary, prefix=self.field_name) + return dictionary.get(self.field_name, empty) - @property - def errors(self): + def to_native(self, data): """ - Run deserialization and return error data, - setting self.object if no errors occurred. + List of dicts of native values <- List of dicts of primitive datatypes. """ - if self._errors is None: - data, files = self.init_data, self.init_files + if html.is_html_input(data): + data = html.parse_html_list(data) - if self.many is not None: - many = self.many - else: - many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) - if many: - warnings.warn('Implicit list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=3) - - if many: - ret = RelationsList() - errors = [] - update = self.object is not None - - if update: - # If this is a bulk update we need to map all the objects - # to a canonical identity so we can determine which - # individual object is being updated for each item in the - # incoming data - objects = self.object - identities = [self.get_identity(self.to_native(obj)) for obj in objects] - identity_to_objects = dict(zip(identities, objects)) - - if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): - for item in data: - if update: - # Determine which object we're updating - identity = self.get_identity(item) - self.object = identity_to_objects.pop(identity, None) - if self.object is None and not self.allow_add_remove: - ret.append(None) - errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}) - continue - - ret.append(self.from_native(item, None)) - errors.append(self._errors) - - if update and self.allow_add_remove: - ret._deleted = identity_to_objects.values() - - self._errors = any(errors) and errors or [] - else: - self._errors = {'non_field_errors': ['Expected a list of items.']} - else: - ret = self.from_native(data, files) - - if not self._errors: - self.object = ret - - return self._errors - - def is_valid(self): - return not self.errors + return [self.child.validate(item) for item in data] - @property - def data(self): + def to_primative(self, data): """ - Returns the serialized data on the serializer. + List of object instances -> List of dicts of primitive datatypes. """ - if self._data is None: - obj = self.object + return [self.child.to_primative(item) for item in data] - if self.many is not None: - many = self.many - else: - many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) - if many: - warnings.warn('Implicit list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=2) - - if many: - self._data = [self.to_native(item) for item in obj] - else: - self._data = self.to_native(obj) + def create(self, attrs_list): + return [self.child.create(attrs) for attrs in attrs_list] - return self._data + def save(self): + if self.instance is not None: + self.update(self.instance, self.validated_data) + self.instance = self.create(self.validated_data) + return self.instance - def save_object(self, obj, **kwargs): - obj.save(**kwargs) - def delete_object(self, obj): - obj.delete() - - def save(self, **kwargs): - """ - Save the deserialized object and return it. - """ - # Clear cached _data, which may be invalidated by `save()` - self._data = None - - if isinstance(self.object, list): - [self.save_object(item, **kwargs) for item in self.object] - - if self.object._deleted: - [self.delete_object(item) for item in self.object._deleted] - else: - self.save_object(self.object, **kwargs) - - return self.object - - def metadata(self): - """ - Return a dictionary of metadata about the fields on the serializer. - Useful for things like responding to OPTIONS requests, or generating - API schemas for auto-documentation. - """ - return SortedDict( - [ - (field_name, field.metadata()) - for field_name, field in six.iteritems(self.fields) - ] - ) +def _resolve_model(obj): + """ + Resolve supplied `obj` to a Django model class. + `obj` must be a Django model class itself, or a string + representation of one. Useful in situtations like GH #1225 where + Django may not have resolved a string-based reference to a model in + another model's foreign key definition. -class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): - pass + String representations should have the format: + 'appname.ModelName' + """ + if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: + app_name, model_name = obj.split('.') + return models.get_model(app_name, model_name) + elif inspect.isclass(obj) and issubclass(obj, models.Model): + return obj + else: + raise ValueError("{0} is not a Django model".format(obj)) -class ModelSerializerOptions(SerializerOptions): +class ModelSerializerOptions(object): """ Meta class options for ModelSerializer """ def __init__(self, meta): - super(ModelSerializerOptions, self).__init__(meta) - self.model = getattr(meta, 'model', None) - self.read_only_fields = getattr(meta, 'read_only_fields', ()) - self.write_only_fields = getattr(meta, 'write_only_fields', ()) + self.model = getattr(meta, 'model') + self.fields = getattr(meta, 'fields', ()) + self.depth = getattr(meta, 'depth', 0) class ModelSerializer(Serializer): - """ - A serializer that deals with model instances and querysets. - """ - _options_class = ModelSerializerOptions - field_mapping = { models.AutoField: IntegerField, - models.FloatField: FloatField, + # models.FloatField: FloatField, models.IntegerField: IntegerField, models.PositiveIntegerField: IntegerField, models.SmallIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, - models.DateTimeField: DateTimeField, - models.DateField: DateField, - models.TimeField: TimeField, - models.DecimalField: DecimalField, - models.EmailField: EmailField, + # models.DateTimeField: DateTimeField, + # models.DateField: DateField, + # models.TimeField: TimeField, + # models.DecimalField: DecimalField, + # models.EmailField: EmailField, models.CharField: CharField, - models.URLField: URLField, - models.SlugField: SlugField, + # models.URLField: URLField, + # models.SlugField: SlugField, models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, models.NullBooleanField: BooleanField, - models.FileField: FileField, - models.ImageField: ImageField, + # models.FileField: FileField, + # models.ImageField: ImageField, } + _options_class = ModelSerializerOptions + + def __init__(self, *args, **kwargs): + self.opts = self._options_class(self.Meta) + super(ModelSerializer, self).__init__(*args, **kwargs) + + def get_fields(self): + # Get the explicitly declared fields. + fields = copy.deepcopy(self.base_fields) + + # Add in the default fields. + for key, val in self.get_default_fields().items(): + if key not in fields: + fields[key] = val + + # If `fields` is set on the `Meta` class, + # then use only those fields, and in that order. + if self.opts.fields: + fields = OrderedDict([ + (key, fields[key]) for key in self.opts.fields + ]) + + return fields + def get_default_fields(self): """ Return all the fields that should be serialized for the model. """ - cls = self.opts.model - assert cls is not None, ( - "Serializer class '%s' is missing 'model' Meta option" % - self.__class__.__name__ - ) opts = cls._meta.concrete_model._meta - ret = SortedDict() + ret = OrderedDict() nested = bool(self.opts.depth) # Deal with adding the primary key field @@ -694,29 +391,9 @@ class ModelSerializer(Serializer): has_through_model = True if model_field.rel and nested: - if len(inspect.getargspec(self.get_nested_field).args) == 2: - warnings.warn( - 'The `get_nested_field(model_field)` call signature ' - 'is deprecated. ' - 'Use `get_nested_field(model_field, related_model, ' - 'to_many) instead', - DeprecationWarning - ) - field = self.get_nested_field(model_field) - else: - field = self.get_nested_field(model_field, related_model, to_many) + field = self.get_nested_field(model_field, related_model, to_many) elif model_field.rel: - if len(inspect.getargspec(self.get_nested_field).args) == 3: - warnings.warn( - 'The `get_related_field(model_field, to_many)` call ' - 'signature is deprecated. ' - 'Use `get_related_field(model_field, related_model, ' - 'to_many) instead', - DeprecationWarning - ) - field = self.get_related_field(model_field, to_many=to_many) - else: - field = self.get_related_field(model_field, related_model, to_many) + field = self.get_related_field(model_field, related_model, to_many) else: field = self.get_field(model_field) @@ -763,38 +440,6 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - # Ensure that 'read_only_fields' is an iterable - assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' - - # Add the `read_only` flag to any fields that have been specified - # in the `read_only_fields` option - for field_name in self.opts.read_only_fields: - assert field_name not in self.base_fields.keys(), ( - "field '%s' on serializer '%s' specified in " - "`read_only_fields`, but also added " - "as an explicit field. Remove it from `read_only_fields`." % - (field_name, self.__class__.__name__)) - assert field_name in ret, ( - "Non-existant field '%s' specified in `read_only_fields` " - "on serializer '%s'." % - (field_name, self.__class__.__name__)) - ret[field_name].read_only = True - - # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' - - for field_name in self.opts.write_only_fields: - assert field_name not in self.base_fields.keys(), ( - "field '%s' on serializer '%s' specified in " - "`write_only_fields`, but also added " - "as an explicit field. Remove it from `write_only_fields`." % - (field_name, self.__class__.__name__)) - assert field_name in ret, ( - "Non-existant field '%s' specified in `write_only_fields` " - "on serializer '%s'." % - (field_name, self.__class__.__name__)) - ret[field_name].write_only = True - return ret def get_pk_field(self, model_field): @@ -825,28 +470,24 @@ class ModelSerializer(Serializer): # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) - kwargs = { - 'queryset': related_model._default_manager, - 'many': to_many - } + kwargs = {} + # 'queryset': related_model._default_manager, + # 'many': to_many + # } if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + # if model_field.help_text is not None: + # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name - if not model_field.editable: kwargs['read_only'] = True - if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text - - return PrimaryKeyRelatedField(**kwargs) + return IntegerField(**kwargs) + # TODO: return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): """ @@ -869,8 +510,8 @@ class ModelSerializer(Serializer): if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + # if model_field.help_text is not None: + # kwargs['help_text'] = model_field.help_text # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices @@ -880,7 +521,7 @@ class ModelSerializer(Serializer): return ChoiceField(**kwargs) # put this below the ChoiceField because min_value isn't a valid initializer - if issubclass(model_field.__class__, models.PositiveIntegerField) or\ + if issubclass(model_field.__class__, models.PositiveIntegerField) or \ issubclass(model_field.__class__, models.PositiveSmallIntegerField): kwargs['min_value'] = 0 @@ -888,170 +529,27 @@ class ModelSerializer(Serializer): issubclass(model_field.__class__, (models.CharField, models.TextField)): kwargs['allow_none'] = True - attribute_dict = { - models.CharField: ['max_length'], - models.CommaSeparatedIntegerField: ['max_length'], - models.DecimalField: ['max_digits', 'decimal_places'], - models.EmailField: ['max_length'], - models.FileField: ['max_length'], - models.ImageField: ['max_length'], - models.SlugField: ['max_length'], - models.URLField: ['max_length'], - } - - if model_field.__class__ in attribute_dict: - attributes = attribute_dict[model_field.__class__] - for attribute in attributes: - kwargs.update({attribute: getattr(model_field, attribute)}) + # attribute_dict = { + # models.CharField: ['max_length'], + # models.CommaSeparatedIntegerField: ['max_length'], + # models.DecimalField: ['max_digits', 'decimal_places'], + # models.EmailField: ['max_length'], + # models.FileField: ['max_length'], + # models.ImageField: ['max_length'], + # models.SlugField: ['max_length'], + # models.URLField: ['max_length'], + # } + + # if model_field.__class__ in attribute_dict: + # attributes = attribute_dict[model_field.__class__] + # for attribute in attributes: + # kwargs.update({attribute: getattr(model_field, attribute)}) try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: - return ModelField(model_field=model_field, **kwargs) - - def get_validation_exclusions(self, instance=None): - """ - Return a list of field names to exclude from model validation. - """ - cls = self.opts.model - opts = cls._meta.concrete_model._meta - exclusions = [field.name for field in opts.fields + opts.many_to_many] - - for field_name, field in self.fields.items(): - field_name = field.source or field_name - if ( - field_name in exclusions - and not field.read_only - and (field.required or hasattr(instance, field_name)) - and not isinstance(field, Serializer) - ): - exclusions.remove(field_name) - return exclusions - - def full_clean(self, instance): - """ - Perform Django's full_clean, and populate the `errors` dictionary - if any validation errors occur. - - Note that we don't perform this inside the `.restore_object()` method, - so that subclasses can override `.restore_object()`, and still get - the full_clean validation checking. - """ - try: - instance.full_clean(exclude=self.get_validation_exclusions(instance)) - except ValidationError as err: - self._errors = err.message_dict - return None - return instance - - def restore_object(self, attrs, instance=None): - """ - Restore the model instance. - """ - m2m_data = {} - related_data = {} - nested_forward_relations = {} - meta = self.opts.model._meta - - # Reverse fk or one-to-one relations - for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.get_accessor_name() - if field_name in attrs: - related_data[field_name] = attrs.pop(field_name) - - # Reverse m2m relations - for (obj, model) in meta.get_all_related_m2m_objects_with_model(): - field_name = obj.get_accessor_name() - if field_name in attrs: - m2m_data[field_name] = attrs.pop(field_name) - - # Forward m2m relations - for field in meta.many_to_many + meta.virtual_fields: - if isinstance(field, GenericForeignKey): - continue - if field.name in attrs: - m2m_data[field.name] = attrs.pop(field.name) - - # Nested forward relations - These need to be marked so we can save - # them before saving the parent model instance. - for field_name in attrs.keys(): - if isinstance(self.fields.get(field_name, None), Serializer): - nested_forward_relations[field_name] = attrs[field_name] - - # Create an empty instance of the model - if instance is None: - instance = self.opts.model() - - for key, val in attrs.items(): - try: - setattr(instance, key, val) - except ValueError: - self._errors[key] = [self.error_messages['required']] - - # Any relations that cannot be set until we've - # saved the model get hidden away on these - # private attributes, so we can deal with them - # at the point of save. - instance._related_data = related_data - instance._m2m_data = m2m_data - instance._nested_forward_relations = nested_forward_relations - - return instance - - def from_native(self, data, files): - """ - Override the default method to also include model field validation. - """ - instance = super(ModelSerializer, self).from_native(data, files) - if not self._errors: - return self.full_clean(instance) - - def save_object(self, obj, **kwargs): - """ - Save the deserialized object. - """ - if getattr(obj, '_nested_forward_relations', None): - # Nested relationships need to be saved before we can save the - # parent instance. - for field_name, sub_object in obj._nested_forward_relations.items(): - if sub_object: - self.save_object(sub_object) - setattr(obj, field_name, sub_object) - - obj.save(**kwargs) - - if getattr(obj, '_m2m_data', None): - for accessor_name, object_list in obj._m2m_data.items(): - setattr(obj, accessor_name, object_list) - del(obj._m2m_data) - - if getattr(obj, '_related_data', None): - related_fields = dict([ - (field.get_accessor_name(), field) - for field, model - in obj._meta.get_all_related_objects_with_model() - ]) - for accessor_name, related in obj._related_data.items(): - if isinstance(related, RelationsList): - # Nested reverse fk relationship - for related_item in related: - fk_field = related_fields[accessor_name].field.name - setattr(related_item, fk_field, obj) - self.save_object(related_item) - - # Delete any removed objects - if related._deleted: - [self.delete_object(item) for item in related._deleted] - - elif isinstance(related, models.Model): - # Nested reverse one-one relationship - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related, fk_field, obj) - self.save_object(related) - else: - # Reverse FK or reverse one-one - setattr(obj, accessor_name, related) - del(obj._related_data) + # TODO: Change this to `return ModelField(model_field=model_field, **kwargs)` + return CharField(**kwargs) class HyperlinkedModelSerializerOptions(ModelSerializerOptions): @@ -1066,14 +564,10 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): - """ - A subclass of ModelSerializer that uses hyperlinked relationships, - instead of primary key relationships. - """ _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' - _hyperlink_field_class = HyperlinkedRelatedField - _hyperlink_identify_field_class = HyperlinkedIdentityField + #_hyperlink_field_class = HyperlinkedRelatedField + #_hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() @@ -1081,15 +575,15 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - if self.opts.url_field_name not in fields: - url_field = self._hyperlink_identify_field_class( - view_name=self.opts.view_name, - lookup_field=self.opts.lookup_field - ) - ret = self._dict_class() - ret[self.opts.url_field_name] = url_field - ret.update(fields) - fields = ret + # if self.opts.url_field_name not in fields: + # url_field = self._hyperlink_identify_field_class( + # view_name=self.opts.view_name, + # lookup_field=self.opts.lookup_field + # ) + # ret = self._dict_class() + # ret[self.opts.url_field_name] = url_field + # ret.update(fields) + # fields = ret return fields @@ -1103,33 +597,25 @@ class HyperlinkedModelSerializer(ModelSerializer): """ # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) - kwargs = { - 'queryset': related_model._default_manager, - 'view_name': self._get_default_view_name(related_model), - 'many': to_many - } + # kwargs = { + # 'queryset': related_model._default_manager, + # 'view_name': self._get_default_view_name(related_model), + # 'many': to_many + # } + kwargs = {} if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + # if model_field.help_text is not None: + # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name - if self.opts.lookup_field: - kwargs['lookup_field'] = self.opts.lookup_field - - return self._hyperlink_field_class(**kwargs) + return IntegerField(**kwargs) + # if self.opts.lookup_field: + # kwargs['lookup_field'] = self.opts.lookup_field - def get_identity(self, data): - """ - This hook is required for bulk update. - We need to override the default, to use the url as the identity. - """ - try: - return data.get(self.opts.url_field_name, None) - except AttributeError: - return None + # return self._hyperlink_field_class(**kwargs) def _get_default_view_name(self, model): """ -- cgit v1.2.3 From 82d4b2083292659358d5df4d03d2115576e8ae4e Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 12:17:36 +0300 Subject: Add subclass matching to serializer field mapping. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index be8ad3f2..6d25161e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -907,6 +907,9 @@ class ModelSerializer(Serializer): try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: + for model_field_class, serializer_field_class in self.field_mapping.items(): + if isinstance(model_field, model_field_class): + return serializer_field_class(**kwargs) return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): -- cgit v1.2.3 From ae84b8b0e8a99261ea2436f77ab5238f21603c0c Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 15:03:39 +0300 Subject: Traverse the method resolution order when mapping serializer fields. --- rest_framework/serializers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6d25161e..f37fbf98 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -904,13 +904,11 @@ class ModelSerializer(Serializer): for attribute in attributes: kwargs.update({attribute: getattr(model_field, attribute)}) - try: - return self.field_mapping[model_field.__class__](**kwargs) - except KeyError: - for model_field_class, serializer_field_class in self.field_mapping.items(): - if isinstance(model_field, model_field_class): - return serializer_field_class(**kwargs) - return ModelField(model_field=model_field, **kwargs) + for model_field_baseclass in inspect.getmro(model_field.__class__): + serializer_field_class = self.field_mapping.get(model_field_baseclass) + if serializer_field_class: + return serializer_field_class(**kwargs) + return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): """ -- cgit v1.2.3 From 582f6fdd4b0fb12a7c0d1fefe265499a284c9b79 Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 15:54:33 +0300 Subject: Add utility function to match classes in dictionary. --- rest_framework/serializers.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f37fbf98..5c33300c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -625,6 +625,21 @@ class ModelSerializerOptions(SerializerOptions): self.write_only_fields = getattr(meta, 'write_only_fields', ()) +def _get_class_mapping(mapping, obj): + """ + Takes a dictionary with classes as keys, and an object. + Traverses the object's inheritance hierarchy in method + resolution order, and returns the first matching value + from the dictionary or None. + + """ + for baseclass in inspect.getmro(obj.__class__): + val = mapping.get(baseclass) + if val: + return val + return None + + class ModelSerializer(Serializer): """ A serializer that deals with model instances and querysets. @@ -899,15 +914,16 @@ class ModelSerializer(Serializer): models.URLField: ['max_length'], } - if model_field.__class__ in attribute_dict: - attributes = attribute_dict[model_field.__class__] + attributes = _get_class_mapping(attribute_dict, model_field) + if attributes: for attribute in attributes: kwargs.update({attribute: getattr(model_field, attribute)}) - for model_field_baseclass in inspect.getmro(model_field.__class__): - serializer_field_class = self.field_mapping.get(model_field_baseclass) - if serializer_field_class: - return serializer_field_class(**kwargs) + serializer_field_class = _get_class_mapping( + self.field_mapping, model_field) + + if serializer_field_class: + return serializer_field_class(**kwargs) return ModelField(model_field=model_field, **kwargs) def get_validation_exclusions(self, instance=None): -- cgit v1.2.3 From e437520217e20d500d641b95482d49484b1f24a7 Mon Sep 17 00:00:00 2001 From: Timo Tuominen Date: Mon, 1 Sep 2014 17:02:48 +0300 Subject: Generator implementation of class mapping. --- rest_framework/serializers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c33300c..b3db3582 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,11 +633,10 @@ def _get_class_mapping(mapping, obj): from the dictionary or None. """ - for baseclass in inspect.getmro(obj.__class__): - val = mapping.get(baseclass) - if val: - return val - return None + return next( + (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping), + None + ) class ModelSerializer(Serializer): -- cgit v1.2.3 From ec096a1caceff6a4f5c75a152dd1c7bea9ed281d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Sep 2014 15:07:56 +0100 Subject: Add relations and get tests running --- rest_framework/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d121812d..2f23b4d9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -477,8 +477,8 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - # if model_field.help_text is not None: - # kwargs['help_text'] = model_field.help_text + # if model_field.help_text is not None: + # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name if not model_field.editable: @@ -566,8 +566,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' - #_hyperlink_field_class = HyperlinkedRelatedField - #_hyperlink_identify_field_class = HyperlinkedIdentityField + # _hyperlink_field_class = HyperlinkedRelatedField + # _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() -- cgit v1.2.3 From f2852811f93863f2eed04d51eeb7ef27716b2409 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Sep 2014 17:41:23 +0100 Subject: Getting tests passing --- rest_framework/serializers.py | 53 ++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 21 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2f23b4d9..c38d8968 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -142,7 +142,7 @@ class Serializer(BaseSerializer): return super(Serializer, cls).__new__(cls) def __init__(self, *args, **kwargs): - kwargs.pop('context', None) + self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) kwargs.pop('many', False) @@ -202,7 +202,7 @@ class Serializer(BaseSerializer): if errors: raise ValidationError(errors) - return ret + return self.validate(ret) def to_primative(self, instance): """ @@ -217,6 +217,9 @@ class Serializer(BaseSerializer): return ret + def validate(self, attrs): + return attrs + def __iter__(self): errors = self.errors if hasattr(self, '_errors') else {} for field in self.fields.values(): @@ -232,8 +235,7 @@ class ListSerializer(BaseSerializer): 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.' - - kwargs.pop('context', None) + self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) super(ListSerializer, self).__init__(*args, **kwargs) @@ -316,19 +318,19 @@ class ModelSerializer(Serializer): models.PositiveIntegerField: IntegerField, models.SmallIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, - # models.DateTimeField: DateTimeField, - # models.DateField: DateField, - # models.TimeField: TimeField, + models.DateTimeField: DateTimeField, + models.DateField: DateField, + models.TimeField: TimeField, # models.DecimalField: DecimalField, - # models.EmailField: EmailField, + models.EmailField: EmailField, models.CharField: CharField, - # models.URLField: URLField, + models.URLField: URLField, # models.SlugField: SlugField, models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, models.NullBooleanField: BooleanField, - # models.FileField: FileField, + models.FileField: FileField, # models.ImageField: ImageField, } @@ -338,6 +340,15 @@ class ModelSerializer(Serializer): self.opts = self._options_class(self.Meta) super(ModelSerializer, self).__init__(*args, **kwargs) + def create(self): + ModelClass = self.opts.model + return ModelClass.objects.create(**self.validated_data) + + def update(self, obj): + for attr, value in self.validated_data.items(): + setattr(obj, attr, value) + obj.save() + def get_fields(self): # Get the explicitly declared fields. fields = copy.deepcopy(self.base_fields) @@ -566,8 +577,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' - # _hyperlink_field_class = HyperlinkedRelatedField - # _hyperlink_identify_field_class = HyperlinkedIdentityField + _hyperlink_field_class = HyperlinkedRelatedField + _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() @@ -575,15 +586,15 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - # if self.opts.url_field_name not in fields: - # url_field = self._hyperlink_identify_field_class( - # view_name=self.opts.view_name, - # lookup_field=self.opts.lookup_field - # ) - # ret = self._dict_class() - # ret[self.opts.url_field_name] = url_field - # ret.update(fields) - # fields = ret + if self.opts.url_field_name not in fields: + url_field = self._hyperlink_identify_field_class( + view_name=self.opts.view_name, + lookup_field=self.opts.lookup_field + ) + ret = fields.__class__() + ret[self.opts.url_field_name] = url_field + ret.update(fields) + fields = ret return fields -- cgit v1.2.3 From d934824bff21e4a11226af61efba319be227f4f0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Sep 2014 16:29:46 +0100 Subject: Workin on --- rest_framework/serializers.py | 64 +++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 23 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c38d8968..49eb6ce9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,7 +13,8 @@ response content is handled by parsers and renderers. from django.db import models from django.utils import six from collections import namedtuple, OrderedDict -from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError +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 import copy @@ -34,43 +35,53 @@ FieldResult = namedtuple('FieldResult', ['field', 'value', 'error']) 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 def to_native(self, data): - raise NotImplementedError() + raise NotImplementedError('`to_native()` must be implemented.') def to_primative(self, instance): - raise NotImplementedError() + raise NotImplementedError('`to_primative()` must be implemented.') - def update(self, instance): - raise NotImplementedError() + def update(self, instance, attrs): + raise NotImplementedError('`update()` must be implemented.') - def create(self): - raise NotImplementedError() + def create(self, attrs): + raise NotImplementedError('`create()` must be implemented.') def save(self, extras=None): if extras is not None: - self._validated_data.update(extras) + self.validated_data.update(extras) if self.instance is not None: - self.update(self.instance) + self.update(self.instance, self._validated_data) else: - self.instance = self.create() + self.instance = self.create(self._validated_data) return self.instance - def is_valid(self): - try: - self._validated_data = self.to_native(self._initial_data) - except ValidationError as exc: - self._validated_data = {} - self._errors = exc.args[0] - return False - self._errors = {} - return True + def is_valid(self, raise_exception=False): + if not hasattr(self, '_validated_data'): + try: + self._validated_data = self.to_native(self._initial_data) + except ValidationError as exc: + self._validated_data = {} + self._errors = exc.detail + else: + self._errors = {} + + if self._errors and raise_exception: + raise ValidationError(self._errors) + + return not bool(self._errors) @property def data(self): @@ -184,14 +195,20 @@ class Serializer(BaseSerializer): """ Dict of native values <- Dict of primitive datatypes. """ + if not isinstance(data, dict): + raise ValidationError({'non_field_errors': ['Invalid data']}) + ret = {} errors = {} fields = [field for field in self.fields.values() if not field.read_only] for field in fields: + validate_method = getattr(self, 'validate_' + field.field_name, None) primitive_value = field.get_value(data) try: validated_value = field.validate(primitive_value) + if validate_method is not None: + validated_value = validate_method(validated_value) except ValidationError as exc: errors[field.field_name] = str(exc) except SkipField: @@ -202,6 +219,7 @@ class Serializer(BaseSerializer): if errors: raise ValidationError(errors) + # TODO: 'Non field errors' return self.validate(ret) def to_primative(self, instance): @@ -340,12 +358,12 @@ class ModelSerializer(Serializer): self.opts = self._options_class(self.Meta) super(ModelSerializer, self).__init__(*args, **kwargs) - def create(self): + def create(self, attrs): ModelClass = self.opts.model - return ModelClass.objects.create(**self.validated_data) + return ModelClass.objects.create(**attrs) - def update(self, obj): - for attr, value in self.validated_data.items(): + def update(self, obj, attrs): + for attr, value in attrs.items(): setattr(obj, attr, value) obj.save() -- cgit v1.2.3 From 21980b800d04a1d82a6003823abfdf4ab80ae979 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Sep 2014 14:24:05 +0100 Subject: More test sorting --- rest_framework/serializers.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 49eb6ce9..93226d32 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,10 +10,10 @@ 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 ValidationError from django.db import models from django.utils import six from collections import namedtuple, 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 @@ -58,13 +58,14 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, extras=None): + attrs = self.validated_data if extras is not None: - self.validated_data.update(extras) + attrs = dict(list(attrs.items()) + list(extras.items())) if self.instance is not None: - self.update(self.instance, self._validated_data) + self.update(self.instance, attrs) else: - self.instance = self.create(self._validated_data) + self.instance = self.create(attrs) return self.instance @@ -74,7 +75,7 @@ class BaseSerializer(Field): self._validated_data = self.to_native(self._initial_data) except ValidationError as exc: self._validated_data = {} - self._errors = exc.detail + self._errors = exc.message_dict else: self._errors = {} @@ -210,7 +211,7 @@ class Serializer(BaseSerializer): if validate_method is not None: validated_value = validate_method(validated_value) except ValidationError as exc: - errors[field.field_name] = str(exc) + errors[field.field_name] = exc.messages except SkipField: pass else: @@ -219,8 +220,10 @@ class Serializer(BaseSerializer): if errors: raise ValidationError(errors) - # TODO: 'Non field errors' - return self.validate(ret) + try: + return self.validate(ret) + except ValidationError, exc: + raise ValidationError({'non_field_errors': exc.messages}) def to_primative(self, instance): """ @@ -539,6 +542,9 @@ class ModelSerializer(Serializer): if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name + if model_field.validators is not None: + kwargs['validators'] = model_field.validators + # if model_field.help_text is not None: # kwargs['help_text'] = model_field.help_text @@ -577,8 +583,7 @@ class ModelSerializer(Serializer): try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: - # TODO: Change this to `return ModelField(model_field=model_field, **kwargs)` - return CharField(**kwargs) + return ModelField(model_field=model_field, **kwargs) class HyperlinkedModelSerializerOptions(ModelSerializerOptions): -- cgit v1.2.3 From b1c07670ca65084c5fef2bbb63d1f4163763014b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Sep 2014 17:46:28 +0100 Subject: Fleshing out serializer fields --- rest_framework/serializers.py | 380 +++++++++++++++++++++--------------------- 1 file changed, 190 insertions(+), 190 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 93226d32..8ca28387 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,15 +10,15 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ +from django.core import validators from django.core.exceptions import ValidationError from django.db import models from django.utils import six from collections import namedtuple, OrderedDict from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings -from rest_framework.utils import html +from rest_framework.utils import html, modelinfo, representation import copy -import inspect # Note: We do the following so that users of the framework can use this style: # @@ -146,12 +146,10 @@ class SerializerMetaclass(type): class Serializer(BaseSerializer): def __new__(cls, *args, **kwargs): - many = kwargs.pop('many', False) - if many: - class DynamicListSerializer(ListSerializer): - child = cls() - return DynamicListSerializer(*args, **kwargs) - return super(Serializer, cls).__new__(cls) + if kwargs.pop('many', False): + kwargs['child'] = cls() + return ListSerializer(*args, **kwargs) + return super(Serializer, cls).__new__(cls, *args, **kwargs) def __init__(self, *args, **kwargs): self.context = kwargs.pop('context', {}) @@ -248,6 +246,9 @@ class Serializer(BaseSerializer): error = errors.get(field.field_name) yield FieldResult(field, value, error) + def __repr__(self): + return representation.serializer_repr(self, indent=1) + class ListSerializer(BaseSerializer): child = None @@ -299,26 +300,8 @@ class ListSerializer(BaseSerializer): self.instance = self.create(self.validated_data) return self.instance - -def _resolve_model(obj): - """ - Resolve supplied `obj` to a Django model class. - - `obj` must be a Django model class itself, or a string - representation of one. Useful in situtations like GH #1225 where - Django may not have resolved a string-based reference to a model in - another model's foreign key definition. - - String representations should have the format: - 'appname.ModelName' - """ - if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: - app_name, model_name = obj.split('.') - return models.get_model(app_name, model_name) - elif inspect.isclass(obj) and issubclass(obj, models.Model): - return obj - else: - raise ValueError("{0} is not a Django model".format(obj)) + def __repr__(self): + return representation.list_repr(self, indent=1) class ModelSerializerOptions(object): @@ -334,24 +317,25 @@ class ModelSerializerOptions(object): class ModelSerializer(Serializer): field_mapping = { models.AutoField: IntegerField, - # models.FloatField: FloatField, + models.BigIntegerField: IntegerField, + models.BooleanField: BooleanField, + models.CharField: CharField, + models.CommaSeparatedIntegerField: CharField, + models.DateField: DateField, + models.DateTimeField: DateTimeField, + models.DecimalField: DecimalField, + models.EmailField: EmailField, + models.FileField: FileField, + models.FloatField: FloatField, models.IntegerField: IntegerField, + models.NullBooleanField: BooleanField, models.PositiveIntegerField: IntegerField, - models.SmallIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, - models.DateTimeField: DateTimeField, - models.DateField: DateField, + models.SlugField: SlugField, + models.SmallIntegerField: IntegerField, + models.TextField: CharField, models.TimeField: TimeField, - # models.DecimalField: DecimalField, - models.EmailField: EmailField, - models.CharField: CharField, models.URLField: URLField, - # models.SlugField: SlugField, - models.TextField: CharField, - models.CommaSeparatedIntegerField: CharField, - models.BooleanField: BooleanField, - models.NullBooleanField: BooleanField, - models.FileField: FileField, # models.ImageField: ImageField, } @@ -392,85 +376,31 @@ class ModelSerializer(Serializer): """ Return all the fields that should be serialized for the model. """ - cls = self.opts.model - opts = cls._meta.concrete_model._meta + info = modelinfo.get_field_info(self.opts.model) ret = OrderedDict() - nested = bool(self.opts.depth) - # Deal with adding the primary key field - pk_field = opts.pk - while pk_field.rel and pk_field.rel.parent_link: - # If model is a child via multitable inheritance, use parent's pk - pk_field = pk_field.rel.to._meta.pk - - serializer_pk_field = self.get_pk_field(pk_field) + serializer_pk_field = self.get_pk_field(info.pk) if serializer_pk_field: - ret[pk_field.name] = serializer_pk_field - - # Deal with forward relationships - forward_rels = [field for field in opts.fields if field.serialize] - forward_rels += [field for field in opts.many_to_many if field.serialize] - - for model_field in forward_rels: - has_through_model = False - - if model_field.rel: - to_many = isinstance(model_field, - models.fields.related.ManyToManyField) - related_model = _resolve_model(model_field.rel.to) - - if to_many and not model_field.rel.through._meta.auto_created: - has_through_model = True + ret[info.pk.name] = serializer_pk_field - if model_field.rel and nested: - field = self.get_nested_field(model_field, related_model, to_many) - elif model_field.rel: - field = self.get_related_field(model_field, related_model, to_many) - else: - field = self.get_field(model_field) - - if field: - if has_through_model: - field.read_only = True + # Regular fields + for field_name, field in info.fields.items(): + ret[field_name] = self.get_field(field) - ret[model_field.name] = field - - # Deal with reverse relationships - if not self.opts.fields: - reverse_rels = [] - else: - # Reverse relationships are only included if they are explicitly - # present in the `fields` option on the serializer - reverse_rels = opts.get_all_related_objects() - reverse_rels += opts.get_all_related_many_to_many_objects() - - for relation in reverse_rels: - accessor_name = relation.get_accessor_name() - if not self.opts.fields or accessor_name not in self.opts.fields: - continue - related_model = relation.model - to_many = relation.field.rel.multiple - has_through_model = False - is_m2m = isinstance(relation.field, - models.fields.related.ManyToManyField) - - if ( - is_m2m and - hasattr(relation.field.rel, 'through') and - not relation.field.rel.through._meta.auto_created - ): - has_through_model = True - - if nested: - field = self.get_nested_field(None, related_model, to_many) + # Forward relations + for field_name, relation_info in info.forward_relations.items(): + if self.opts.depth: + ret[field_name] = self.get_nested_field(*relation_info) else: - field = self.get_related_field(None, related_model, to_many) + ret[field_name] = self.get_related_field(*relation_info) - if field: - if has_through_model: - field.read_only = True - - ret[accessor_name] = field + # Reverse relations + for accessor_name, relation_info in info.reverse_relations.items(): + if accessor_name in self.opts.fields: + if self.opts.depth: + ret[field_name] = self.get_nested_field(*relation_info) + else: + ret[field_name] = self.get_related_field(*relation_info) return ret @@ -480,7 +410,7 @@ class ModelSerializer(Serializer): """ return self.get_field(model_field) - def get_nested_field(self, model_field, related_model, to_many): + def get_nested_field(self, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a nested relational field. @@ -491,59 +421,148 @@ class ModelSerializer(Serializer): model = related_model depth = self.opts.depth - 1 - return NestedModelSerializer(many=to_many) + kwargs = {'read_only': True} + if to_many: + kwargs['many'] = True + return NestedModelSerializer(**kwargs) - def get_related_field(self, model_field, related_model, to_many): + def get_related_field(self, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a flat relational field. Note that model_field will be `None` for reverse relationships. """ - # TODO: filter queryset using: - # .using(db).complex_filter(self.rel.limit_choices_to) + kwargs = { + 'queryset': related_model._default_manager, + } - kwargs = {} - # 'queryset': related_model._default_manager, - # 'many': to_many - # } + if to_many: + kwargs['many'] = True + + if has_through_model: + kwargs['read_only'] = True + kwargs.pop('queryset', None) if model_field: - kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.null or model_field.blank: + kwargs['required'] = False # if model_field.help_text is not None: # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name if not model_field.editable: kwargs['read_only'] = True - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + kwargs.pop('queryset', None) - return IntegerField(**kwargs) - # TODO: return PrimaryKeyRelatedField(**kwargs) + return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): """ Creates a default instance of a basic non-relational field. """ kwargs = {} + validator_kwarg = model_field.validators if model_field.null or model_field.blank: kwargs['required'] = False + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name + if isinstance(model_field, models.AutoField) or not model_field.editable: kwargs['read_only'] = True + # Read only implies that the field is not required. + # We have a cleaner repr on the instance if we don't set it. + kwargs.pop('required', None) if model_field.has_default(): kwargs['default'] = model_field.get_default() - - if issubclass(model_field.__class__, models.TextField): - kwargs['widget'] = widgets.Textarea - - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name - - if model_field.validators is not None: - kwargs['validators'] = model_field.validators + # Having a default implies that the field is not required. + # We have a cleaner repr on the instance if we don't set it. + kwargs.pop('required', None) + + # Ensure that max_length is passed explicitly as a keyword arg, + # rather than as a validator. + max_length = getattr(model_field, 'max_length', None) + if max_length is not None: + kwargs['max_length'] = max_length + validator_kwarg = [ + validator for validator in validator_kwarg + if not isinstance(validator, validators.MaxLengthValidator) + ] + + # Ensure that min_length is passed explicitly as a keyword arg, + # rather than as a validator. + min_length = getattr(model_field, 'min_length', None) + if min_length is not None: + kwargs['min_length'] = min_length + validator_kwarg = [ + validator for validator in validator_kwarg + if not isinstance(validator, validators.MinLengthValidator) + ] + + # Ensure that max_value is passed explicitly as a keyword arg, + # rather than as a validator. + max_value = next(( + validator.limit_value for validator in validator_kwarg + if isinstance(validator, validators.MaxValueValidator) + ), None) + if max_value is not None: + kwargs['max_value'] = max_value + validator_kwarg = [ + validator for validator in validator_kwarg + if not isinstance(validator, validators.MaxValueValidator) + ] + + # Ensure that max_value is passed explicitly as a keyword arg, + # rather than as a validator. + min_value = next(( + validator.limit_value for validator in validator_kwarg + if isinstance(validator, validators.MinValueValidator) + ), None) + if min_value is not None: + kwargs['min_value'] = min_value + validator_kwarg = [ + validator for validator in validator_kwarg + if not isinstance(validator, validators.MinValueValidator) + ] + + # URLField does not need to include the URLValidator argument, + # as it is explicitly added in. + if isinstance(model_field, models.URLField): + validator_kwarg = [ + validator for validator in validator_kwarg + if not isinstance(validator, validators.URLValidator) + ] + + # EmailField does not need to include the validate_email argument, + # as it is explicitly added in. + if isinstance(model_field, models.EmailField): + validator_kwarg = [ + validator for validator in validator_kwarg + if validator is not validators.validate_email + ] + + # SlugField do not need to include the 'validate_slug' argument, + if isinstance(model_field, models.SlugField): + validator_kwarg = [ + validator for validator in validator_kwarg + if validator is not validators.validate_slug + ] + + max_digits = getattr(model_field, 'max_digits', None) + if max_digits is not None: + kwargs['max_digits'] = max_digits + + decimal_places = getattr(model_field, 'decimal_places', None) + if decimal_places is not None: + kwargs['decimal_places'] = decimal_places + + if validator_kwarg: + kwargs['validators'] = validator_kwarg + + # if issubclass(model_field.__class__, models.TextField): + # kwargs['widget'] = widgets.Textarea # if model_field.help_text is not None: # kwargs['help_text'] = model_field.help_text @@ -555,31 +574,10 @@ class ModelSerializer(Serializer): kwargs['empty'] = None return ChoiceField(**kwargs) - # put this below the ChoiceField because min_value isn't a valid initializer - if issubclass(model_field.__class__, models.PositiveIntegerField) or \ - issubclass(model_field.__class__, models.PositiveSmallIntegerField): - kwargs['min_value'] = 0 - if model_field.null and \ issubclass(model_field.__class__, (models.CharField, models.TextField)): kwargs['allow_none'] = True - # attribute_dict = { - # models.CharField: ['max_length'], - # models.CommaSeparatedIntegerField: ['max_length'], - # models.DecimalField: ['max_digits', 'decimal_places'], - # models.EmailField: ['max_length'], - # models.FileField: ['max_length'], - # models.ImageField: ['max_length'], - # models.SlugField: ['max_length'], - # models.URLField: ['max_length'], - # } - - # if model_field.__class__ in attribute_dict: - # attributes = attribute_dict[model_field.__class__] - # for attribute in attributes: - # kwargs.update({attribute: getattr(model_field, attribute)}) - try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: @@ -594,28 +592,21 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) self.lookup_field = getattr(meta, 'lookup_field', None) - self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME) class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions - _default_view_name = '%(model_name)s-detail' - _hyperlink_field_class = HyperlinkedRelatedField - _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() if self.opts.view_name is None: - self.opts.view_name = self._get_default_view_name(self.opts.model) + self.opts.view_name = self.get_default_view_name(self.opts.model) - if self.opts.url_field_name not in fields: - url_field = self._hyperlink_identify_field_class( - view_name=self.opts.view_name, - lookup_field=self.opts.lookup_field - ) + url_field_name = api_settings.URL_FIELD_NAME + if url_field_name not in fields: ret = fields.__class__() - ret[self.opts.url_field_name] = url_field + ret[url_field_name] = self.get_url_field() ret.update(fields) fields = ret @@ -625,39 +616,48 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.fields and model_field.name in self.opts.fields: return self.get_field(model_field) - def get_related_field(self, model_field, related_model, to_many): + def get_url_field(self): + kwargs = { + 'view_name': self.get_default_view_name(self.opts.model) + } + if self.opts.lookup_field: + kwargs['lookup_field'] = self.opts.lookup_field + return HyperlinkedIdentityField(**kwargs) + + def get_related_field(self, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a flat relational field. """ - # TODO: filter queryset using: - # .using(db).complex_filter(self.rel.limit_choices_to) - # kwargs = { - # 'queryset': related_model._default_manager, - # 'view_name': self._get_default_view_name(related_model), - # 'many': to_many - # } - kwargs = {} + kwargs = { + 'queryset': related_model._default_manager, + 'view_name': self.get_default_view_name(related_model), + } + + if to_many: + kwargs['many'] = True + + if has_through_model: + kwargs['read_only'] = True + kwargs.pop('queryset', None) if model_field: - kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.null or model_field.blank: + kwargs['required'] = False # if model_field.help_text is not None: # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name + if not model_field.editable: + kwargs['read_only'] = True + kwargs.pop('queryset', None) - return IntegerField(**kwargs) - # if self.opts.lookup_field: - # kwargs['lookup_field'] = self.opts.lookup_field - - # return self._hyperlink_field_class(**kwargs) + return HyperlinkedRelatedField(**kwargs) - def _get_default_view_name(self, model): + def get_default_view_name(self, model): """ - Return the view name to use if 'view_name' is not specified in 'Meta' + Return the view name to use for related models. """ - model_meta = model._meta - format_kwargs = { - 'app_label': model_meta.app_label, - 'model_name': model_meta.object_name.lower() + return '%(model_name)s-detail' % { + 'app_label': model._meta.app_label, + 'model_name': model._meta.object_name.lower() } - return self._default_view_name % format_kwargs -- cgit v1.2.3 From 234369aefdf08d7d0161d851866990754c00d31f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Sep 2014 08:53:33 +0100 Subject: Tweaks --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8ca28387..0727b8cd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -205,7 +205,7 @@ class Serializer(BaseSerializer): validate_method = getattr(self, 'validate_' + field.field_name, None) primitive_value = field.get_value(data) try: - validated_value = field.validate(primitive_value) + validated_value = field.validate_value(primitive_value) if validate_method is not None: validated_value = validate_method(validated_value) except ValidationError as exc: @@ -327,6 +327,7 @@ class ModelSerializer(Serializer): models.EmailField: EmailField, models.FileField: FileField, models.FloatField: FloatField, + models.ImageField: ImageField, models.IntegerField: IntegerField, models.NullBooleanField: BooleanField, models.PositiveIntegerField: IntegerField, @@ -336,7 +337,6 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, - # models.ImageField: ImageField, } _options_class = ModelSerializerOptions -- cgit v1.2.3 From 01c8c0cad977fc0787dbfc78bd34f4fd37e613f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Sep 2014 13:52:16 +0100 Subject: Added help_text argument to fields --- rest_framework/serializers.py | 86 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 45 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0727b8cd..459f8a8c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,6 +15,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils import six from collections import namedtuple, OrderedDict +from rest_framework.compat import clean_manytomany_helptext from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings from rest_framework.utils import html, modelinfo, representation @@ -117,8 +118,9 @@ class SerializerMetaclass(type): """ This metaclass sets a dictionary named `base_fields` on the class. - Any fields included as attributes on either the class or it's superclasses - will be include in the `base_fields` dictionary. + Any instances of `Field` included as attributes on either the class + or on any of its superclasses will be include in the + `base_fields` dictionary. """ @classmethod @@ -379,6 +381,10 @@ class ModelSerializer(Serializer): info = modelinfo.get_field_info(self.opts.model) ret = OrderedDict() + serializer_url_field = self.get_url_field() + if serializer_url_field: + ret[api_settings.URL_FIELD_NAME] = serializer_url_field + serializer_pk_field = self.get_pk_field(info.pk) if serializer_pk_field: ret[info.pk.name] = serializer_pk_field @@ -404,6 +410,9 @@ class ModelSerializer(Serializer): return ret + def get_url_field(self): + return None + def get_pk_field(self, model_field): """ Returns a default instance of the pk field. @@ -446,13 +455,14 @@ class ModelSerializer(Serializer): if model_field: if model_field.null or model_field.blank: kwargs['required'] = False - # if model_field.help_text is not None: - # kwargs['help_text'] = model_field.help_text - if model_field.verbose_name is not None: + if model_field.verbose_name: kwargs['label'] = model_field.verbose_name if not model_field.editable: kwargs['read_only'] = True kwargs.pop('queryset', None) + help_text = clean_manytomany_helptext(model_field.help_text) + if help_text: + kwargs['help_text'] = help_text return PrimaryKeyRelatedField(**kwargs) @@ -469,6 +479,9 @@ class ModelSerializer(Serializer): if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name + if model_field.help_text: + kwargs['help_text'] = model_field.help_text + if isinstance(model_field, models.AutoField) or not model_field.editable: kwargs['read_only'] = True # Read only implies that the field is not required. @@ -481,6 +494,14 @@ class ModelSerializer(Serializer): # We have a cleaner repr on the instance if we don't set it. kwargs.pop('required', None) + if model_field.flatchoices: + # If this model field contains choices, then use a ChoiceField, + # rather than the standard serializer field for this type. + # Note that we return this prior to setting any validation type + # keyword arguments, as those are not valid initializers. + kwargs['choices'] = model_field.flatchoices + return ChoiceField(**kwargs) + # Ensure that max_length is passed explicitly as a keyword arg, # rather than as a validator. max_length = getattr(model_field, 'max_length', None) @@ -561,23 +582,6 @@ class ModelSerializer(Serializer): if validator_kwarg: kwargs['validators'] = validator_kwarg - # if issubclass(model_field.__class__, models.TextField): - # kwargs['widget'] = widgets.Textarea - - # if model_field.help_text is not None: - # kwargs['help_text'] = model_field.help_text - - # TODO: TypedChoiceField? - if model_field.flatchoices: # This ModelField contains choices - kwargs['choices'] = model_field.flatchoices - if model_field.null: - kwargs['empty'] = None - return ChoiceField(**kwargs) - - if model_field.null and \ - issubclass(model_field.__class__, (models.CharField, models.TextField)): - kwargs['allow_none'] = True - try: return self.field_mapping[model_field.__class__](**kwargs) except KeyError: @@ -597,33 +601,24 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions - def get_default_fields(self): - fields = super(HyperlinkedModelSerializer, self).get_default_fields() - - if self.opts.view_name is None: - self.opts.view_name = self.get_default_view_name(self.opts.model) - - url_field_name = api_settings.URL_FIELD_NAME - if url_field_name not in fields: - ret = fields.__class__() - ret[url_field_name] = self.get_url_field() - ret.update(fields) - fields = ret - - return fields - - def get_pk_field(self, model_field): - if self.opts.fields and model_field.name in self.opts.fields: - return self.get_field(model_field) - def get_url_field(self): + if self.opts.view_name is not None: + view_name = self.opts.view_name + else: + view_name = self.get_default_view_name(self.opts.model) + kwargs = { - 'view_name': self.get_default_view_name(self.opts.model) + 'view_name': view_name } if self.opts.lookup_field: kwargs['lookup_field'] = self.opts.lookup_field + return HyperlinkedIdentityField(**kwargs) + def get_pk_field(self, model_field): + if self.opts.fields and model_field.name in self.opts.fields: + return self.get_field(model_field) + def get_related_field(self, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a flat relational field. @@ -643,13 +638,14 @@ class HyperlinkedModelSerializer(ModelSerializer): if model_field: if model_field.null or model_field.blank: kwargs['required'] = False - # if model_field.help_text is not None: - # kwargs['help_text'] = model_field.help_text - if model_field.verbose_name is not None: + if model_field.verbose_name: kwargs['label'] = model_field.verbose_name if not model_field.editable: kwargs['read_only'] = True kwargs.pop('queryset', None) + help_text = clean_manytomany_helptext(model_field.help_text) + if help_text: + kwargs['help_text'] = help_text return HyperlinkedRelatedField(**kwargs) -- cgit v1.2.3 From 80ba0473473501968154c5cc5dd5922e53d96a70 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Sep 2014 16:57:22 +0100 Subject: Compat fixes --- rest_framework/serializers.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 459f8a8c..13e57939 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,8 @@ from django.core import validators from django.core.exceptions import ValidationError from django.db import models from django.utils import six -from collections import namedtuple, OrderedDict +from django.utils.datastructures import SortedDict +from collections import namedtuple from rest_framework.compat import clean_manytomany_helptext from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings @@ -91,10 +92,10 @@ class BaseSerializer(Field): if self.instance is not None: self._data = self.to_primative(self.instance) elif self._initial_data is not None: - self._data = { - field_name: field.get_value(self._initial_data) + self._data = dict([ + (field_name, field.get_value(self._initial_data)) for field_name, field in self.fields.items() - } + ]) else: self._data = self.get_initial() return self._data @@ -137,7 +138,7 @@ class SerializerMetaclass(type): if hasattr(base, 'base_fields'): fields = list(base.base_fields.items()) + fields - return OrderedDict(fields) + return SortedDict(fields) def __new__(cls, name, bases, attrs): attrs['base_fields'] = cls._get_fields(bases, attrs) @@ -180,10 +181,10 @@ class Serializer(BaseSerializer): field.bind(field_name, self, root) def get_initial(self): - return { - field.field_name: field.get_initial() + return dict([ + (field.field_name, field.get_initial()) for field in self.fields.values() - } + ]) def get_value(self, dictionary): # We override the default field access in order to support @@ -222,14 +223,14 @@ class Serializer(BaseSerializer): try: return self.validate(ret) - except ValidationError, exc: + except ValidationError as exc: raise ValidationError({'non_field_errors': exc.messages}) def to_primative(self, instance): """ Object instance -> Dict of primitive datatypes. """ - ret = OrderedDict() + ret = SortedDict() fields = [field for field in self.fields.values() if not field.write_only] for field in fields: @@ -368,7 +369,7 @@ class ModelSerializer(Serializer): # If `fields` is set on the `Meta` class, # then use only those fields, and in that order. if self.opts.fields: - fields = OrderedDict([ + fields = SortedDict([ (key, fields[key]) for key in self.opts.fields ]) @@ -379,7 +380,7 @@ class ModelSerializer(Serializer): Return all the fields that should be serialized for the model. """ info = modelinfo.get_field_info(self.opts.model) - ret = OrderedDict() + ret = SortedDict() serializer_url_field = self.get_url_field() if serializer_url_field: -- cgit v1.2.3 From ab40780dc2f341a271c2f489659dcd48eb47c07d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Sep 2014 20:22:32 +0100 Subject: Tidy up lookup_class --- rest_framework/serializers.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8fe999ae..4322f213 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -317,17 +317,17 @@ class ModelSerializerOptions(object): self.depth = getattr(meta, 'depth', 0) -def lookup_class(mapping, obj): +def lookup_class(mapping, instance): """ Takes a dictionary with classes as keys, and an object. Traverses the object's inheritance hierarchy in method resolution order, and returns the first matching value - from the dictionary or None. + from the dictionary or raises a KeyError if nothing matches. """ - return next( - (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping), - None - ) + for cls in inspect.getmro(instance.__class__): + if cls in mapping: + return mapping[cls] + raise KeyError('Class %s not found in lookup.', cls.__name__) class ModelSerializer(Serializer): @@ -341,6 +341,7 @@ class ModelSerializer(Serializer): models.DateTimeField: DateTimeField, models.DecimalField: DecimalField, models.EmailField: EmailField, + models.Field: ModelField, models.FileField: FileField, models.FloatField: FloatField, models.ImageField: ImageField, @@ -484,6 +485,7 @@ class ModelSerializer(Serializer): """ Creates a default instance of a basic non-relational field. """ + serializer_cls = lookup_class(self.field_mapping, model_field) kwargs = {} validator_kwarg = model_field.validators @@ -602,11 +604,10 @@ class ModelSerializer(Serializer): if validator_kwarg: kwargs['validators'] = validator_kwarg - cls = lookup_class(self.field_mapping, model_field) - if cls is None: - cls = ModelField + if issubclass(serializer_cls, ModelField): kwargs['model_field'] = model_field - return cls(**kwargs) + + return serializer_cls(**kwargs) class HyperlinkedModelSerializerOptions(ModelSerializerOptions): -- cgit v1.2.3 From adcb64ab4198f35c61d5be68956d201685ed3538 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 12 Sep 2014 09:12:56 +0100 Subject: MethodField -> SerializerMethodField --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4322f213..388fe29f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -598,7 +598,8 @@ class ModelSerializer(Serializer): if isinstance(model_field, models.BooleanField): # models.BooleanField has `blank=True`, but *is* actually # required *unless* a default is provided. - # Also note that <1.6 `default=False`, >=1.6 `default=None`. + # Also note that Django<1.6 uses `default=False` for + # models.BooleanField, but Django>=1.6 uses `default=None`. kwargs.pop('required', None) if validator_kwarg: -- cgit v1.2.3 From 0d354e8f92c7daaf8dac3b80f0fd64f983f21e0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 12 Sep 2014 09:49:35 +0100 Subject: to_internal_value() and to_representation() --- rest_framework/serializers.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 388fe29f..502b1e19 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -47,11 +47,11 @@ class BaseSerializer(Field): self.instance = instance self._initial_data = data - def to_native(self, data): - raise NotImplementedError('`to_native()` must be implemented.') + def to_internal_value(self, data): + raise NotImplementedError('`to_internal_value()` must be implemented.') - def to_primative(self, instance): - raise NotImplementedError('`to_primative()` must be implemented.') + def to_representation(self, instance): + raise NotImplementedError('`to_representation()` must be implemented.') def update(self, instance, attrs): raise NotImplementedError('`update()` must be implemented.') @@ -74,7 +74,7 @@ class BaseSerializer(Field): def is_valid(self, raise_exception=False): if not hasattr(self, '_validated_data'): try: - self._validated_data = self.to_native(self._initial_data) + self._validated_data = self.to_internal_value(self._initial_data) except ValidationError as exc: self._validated_data = {} self._errors = exc.message_dict @@ -90,7 +90,7 @@ class BaseSerializer(Field): def data(self): if not hasattr(self, '_data'): if self.instance is not None: - self._data = self.to_primative(self.instance) + 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)) @@ -193,7 +193,7 @@ class Serializer(BaseSerializer): return html.parse_html_dict(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) - def to_native(self, data): + def to_internal_value(self, data): """ Dict of native values <- Dict of primitive datatypes. """ @@ -208,7 +208,7 @@ class Serializer(BaseSerializer): validate_method = getattr(self, 'validate_' + field.field_name, None) primitive_value = field.get_value(data) try: - validated_value = field.validate_value(primitive_value) + validated_value = field.run_validation(primitive_value) if validate_method is not None: validated_value = validate_method(validated_value) except ValidationError as exc: @@ -226,7 +226,7 @@ class Serializer(BaseSerializer): except ValidationError as exc: raise ValidationError({'non_field_errors': exc.messages}) - def to_primative(self, instance): + def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ @@ -235,7 +235,7 @@ class Serializer(BaseSerializer): for field in fields: native_value = field.get_attribute(instance) - ret[field.field_name] = field.to_primative(native_value) + ret[field.field_name] = field.to_representation(native_value) return ret @@ -279,20 +279,20 @@ class ListSerializer(BaseSerializer): return html.parse_html_list(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) - def to_native(self, data): + def to_internal_value(self, data): """ List of dicts of native values <- List of dicts of primitive datatypes. """ if html.is_html_input(data): data = html.parse_html_list(data) - return [self.child.validate(item) for item in data] + return [self.child.run_validation(item) for item in data] - def to_primative(self, data): + def to_representation(self, data): """ List of object instances -> List of dicts of primitive datatypes. """ - return [self.child.to_primative(item) for item in data] + return [self.child.to_representation(item) for item in data] def create(self, attrs_list): return [self.child.create(attrs) for attrs in attrs_list] -- cgit v1.2.3 From 6db3356c4d1aa4f9a042b0ec67d47238abc16dd7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 12 Sep 2014 10:21:35 +0100 Subject: NON_FIELD_ERRORS_KEY setting --- rest_framework/serializers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 502b1e19..0c2aedfa 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -198,7 +198,9 @@ class Serializer(BaseSerializer): Dict of native values <- Dict of primitive datatypes. """ if not isinstance(data, dict): - raise ValidationError({'non_field_errors': ['Invalid data']}) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data'] + }) ret = {} errors = {} @@ -224,7 +226,9 @@ class Serializer(BaseSerializer): try: return self.validate(ret) except ValidationError as exc: - raise ValidationError({'non_field_errors': exc.messages}) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: exc.messages + }) def to_representation(self, instance): """ -- cgit v1.2.3 From afb28a44ad1737cd6fcd6da50ba9552f38293368 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 12 Sep 2014 21:32:20 +0100 Subject: Dealing with reverse relationships --- rest_framework/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0c2aedfa..ecb2829b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -157,7 +157,7 @@ class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) - kwargs.pop('many', False) + kwargs.pop('many', None) super(Serializer, self).__init__(*args, **kwargs) @@ -423,9 +423,9 @@ class ModelSerializer(Serializer): for accessor_name, relation_info in info.reverse_relations.items(): if accessor_name in self.opts.fields: if self.opts.depth: - ret[field_name] = self.get_nested_field(*relation_info) + ret[accessor_name] = self.get_nested_field(*relation_info) else: - ret[field_name] = self.get_related_field(*relation_info) + ret[accessor_name] = self.get_related_field(*relation_info) return ret @@ -444,7 +444,7 @@ class ModelSerializer(Serializer): Note that model_field will be `None` for reverse relationships. """ - class NestedModelSerializer(ModelSerializer): + class NestedModelSerializer(ModelSerializer): # Not right! class Meta: model = related_model depth = self.opts.depth - 1 -- cgit v1.2.3 From 40dc588a372375608701f7e521dea6d860a49eb2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Sep 2014 09:50:51 +0100 Subject: Drop label from serializer fields when not needed --- rest_framework/serializers.py | 56 +++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 23 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ecb2829b..ba8d475f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,11 +15,12 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict +from django.utils.text import capfirst from collections import namedtuple from rest_framework.compat import clean_manytomany_helptext from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings -from rest_framework.utils import html, modelinfo, representation +from rest_framework.utils import html, model_meta, representation import copy # Note: We do the following so that users of the framework can use this style: @@ -334,6 +335,14 @@ def lookup_class(mapping, instance): raise KeyError('Class %s not found in lookup.', cls.__name__) +def needs_label(model_field, field_name): + """ + Returns `True` if the label based on the model's verbose name + is not equal to the default label it would have based on it's field name. + """ + return capfirst(model_field.verbose_name) != field_name_to_label(field_name) + + class ModelSerializer(Serializer): field_mapping = { models.AutoField: IntegerField, @@ -397,54 +406,55 @@ class ModelSerializer(Serializer): """ Return all the fields that should be serialized for the model. """ - info = modelinfo.get_field_info(self.opts.model) + info = model_meta.get_field_info(self.opts.model) ret = SortedDict() serializer_url_field = self.get_url_field() if serializer_url_field: ret[api_settings.URL_FIELD_NAME] = serializer_url_field - serializer_pk_field = self.get_pk_field(info.pk) + field_name = info.pk.name + serializer_pk_field = self.get_pk_field(field_name, info.pk) if serializer_pk_field: - ret[info.pk.name] = serializer_pk_field + ret[field_name] = serializer_pk_field # Regular fields for field_name, field in info.fields.items(): - ret[field_name] = self.get_field(field) + ret[field_name] = self.get_field(field_name, field) # Forward relations for field_name, relation_info in info.forward_relations.items(): if self.opts.depth: - ret[field_name] = self.get_nested_field(*relation_info) + ret[field_name] = self.get_nested_field(field_name, *relation_info) else: - ret[field_name] = self.get_related_field(*relation_info) + ret[field_name] = self.get_related_field(field_name, *relation_info) # Reverse relations for accessor_name, relation_info in info.reverse_relations.items(): if accessor_name in self.opts.fields: if self.opts.depth: - ret[accessor_name] = self.get_nested_field(*relation_info) + ret[accessor_name] = self.get_nested_field(accessor_name, *relation_info) else: - ret[accessor_name] = self.get_related_field(*relation_info) + ret[accessor_name] = self.get_related_field(accessor_name, *relation_info) return ret def get_url_field(self): return None - def get_pk_field(self, model_field): + def get_pk_field(self, field_name, model_field): """ Returns a default instance of the pk field. """ - return self.get_field(model_field) + return self.get_field(field_name, model_field) - def get_nested_field(self, model_field, related_model, to_many, has_through_model): + def get_nested_field(self, field_name, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a nested relational field. Note that model_field will be `None` for reverse relationships. """ - class NestedModelSerializer(ModelSerializer): # Not right! + class NestedModelSerializer(ModelSerializer): class Meta: model = related_model depth = self.opts.depth - 1 @@ -454,7 +464,7 @@ class ModelSerializer(Serializer): kwargs['many'] = True return NestedModelSerializer(**kwargs) - def get_related_field(self, model_field, related_model, to_many, has_through_model): + def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a flat relational field. @@ -474,8 +484,8 @@ class ModelSerializer(Serializer): if model_field: if model_field.null or model_field.blank: kwargs['required'] = False - if model_field.verbose_name: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name and needs_label(model_field, field_name): + kwargs['label'] = capfirst(model_field.verbose_name) if not model_field.editable: kwargs['read_only'] = True kwargs.pop('queryset', None) @@ -485,7 +495,7 @@ class ModelSerializer(Serializer): return PrimaryKeyRelatedField(**kwargs) - def get_field(self, model_field): + def get_field(self, field_name, model_field): """ Creates a default instance of a basic non-relational field. """ @@ -496,8 +506,8 @@ class ModelSerializer(Serializer): if model_field.null or model_field.blank: kwargs['required'] = False - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name and needs_label(model_field, field_name): + kwargs['label'] = capfirst(model_field.verbose_name) if model_field.help_text: kwargs['help_text'] = model_field.help_text @@ -642,11 +652,11 @@ class HyperlinkedModelSerializer(ModelSerializer): return HyperlinkedIdentityField(**kwargs) - def get_pk_field(self, model_field): + def get_pk_field(self, field_name, model_field): if self.opts.fields and model_field.name in self.opts.fields: return self.get_field(model_field) - def get_related_field(self, model_field, related_model, to_many, has_through_model): + def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model): """ Creates a default instance of a flat relational field. """ @@ -665,8 +675,8 @@ class HyperlinkedModelSerializer(ModelSerializer): if model_field: if model_field.null or model_field.blank: kwargs['required'] = False - if model_field.verbose_name: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name and needs_label(model_field, field_name): + kwargs['label'] = capfirst(model_field.verbose_name) if not model_field.editable: kwargs['read_only'] = True kwargs.pop('queryset', None) -- cgit v1.2.3 From d196608d5af912057baba79ab13d05d876368ad2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Sep 2014 13:55:09 +0100 Subject: Fix nested model serializer base class --- rest_framework/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ba8d475f..40d76897 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -368,6 +368,7 @@ class ModelSerializer(Serializer): models.TimeField: TimeField, models.URLField: URLField, } + nested_class = None # We fill this in at the end of this module. _options_class = ModelSerializerOptions @@ -454,7 +455,7 @@ class ModelSerializer(Serializer): Note that model_field will be `None` for reverse relationships. """ - class NestedModelSerializer(ModelSerializer): + class NestedModelSerializer(self.nested_class): class Meta: model = related_model depth = self.opts.depth - 1 @@ -694,3 +695,7 @@ class HyperlinkedModelSerializer(ModelSerializer): 'app_label': model._meta.app_label, 'model_name': model._meta.object_name.lower() } + + +ModelSerializer.nested_class = ModelSerializer +HyperlinkedModelSerializer.nested_class = HyperlinkedModelSerializer -- cgit v1.2.3 From c0155fd9dc654dc5932effd46a00f66495ce700b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Sep 2014 14:11:53 +0100 Subject: Update comments --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 40d76897..1fea1380 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -410,10 +410,12 @@ class ModelSerializer(Serializer): info = model_meta.get_field_info(self.opts.model) ret = SortedDict() + # URL field serializer_url_field = self.get_url_field() if serializer_url_field: ret[api_settings.URL_FIELD_NAME] = serializer_url_field + # Primary key field field_name = info.pk.name serializer_pk_field = self.get_pk_field(field_name, info.pk) if serializer_pk_field: -- cgit v1.2.3 From 5b7e4af0d657a575cb15eea85a63a7100c636085 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2014 11:20:56 +0100 Subject: get_base_field() refactor --- rest_framework/serializers.py | 464 ++++++++++-------------------------------- 1 file changed, 104 insertions(+), 360 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1fea1380..99dcc349 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,17 +10,19 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -from django.core import validators from django.core.exceptions import ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict -from django.utils.text import capfirst from collections import namedtuple -from rest_framework.compat import clean_manytomany_helptext 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 +) import copy # Note: We do the following so that users of the framework can use this style: @@ -126,7 +128,7 @@ class SerializerMetaclass(type): """ @classmethod - def _get_fields(cls, bases, attrs): + def _get_declared_fields(cls, bases, attrs): fields = [(field_name, attrs.pop(field_name)) for field_name, obj in list(attrs.items()) if isinstance(obj, Field)] @@ -136,25 +138,18 @@ class SerializerMetaclass(type): # fields. Note that we loop over the bases in *reverse*. This is necessary # in order to maintain the correct order of fields. for base in bases[::-1]: - if hasattr(base, 'base_fields'): - fields = list(base.base_fields.items()) + fields + if hasattr(base, '_declared_fields'): + fields = list(base._declared_fields.items()) + fields return SortedDict(fields) def __new__(cls, name, bases, attrs): - attrs['base_fields'] = cls._get_fields(bases, attrs) + attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): - - def __new__(cls, *args, **kwargs): - if kwargs.pop('many', False): - kwargs['child'] = cls() - return ListSerializer(*args, **kwargs) - return super(Serializer, cls).__new__(cls, *args, **kwargs) - def __init__(self, *args, **kwargs): self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) @@ -165,14 +160,22 @@ class Serializer(BaseSerializer): # 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_fields() + 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 get_fields(self): - return copy.deepcopy(self.base_fields) + 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 @@ -312,39 +315,8 @@ class ListSerializer(BaseSerializer): return representation.list_repr(self, indent=1) -class ModelSerializerOptions(object): - """ - Meta class options for ModelSerializer - """ - def __init__(self, meta): - self.model = getattr(meta, 'model') - self.fields = getattr(meta, 'fields', ()) - self.depth = getattr(meta, 'depth', 0) - - -def lookup_class(mapping, instance): - """ - Takes a dictionary with classes as keys, and an object. - Traverses the object's inheritance hierarchy in method - resolution order, and returns the first matching value - from the dictionary or raises a KeyError if nothing matches. - """ - for cls in inspect.getmro(instance.__class__): - if cls in mapping: - return mapping[cls] - raise KeyError('Class %s not found in lookup.', cls.__name__) - - -def needs_label(model_field, field_name): - """ - Returns `True` if the label based on the model's verbose name - is not equal to the default label it would have based on it's field name. - """ - return capfirst(model_field.verbose_name) != field_name_to_label(field_name) - - class ModelSerializer(Serializer): - field_mapping = { + _field_mapping = { models.AutoField: IntegerField, models.BigIntegerField: IntegerField, models.BooleanField: BooleanField, @@ -368,16 +340,10 @@ class ModelSerializer(Serializer): models.TimeField: TimeField, models.URLField: URLField, } - nested_class = None # We fill this in at the end of this module. - - _options_class = ModelSerializerOptions - - def __init__(self, *args, **kwargs): - self.opts = self._options_class(self.Meta) - super(ModelSerializer, self).__init__(*args, **kwargs) + _related_class = PrimaryKeyRelatedField def create(self, attrs): - ModelClass = self.opts.model + ModelClass = self.Meta.model return ModelClass.objects.create(**attrs) def update(self, obj, attrs): @@ -385,319 +351,97 @@ class ModelSerializer(Serializer): setattr(obj, attr, value) obj.save() - def get_fields(self): - # Get the explicitly declared fields. - fields = copy.deepcopy(self.base_fields) + def _get_base_fields(self): + declared_fields = copy.deepcopy(self._declared_fields) - # Add in the default fields. - for key, val in self.get_default_fields().items(): - if key not in fields: - fields[key] = val - - # If `fields` is set on the `Meta` class, - # then use only those fields, and in that order. - if self.opts.fields: - fields = SortedDict([ - (key, fields[key]) for key in self.opts.fields - ]) - - return fields - - def get_default_fields(self): - """ - Return all the fields that should be serialized for the model. - """ - info = model_meta.get_field_info(self.opts.model) ret = SortedDict() + model = getattr(self.Meta, 'model') + fields = getattr(self.Meta, 'fields', None) + depth = getattr(self.Meta, 'depth', 0) + + # 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. + if fields is None: + fields = self._get_default_field_names(declared_fields, info) + + 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) + 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) + + 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) - # URL field - serializer_url_field = self.get_url_field() - if serializer_url_field: - ret[api_settings.URL_FIELD_NAME] = serializer_url_field - - # Primary key field - field_name = info.pk.name - serializer_pk_field = self.get_pk_field(field_name, info.pk) - if serializer_pk_field: - ret[field_name] = serializer_pk_field - - # Regular fields - for field_name, field in info.fields.items(): - ret[field_name] = self.get_field(field_name, field) - - # Forward relations - for field_name, relation_info in info.forward_relations.items(): - if self.opts.depth: - ret[field_name] = self.get_nested_field(field_name, *relation_info) else: - ret[field_name] = self.get_related_field(field_name, *relation_info) + assert False, 'Field name `%s` is not valid.' % field_name - # Reverse relations - for accessor_name, relation_info in info.reverse_relations.items(): - if accessor_name in self.opts.fields: - if self.opts.depth: - ret[accessor_name] = self.get_nested_field(accessor_name, *relation_info) - else: - ret[accessor_name] = self.get_related_field(accessor_name, *relation_info) + ret[field_name] = field_cls(**kwargs) return ret - def get_url_field(self): - return None - - def get_pk_field(self, field_name, model_field): - """ - Returns a default instance of the pk field. - """ - return self.get_field(field_name, model_field) - - def get_nested_field(self, field_name, model_field, related_model, to_many, has_through_model): - """ - Creates a default instance of a nested relational field. + def _get_default_field_names(self, declared_fields, model_info): + return ( + [model_info.pk.name] + + list(declared_fields.keys()) + + list(model_info.fields.keys()) + + list(model_info.forward_relations.keys()) + ) - Note that model_field will be `None` for reverse relationships. - """ - class NestedModelSerializer(self.nested_class): + def _get_nested_class(self, nested_depth, relation_info): + class NestedSerializer(ModelSerializer): class Meta: - model = related_model - depth = self.opts.depth - 1 - - kwargs = {'read_only': True} - if to_many: - kwargs['many'] = True - return NestedModelSerializer(**kwargs) - - def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model): - """ - Creates a default instance of a flat relational field. - - Note that model_field will be `None` for reverse relationships. - """ - kwargs = { - 'queryset': related_model._default_manager, - } - - if to_many: - kwargs['many'] = True - - if has_through_model: - kwargs['read_only'] = True - kwargs.pop('queryset', None) - - if model_field: - if model_field.null or model_field.blank: - kwargs['required'] = False - if model_field.verbose_name and needs_label(model_field, field_name): - kwargs['label'] = capfirst(model_field.verbose_name) - if not model_field.editable: - kwargs['read_only'] = True - kwargs.pop('queryset', None) - help_text = clean_manytomany_helptext(model_field.help_text) - if help_text: - kwargs['help_text'] = help_text - - return PrimaryKeyRelatedField(**kwargs) - - def get_field(self, field_name, model_field): - """ - Creates a default instance of a basic non-relational field. - """ - serializer_cls = lookup_class(self.field_mapping, model_field) - kwargs = {} - validator_kwarg = model_field.validators - - if model_field.null or model_field.blank: - kwargs['required'] = False - - if model_field.verbose_name and needs_label(model_field, field_name): - kwargs['label'] = capfirst(model_field.verbose_name) - - if model_field.help_text: - kwargs['help_text'] = model_field.help_text - - if isinstance(model_field, models.AutoField) or not model_field.editable: - kwargs['read_only'] = True - # Read only implies that the field is not required. - # We have a cleaner repr on the instance if we don't set it. - kwargs.pop('required', None) - - if model_field.has_default(): - kwargs['default'] = model_field.get_default() - # Having a default implies that the field is not required. - # We have a cleaner repr on the instance if we don't set it. - kwargs.pop('required', None) - - if model_field.flatchoices: - # If this model field contains choices, then use a ChoiceField, - # rather than the standard serializer field for this type. - # Note that we return this prior to setting any validation type - # keyword arguments, as those are not valid initializers. - kwargs['choices'] = model_field.flatchoices - return ChoiceField(**kwargs) - - # Ensure that max_length is passed explicitly as a keyword arg, - # rather than as a validator. - max_length = getattr(model_field, 'max_length', None) - if max_length is not None: - kwargs['max_length'] = max_length - validator_kwarg = [ - validator for validator in validator_kwarg - if not isinstance(validator, validators.MaxLengthValidator) - ] - - # Ensure that min_length is passed explicitly as a keyword arg, - # rather than as a validator. - min_length = getattr(model_field, 'min_length', None) - if min_length is not None: - kwargs['min_length'] = min_length - validator_kwarg = [ - validator for validator in validator_kwarg - if not isinstance(validator, validators.MinLengthValidator) - ] - - # Ensure that max_value is passed explicitly as a keyword arg, - # rather than as a validator. - max_value = next(( - validator.limit_value for validator in validator_kwarg - if isinstance(validator, validators.MaxValueValidator) - ), None) - if max_value is not None: - kwargs['max_value'] = max_value - validator_kwarg = [ - validator for validator in validator_kwarg - if not isinstance(validator, validators.MaxValueValidator) - ] - - # Ensure that max_value is passed explicitly as a keyword arg, - # rather than as a validator. - min_value = next(( - validator.limit_value for validator in validator_kwarg - if isinstance(validator, validators.MinValueValidator) - ), None) - if min_value is not None: - kwargs['min_value'] = min_value - validator_kwarg = [ - validator for validator in validator_kwarg - if not isinstance(validator, validators.MinValueValidator) - ] - - # URLField does not need to include the URLValidator argument, - # as it is explicitly added in. - if isinstance(model_field, models.URLField): - validator_kwarg = [ - validator for validator in validator_kwarg - if not isinstance(validator, validators.URLValidator) - ] - - # EmailField does not need to include the validate_email argument, - # as it is explicitly added in. - if isinstance(model_field, models.EmailField): - validator_kwarg = [ - validator for validator in validator_kwarg - if validator is not validators.validate_email - ] - - # SlugField do not need to include the 'validate_slug' argument, - if isinstance(model_field, models.SlugField): - validator_kwarg = [ - validator for validator in validator_kwarg - if validator is not validators.validate_slug - ] - - max_digits = getattr(model_field, 'max_digits', None) - if max_digits is not None: - kwargs['max_digits'] = max_digits - - decimal_places = getattr(model_field, 'decimal_places', None) - if decimal_places is not None: - kwargs['decimal_places'] = decimal_places - - if isinstance(model_field, models.BooleanField): - # models.BooleanField has `blank=True`, but *is* actually - # required *unless* a default is provided. - # Also note that Django<1.6 uses `default=False` for - # models.BooleanField, but Django>=1.6 uses `default=None`. - kwargs.pop('required', None) - - if validator_kwarg: - kwargs['validators'] = validator_kwarg - - if issubclass(serializer_cls, ModelField): - kwargs['model_field'] = model_field - - return serializer_cls(**kwargs) - - -class HyperlinkedModelSerializerOptions(ModelSerializerOptions): - """ - Options for HyperlinkedModelSerializer - """ - def __init__(self, meta): - super(HyperlinkedModelSerializerOptions, self).__init__(meta) - self.view_name = getattr(meta, 'view_name', None) - self.lookup_field = getattr(meta, 'lookup_field', None) + model = relation_info.related + depth = nested_depth + return NestedSerializer class HyperlinkedModelSerializer(ModelSerializer): - _options_class = HyperlinkedModelSerializerOptions - - def get_url_field(self): - if self.opts.view_name is not None: - view_name = self.opts.view_name - else: - view_name = self.get_default_view_name(self.opts.model) - - kwargs = { - 'view_name': view_name - } - if self.opts.lookup_field: - kwargs['lookup_field'] = self.opts.lookup_field - - return HyperlinkedIdentityField(**kwargs) - - def get_pk_field(self, field_name, model_field): - if self.opts.fields and model_field.name in self.opts.fields: - return self.get_field(model_field) - - def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model): - """ - Creates a default instance of a flat relational field. - """ - kwargs = { - 'queryset': related_model._default_manager, - 'view_name': self.get_default_view_name(related_model), - } - - if to_many: - kwargs['many'] = True - - if has_through_model: - kwargs['read_only'] = True - kwargs.pop('queryset', None) - - if model_field: - if model_field.null or model_field.blank: - kwargs['required'] = False - if model_field.verbose_name and needs_label(model_field, field_name): - kwargs['label'] = capfirst(model_field.verbose_name) - if not model_field.editable: - kwargs['read_only'] = True - kwargs.pop('queryset', None) - help_text = clean_manytomany_helptext(model_field.help_text) - if help_text: - kwargs['help_text'] = help_text - - return HyperlinkedRelatedField(**kwargs) - - def get_default_view_name(self, model): - """ - Return the view name to use for related models. - """ - return '%(model_name)s-detail' % { - 'app_label': model._meta.app_label, - 'model_name': model._meta.object_name.lower() - } - - -ModelSerializer.nested_class = ModelSerializer -HyperlinkedModelSerializer.nested_class = HyperlinkedModelSerializer + _related_class = HyperlinkedRelatedField + + def _get_default_field_names(self, declared_fields, model_info): + return ( + [api_settings.URL_FIELD_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(HyperlinkedModelSerializer): + class Meta: + model = relation_info.related + depth = nested_depth + return NestedSerializer -- cgit v1.2.3 From 87734be5f41de921ac32ad1f6664db243aab6d07 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2014 12:17:21 +0100 Subject: Configuration correctness tests on ModelSerializer --- rest_framework/serializers.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 99dcc349..9f3e53fd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,7 +10,7 @@ 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 ValidationError +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict @@ -358,6 +358,7 @@ class ModelSerializer(Serializer): model = getattr(self.Meta, 'model') fields = getattr(self.Meta, 'fields', None) depth = getattr(self.Meta, 'depth', 0) + extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) @@ -405,9 +406,32 @@ class ModelSerializer(Serializer): if not issubclass(field_cls, HyperlinkedRelatedField): kwargs.pop('view_name', None) - else: - assert False, 'Field name `%s` is not valid.' % field_name + elif hasattr(model, field_name): + # Create a read only field for model methods and properties. + field_cls = ReadOnlyField + kwargs = {} + else: + raise ImproperlyConfigured( + 'Field name `%s` is not valid for model `%s`.' % + (field_name, model.__class__.__name__) + ) + + # Check that any fields declared on the class are + # also explicity 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` + kwargs.update(extra_kwargs.get(field_name, {})) + + # Create the serializer field. ret[field_name] = field_cls(**kwargs) return ret -- cgit v1.2.3 From 106362b437f45e04faaea759df57a66a8a2d7cfd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2014 14:58:08 +0100 Subject: ModelSerializer.create() to handle many to many by default --- rest_framework/serializers.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9f3e53fd..03e20df8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -344,7 +344,25 @@ class ModelSerializer(Serializer): def create(self, attrs): ModelClass = self.Meta.model - return ModelClass.objects.create(**attrs) + + # Remove many-to-many relationships from 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 key, relation_info in info.relations.items(): + if relation_info.to_many and (key in attrs): + many_to_many[key] = attrs.pop(key) + + instance = ModelClass.objects.create(**attrs) + + # Save many to many relationships after the instance is created. + if many_to_many: + for key, value in many_to_many.items(): + setattr(instance, key, value) + instance.save() + + return instance def update(self, obj, attrs): for attr, value in attrs.items(): -- cgit v1.2.3 From f90049316a3ecca6c92e10b57bfa5becbceff386 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2014 15:47:27 +0100 Subject: Added a model update integration test --- rest_framework/serializers.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 03e20df8..d2740fc2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -350,17 +350,16 @@ class ModelSerializer(Serializer): # as they require that the instance has already been saved. info = model_meta.get_field_info(ModelClass) many_to_many = {} - for key, relation_info in info.relations.items(): - if relation_info.to_many and (key in attrs): - many_to_many[key] = attrs.pop(key) + 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) instance = ModelClass.objects.create(**attrs) - # Save many to many relationships after the instance is created. + # Save many-to-many relationships after the instance is created. if many_to_many: - for key, value in many_to_many.items(): - setattr(instance, key, value) - instance.save() + for field_name, value in many_to_many.items(): + setattr(instance, field_name, value) return instance -- cgit v1.2.3 From cf72b9a8b755652cec4ad19a27488e3a79c2e401 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Sep 2014 16:43:13 +0100 Subject: Moar tests --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d2740fc2..d9f9c8cb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -24,6 +24,7 @@ from rest_framework.utils.field_mapping import ( lookup_class ) import copy +import inspect # Note: We do the following so that users of the framework can use this style: # @@ -268,6 +269,7 @@ class ListSerializer(BaseSerializer): 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.' + assert not inspect.isclass(self.child), '`child` has not been instantiated.' self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) -- cgit v1.2.3 From f22d0afc3dfc7478e084d1d6ed6b53f71641dec6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 23 Sep 2014 14:15:00 +0100 Subject: Tests for field choices --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d9f9c8cb..949f5915 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -411,6 +411,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. -- cgit v1.2.3 From 0404f09a7e69f533038d47ca25caad90c0c2659f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 23 Sep 2014 14:30:17 +0100 Subject: NullBooleanField --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 949f5915..d8d72a4c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -333,7 +333,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, -- cgit v1.2.3 From f4b1dcb167be0bbdaae2cc2a92f651536896dc16 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Sep 2014 14:09:49 +0100 Subject: OPTIONS support --- rest_framework/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d8d72a4c..8902294b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -21,7 +21,7 @@ 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 ) import copy import inspect @@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer): class ModelSerializer(Serializer): - _field_mapping = { + _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, models.BigIntegerField: IntegerField, models.BooleanField: BooleanField, @@ -341,7 +341,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, - } + }) _related_class = PrimaryKeyRelatedField def create(self, attrs): @@ -400,7 +400,7 @@ class ModelSerializer(Serializer): 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` -- cgit v1.2.3 From b22c9602fa0f717b688fdb35e4f6f42c189af3f3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 11:04:18 +0100 Subject: Automatic field binding --- rest_framework/serializers.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8902294b..12e38090 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -149,6 +149,28 @@ class SerializerMetaclass(type): return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) +class BindingDict(object): + def __init__(self, serializer): + self.serializer = serializer + self.fields = SortedDict() + + def __setitem__(self, key, field): + self.fields[key] = field + field.bind(field_name=key, parent=self.serializer, root=self.serializer) + + def __getitem__(self, key): + return self.fields[key] + + def __delitem__(self, key): + del self.fields[key] + + def items(self): + return self.fields.items() + + def values(self): + return self.fields.values() + + @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): @@ -161,11 +183,9 @@ class Serializer(BaseSerializer): # 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) + self.fields = BindingDict(self) + for key, value in self._get_base_fields().items(): + self.fields[key] = value def __new__(cls, *args, **kwargs): # We override this method in order to automagically create -- cgit v1.2.3 From 64632da3718f501cb8174243385d38b547c2fefd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 11:40:32 +0100 Subject: Clean up bind - no longer needs to be called multiple times in nested fields --- rest_framework/serializers.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 12e38090..04721c7a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -150,13 +150,20 @@ class SerializerMetaclass(type): class BindingDict(object): + """ + This dict-like object is used to store fields on a serializer. + + This ensures that whenever fields are added to the serializer we call + `field.bind()` so that the `field_name` and `parent` attributes + can be set correctly. + """ def __init__(self, serializer): self.serializer = serializer self.fields = SortedDict() def __setitem__(self, key, field): self.fields[key] = field - field.bind(field_name=key, parent=self.serializer, root=self.serializer) + field.bind(field_name=key, parent=self.serializer) def __getitem__(self, key): return self.fields[key] @@ -174,7 +181,6 @@ class BindingDict(object): @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) @@ -198,13 +204,6 @@ class Serializer(BaseSerializer): 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_initial(self): return dict([ (field.field_name, field.get_initial()) @@ -290,17 +289,10 @@ class ListSerializer(BaseSerializer): self.child = kwargs.pop('child', copy.deepcopy(self.child)) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' - self.context = kwargs.pop('context', {}) kwargs.pop('partial', None) super(ListSerializer, self).__init__(*args, **kwargs) - self.child.bind('', self, 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) + self.child.bind(field_name='', parent=self) def get_value(self, dictionary): # We override the default field access in order to support -- cgit v1.2.3 From 417fe1b675bd1d42518fb89a6f81547caef5b735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 13:37:26 +0100 Subject: Partial support --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 04721c7a..b6a1898c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -181,8 +181,9 @@ class BindingDict(object): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): - kwargs.pop('partial', None) kwargs.pop('many', None) + self.partial = kwargs.pop('partial', False) + self._context = kwargs.pop('context', {}) super(Serializer, self).__init__(*args, **kwargs) @@ -289,7 +290,8 @@ class ListSerializer(BaseSerializer): self.child = kwargs.pop('child', copy.deepcopy(self.child)) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' - kwargs.pop('partial', None) + self.partial = kwargs.pop('partial', False) + self._context = kwargs.pop('context', {}) super(ListSerializer, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) -- cgit v1.2.3 From 2859eaf524bca23f27e666d24a0b63ba61698a76 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 10:46:52 +0100 Subject: request.data attribute --- rest_framework/serializers.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b6a1898c..a2b878ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -57,21 +57,24 @@ 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 + validated_data = self.validated_data if extras is not None: - attrs = dict(list(attrs.items()) + list(extras.items())) + validated_data = dict( + list(validated_data.items()) + + list(extras.items()) + ) if self.instance is not None: - self.update(self.instance, attrs) + self.update(self.instance, validated_data) else: - self.instance = self.create(attrs) + self.instance = self.create(validated_data) return self.instance @@ -321,12 +324,6 @@ class ListSerializer(BaseSerializer): def create(self, attrs_list): return [self.child.create(attrs) for attrs in attrs_list] - def save(self): - if self.instance is not None: - self.update(self.instance, self.validated_data) - self.instance = self.create(self.validated_data) - return self.instance - def __repr__(self): return representation.list_repr(self, indent=1) -- cgit v1.2.3 From 43e80c74b225e17edfe8a90da893823bf50b946f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 11:56:29 +0100 Subject: Release notes --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a2b878ec..86bed773 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -75,6 +75,9 @@ class BaseSerializer(Field): self.update(self.instance, validated_data) else: self.instance = self.create(validated_data) + assert self.instance is not None, ( + '`create()` did not return an object instance.' + ) return self.instance -- cgit v1.2.3 From 8b8623c5f84d443d26804cac52a793a3037a1dd0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 12:48:20 +0100 Subject: Allow many, partial and context in BaseSerializer --- rest_framework/serializers.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 86bed773..245ec26f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -47,9 +47,20 @@ class BaseSerializer(Field): """ 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): + kwargs['child'] = cls() + return ListSerializer(*args, **kwargs) + return super(BaseSerializer, cls).__new__(cls, *args, **kwargs) def to_internal_value(self, data): raise NotImplementedError('`to_internal_value()` must be implemented.') @@ -187,10 +198,6 @@ class BindingDict(object): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): - kwargs.pop('many', None) - self.partial = kwargs.pop('partial', False) - self._context = kwargs.pop('context', {}) - super(Serializer, self).__init__(*args, **kwargs) # Every new serializer is created with a clone of the field instances. @@ -200,14 +207,6 @@ class Serializer(BaseSerializer): for key, value in self._get_base_fields().items(): self.fields[key] = value - 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) @@ -296,9 +295,6 @@ class ListSerializer(BaseSerializer): self.child = kwargs.pop('child', copy.deepcopy(self.child)) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' - self.partial = kwargs.pop('partial', False) - self._context = kwargs.pop('context', {}) - super(ListSerializer, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) -- cgit v1.2.3 From 2e87de01430d7fec83f00948e60c8d61b317053b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 13:08:20 +0100 Subject: Added ListField --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 245ec26f..fa2e8fb1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -287,6 +287,9 @@ class Serializer(BaseSerializer): return representation.serializer_repr(self, indent=1) +# There's some replication of `ListField` here, +# but that's probably better than obfuscating the call hierarchy. + class ListSerializer(BaseSerializer): child = None initial = [] @@ -301,7 +304,7 @@ class ListSerializer(BaseSerializer): def get_value(self, dictionary): # 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) @@ -311,7 +314,6 @@ 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] def to_representation(self, data): -- cgit v1.2.3 From 609014460861fdfe82054551790d6439292dde7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 14:32:44 +0100 Subject: Simplify serialization slightly --- rest_framework/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fa2e8fb1..080b958d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -268,8 +268,7 @@ class Serializer(BaseSerializer): 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) + ret[field.field_name] = field.get_field_representation(instance) return ret -- cgit v1.2.3 From 9805a085fb115785f272489dc24b51ba6f8e6329 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Sep 2014 11:23:02 +0100 Subject: UniqueTogetherValidator --- rest_framework/serializers.py | 80 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 9 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 080b958d..09ad376a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -23,6 +23,7 @@ from rest_framework.utils.field_mapping import ( get_relation_kwargs, get_nested_relation_kwargs, ClassLookupDict ) +from rest_framework.validators import UniqueTogetherValidator import copy import inspect @@ -95,7 +96,7 @@ class BaseSerializer(Field): def is_valid(self, raise_exception=False): 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 @@ -223,15 +224,43 @@ 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): raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data'] }) + value = self.to_internal_value(data) + try: + self.run_validators(value) + self.validate(value) + except ValidationError as exc: + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: exc.messages + }) + return value + + def to_internal_value(self, data): + """ + Dict of native values <- Dict of primitive datatypes. + """ ret = {} errors = {} fields = [field for field in self.fields.values() if not field.read_only] @@ -253,12 +282,7 @@ 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): """ @@ -355,6 +379,14 @@ class ModelSerializer(Serializer): }) _related_class = PrimaryKeyRelatedField + def __init__(self, *args, **kwargs): + super(ModelSerializer, self).__init__(*args, **kwargs) + if 'validators' not in kwargs: + validators = self.get_unique_together_validators() + if validators: + self.validators.extend(validators) + self._kwargs['validators'] = validators + def create(self, attrs): ModelClass = self.Meta.model @@ -381,6 +413,36 @@ class ModelSerializer(Serializer): setattr(obj, attr, value) obj.save() + def get_unique_together_validators(self): + field_names = set([ + field.source for field in self.fields.values() + if (field.source != '*') and ('.' not in field.source) + ]) + + validators = [] + model_class = self.Meta.model + + for unique_together in model_class._meta.unique_together: + if field_names.issuperset(set(unique_together)): + validator = UniqueTogetherValidator( + queryset=model_class._default_manager, + fields=unique_together + ) + validator.serializer_field = self + validators.append(validator) + + for parent_class in 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 + ) + validator.serializer_field = self + validators.append(validator) + + return validators + def _get_base_fields(self): declared_fields = copy.deepcopy(self._declared_fields) -- cgit v1.2.3 From d1b2c8ac7faec65483cbddf4f1718ca4f5805246 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Sep 2014 14:12:26 +0100 Subject: Absolute URLs for file fields --- rest_framework/serializers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 09ad376a..0faa5671 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -428,7 +428,6 @@ class ModelSerializer(Serializer): queryset=model_class._default_manager, fields=unique_together ) - validator.serializer_field = self validators.append(validator) for parent_class in model_class._meta.parents.keys(): @@ -438,7 +437,6 @@ class ModelSerializer(Serializer): queryset=parent_class._default_manager, fields=unique_together ) - validator.serializer_field = self validators.append(validator) return validators -- cgit v1.2.3 From c171fa21ac62538331755524057d2435f33ec8a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 1 Oct 2014 19:44:46 +0100 Subject: First pass at HTML form rendering --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0faa5671..5da81247 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -302,6 +302,8 @@ class Serializer(BaseSerializer): def __iter__(self): errors = self.errors if hasattr(self, '_errors') else {} for field in self.fields.values(): + if field.read_only: + continue value = self.data.get(field.field_name) if self.data else None error = errors.get(field.field_name) yield FieldResult(field, value, error) -- cgit v1.2.3 From df7b6fcf58417fd95e49655eb140b387899b1ceb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Oct 2014 16:24:24 +0100 Subject: First pass on incorperating the form rendering into the browsable API --- rest_framework/serializers.py | 129 ++++++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 37 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5da81247..0f24ed40 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,6 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict -from collections import namedtuple 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 @@ -38,8 +37,8 @@ from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA -FieldResult = namedtuple('FieldResult', ['field', 'value', 'error']) - +# BaseSerializer +# -------------- class BaseSerializer(Field): """ @@ -113,11 +112,6 @@ class BaseSerializer(Field): if not hasattr(self, '_data'): if self.instance is not 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() - ]) else: self._data = self.get_initial() return self._data @@ -137,34 +131,48 @@ class BaseSerializer(Field): return self._validated_data -class SerializerMetaclass(type): +# Serializer & ListSerializer classes +# ----------------------------------- + +class ReturnDict(SortedDict): """ - This metaclass sets a dictionary named `base_fields` on the class. + Return object from `serialier.data` for the `Serializer` class. + Includes a backlink to the serializer instance for renderers + to use if they need richer field information. + """ + def __init__(self, *args, **kwargs): + self.serializer = kwargs.pop('serializer') + super(ReturnDict, self).__init__(*args, **kwargs) - Any instances of `Field` included as attributes on either the class - or on any of its superclasses will be include in the - `base_fields` dictionary. + +class ReturnList(list): + """ + Return object from `serialier.data` for the `SerializerList` class. + Includes a backlink to the serializer instance for renderers + to use if they need richer field information. """ + def __init__(self, *args, **kwargs): + self.serializer = kwargs.pop('serializer') + super(ReturnList, self).__init__(*args, **kwargs) - @classmethod - def _get_declared_fields(cls, bases, attrs): - fields = [(field_name, attrs.pop(field_name)) - for field_name, obj in list(attrs.items()) - if isinstance(obj, Field)] - fields.sort(key=lambda x: x[1]._creation_counter) - # If this class is subclassing another Serializer, add that Serializer's - # fields. Note that we loop over the bases in *reverse*. This is necessary - # in order to maintain the correct order of fields. - for base in bases[::-1]: - if hasattr(base, '_declared_fields'): - fields = list(base._declared_fields.items()) + fields +class BoundField(object): + """ + A field object that also includes `.value` and `.error` properties. + Returned when iterating over a serializer instance, + providing an API similar to Django forms and form fields. + """ + def __init__(self, field, value, errors): + self._field = field + self.value = value + self.errors = errors - return SortedDict(fields) + def __getattr__(self, attr_name): + return getattr(self._field, attr_name) - def __new__(cls, name, bases, attrs): - attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) - return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) + @property + def _proxy_class(self): + return self._field.__class__ class BindingDict(object): @@ -196,6 +204,36 @@ class BindingDict(object): return self.fields.values() +class SerializerMetaclass(type): + """ + This metaclass sets a dictionary named `base_fields` on the class. + + Any instances of `Field` included as attributes on either the class + or on any of its superclasses will be include in the + `base_fields` dictionary. + """ + + @classmethod + def _get_declared_fields(cls, bases, attrs): + fields = [(field_name, attrs.pop(field_name)) + for field_name, obj in list(attrs.items()) + if isinstance(obj, Field)] + fields.sort(key=lambda x: x[1]._creation_counter) + + # If this class is subclassing another Serializer, add that Serializer's + # fields. Note that we loop over the bases in *reverse*. This is necessary + # in order to maintain the correct order of fields. + for base in bases[::-1]: + if hasattr(base, '_declared_fields'): + fields = list(base._declared_fields.items()) + fields + + return SortedDict(fields) + + def __new__(cls, name, bases, attrs): + attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) + return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) + + @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): @@ -212,10 +250,18 @@ class Serializer(BaseSerializer): return copy.deepcopy(self._declared_fields) def get_initial(self): - return dict([ + if self._initial_data is not None: + return ReturnDict([ + (field_name, field.get_value(self._initial_data)) + for field_name, field in self.fields.items() + ], serializer=self) + #return self.to_representation(self._initial_data) + + return ReturnDict([ (field.field_name, field.get_initial()) for field in self.fields.values() - ]) + if not field.write_only + ], serializer=self) def get_value(self, dictionary): # We override the default field access in order to support @@ -288,7 +334,7 @@ class Serializer(BaseSerializer): """ Object instance -> Dict of primitive datatypes. """ - ret = SortedDict() + ret = ReturnDict(serializer=self) fields = [field for field in self.fields.values() if not field.write_only] for field in fields: @@ -302,11 +348,9 @@ class Serializer(BaseSerializer): def __iter__(self): errors = self.errors if hasattr(self, '_errors') else {} for field in self.fields.values(): - if field.read_only: - continue value = self.data.get(field.field_name) if self.data else None error = errors.get(field.field_name) - yield FieldResult(field, value, error) + yield BoundField(field, value, error) def __repr__(self): return representation.serializer_repr(self, indent=1) @@ -317,7 +361,7 @@ class Serializer(BaseSerializer): class ListSerializer(BaseSerializer): child = None - initial = [] + many = True def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) @@ -326,6 +370,11 @@ class ListSerializer(BaseSerializer): super(ListSerializer, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) + def get_initial(self): + if self._initial_data is not None: + return self.to_representation(self._initial_data) + return ReturnList(serializer=self) + def get_value(self, dictionary): # We override the default field access in order to support # lists in HTML forms. @@ -345,7 +394,10 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ - return [self.child.to_representation(item) for item in data] + return ReturnList( + [self.child.to_representation(item) for item in data], + serializer=self + ) def create(self, attrs_list): return [self.child.create(attrs) for attrs in attrs_list] @@ -354,6 +406,9 @@ class ListSerializer(BaseSerializer): return representation.list_repr(self, indent=1) +# ModelSerializer & HyperlinkedModelSerializer +# -------------------------------------------- + class ModelSerializer(Serializer): _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, -- cgit v1.2.3 From fec7c4b45812d22423e73ec3ab801857a55d7340 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Oct 2014 18:13:15 +0100 Subject: Browsable API tweaks --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f24ed40..21cb7ea2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -254,6 +254,7 @@ class Serializer(BaseSerializer): return ReturnDict([ (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 ], serializer=self) #return self.to_representation(self._initial_data) -- cgit v1.2.3 From e6c5ebdda6d0f169f21498909e2d390c460138a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Oct 2014 13:14:17 +0100 Subject: Fix indentation --- rest_framework/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 21cb7ea2..c3a0815e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -252,10 +252,10 @@ class Serializer(BaseSerializer): def get_initial(self): if self._initial_data is not None: return ReturnDict([ - (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 - ], serializer=self) + (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 + ], serializer=self) #return self.to_representation(self._initial_data) return ReturnDict([ -- cgit v1.2.3 From 3a3e2bf57d5443dc0b058d5beb3111f87c418947 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Oct 2014 13:42:06 +0100 Subject: Serializer.save() takes keyword arguments, not 'extras' argument --- rest_framework/serializers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c3a0815e..ed024f87 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -74,12 +74,12 @@ class BaseSerializer(Field): def create(self, validated_data): raise NotImplementedError('`create()` must be implemented.') - def save(self, extras=None): + def save(self, **kwargs): validated_data = self.validated_data - if extras is not None: + if kwargs: validated_data = dict( list(validated_data.items()) + - list(extras.items()) + list(kwargs.items()) ) if self.instance is not None: @@ -256,7 +256,6 @@ class Serializer(BaseSerializer): for field_name, field in self.fields.items() if field.get_value(self._initial_data) is not empty ], serializer=self) - #return self.to_representation(self._initial_data) return ReturnDict([ (field.field_name, field.get_initial()) -- cgit v1.2.3 From 6b09e5f2bba9167404ec329fa12c7f0215ca51ac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 11:22:10 +0100 Subject: Tests for generic relationships --- rest_framework/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ed024f87..3d868a9e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -520,11 +520,6 @@ class ModelSerializer(Serializer): 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] @@ -561,6 +556,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`.' % -- cgit v1.2.3 From 0cbb57b40fdb073c7ca09c9d1078926260c646db Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 12:17:30 +0100 Subject: Tweak pre/post save hooks. Return instance in .update(). --- rest_framework/serializers.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3d868a9e..e7cd50d6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -83,7 +83,10 @@ class BaseSerializer(Field): ) if self.instance is not None: - self.update(self.instance, 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, ( @@ -444,19 +447,19 @@ class ModelSerializer(Serializer): self.validators.extend(validators) self._kwargs['validators'] = validators - def create(self, attrs): + def create(self, validated_attrs): 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: @@ -465,10 +468,11 @@ 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): + for attr, value in validated_attrs.items(): + setattr(instance, attr, value) + instance.save() + return instance def get_unique_together_validators(self): field_names = set([ -- cgit v1.2.3 From 28f3b314f12cbff33c55602c2c5f5f5cce956171 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 12:36:28 +0100 Subject: .validate() returning validated data. transform_ hooks. --- rest_framework/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e7cd50d6..8513428c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -299,7 +299,8 @@ class Serializer(BaseSerializer): value = self.to_internal_value(data) try: self.run_validators(value) - self.validate(value) + value = self.validate(value) + assert value is not None, '.validate() should return the validated data' except ValidationError as exc: raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: exc.messages @@ -341,7 +342,12 @@ class Serializer(BaseSerializer): fields = [field for field in self.fields.values() if not field.write_only] for field in fields: - ret[field.field_name] = field.get_field_representation(instance) + value = field.get_field_representation(instance) + transform_method = getattr(self, 'transform_' + field.field_name, None) + if transform_method is not None: + value = transform_method(value) + + ret[field.field_name] = value return ret -- cgit v1.2.3 From 14ae52a24e93063f77c6010269bf9cd3316627fe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 16:09:37 +0100 Subject: More gradual deprecation --- rest_framework/serializers.py | 71 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8513428c..9fcbcba7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -25,6 +25,7 @@ from rest_framework.utils.field_mapping import ( from rest_framework.validators import UniqueTogetherValidator import copy import inspect +import warnings # Note: We do the following so that users of the framework can use this style: # @@ -517,12 +518,24 @@ class ModelSerializer(Serializer): 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) # Use the default set of fields 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: + warnings.warn( + "The `Meta.exclude` option is pending deprecation. " + "Use the explicit `Meta.fields` instead.", + PendingDeprecationWarning, + stacklevel=3 + ) + for field_name in exclude: + fields.remove(field_name) for field_name in fields: if field_name in declared_fields: @@ -589,13 +602,69 @@ 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' + ]: + kwargs.pop(attr, None) + kwargs.update(extras) # Create the serializer field. ret[field_name] = field_cls(**kwargs) 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={: {'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(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(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] + -- cgit v1.2.3 From 5d247a65c89594a7ab5ce2333612f23eadc6828d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 Oct 2014 15:11:19 +0100 Subject: First pass on nested serializers in HTML --- rest_framework/serializers.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9fcbcba7..1c006990 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -166,14 +166,25 @@ class BoundField(object): Returned when iterating over a serializer instance, providing an API similar to Django forms and form fields. """ - def __init__(self, field, value, errors): + def __init__(self, field, value, errors, prefix=''): self._field = field self.value = value self.errors = errors + self.name = prefix + self.field_name def __getattr__(self, attr_name): return getattr(self._field, attr_name) + def __iter__(self): + for field in self.fields.values(): + yield self[field.field_name] + + def __getitem__(self, key): + field = self.fields[key] + value = self.value.get(key) if self.value else None + error = self.errors.get(key) if self.errors else None + return BoundField(field, value, error, prefix=self.name + '.') + @property def _proxy_class(self): return self._field.__class__ @@ -355,15 +366,22 @@ class Serializer(BaseSerializer): 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 BoundField(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 + return BoundField(field, value, error) # There's some replication of `ListField` here, @@ -404,8 +422,9 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ + iterable = data.all() if (hasattr(data, 'all')) else data return ReturnList( - [self.child.to_representation(item) for item in data], + [self.child.to_representation(item) for item in iterable], serializer=self ) -- cgit v1.2.3 From f83ed19d22250eb646c9d77ccb1614a78d134e75 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 Oct 2014 16:29:34 +0100 Subject: Checks and repr on BoundField --- rest_framework/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1c006990..3bd7b17b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -180,6 +180,7 @@ class BoundField(object): yield self[field.field_name] def __getitem__(self, key): + assert hasattr(self, 'fields'), '"%s" is not a nested field. Cannot perform indexing.' % self.name field = self.fields[key] value = self.value.get(key) if self.value else None error = self.errors.get(key) if self.errors else None @@ -189,6 +190,9 @@ class BoundField(object): def _proxy_class(self): return self._field.__class__ + def __repr__(self): + return '<%s value=%s errors=%s>' % (self.__class__.__name__, self.value, self.errors) + class BindingDict(object): """ -- cgit v1.2.3 From d9a199ca0ddf92f999aa37b396596d0e3e0a26d9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 Oct 2014 14:16:09 +0100 Subject: exceptions.ValidationFailed, not Django's ValidationError --- rest_framework/serializers.py | 60 +++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3bd7b17b..2f683562 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,10 +10,11 @@ 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.utils import six from django.utils.datastructures import SortedDict +from rest_framework.exceptions import ValidationFailed 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 @@ -100,14 +101,14 @@ class BaseSerializer(Field): if not hasattr(self, '_validated_data'): try: self._validated_data = self.run_validation(self._initial_data) - except ValidationError as exc: + except ValidationFailed as exc: self._validated_data = {} - self._errors = exc.message_dict + self._errors = exc.detail else: self._errors = {} if self._errors and raise_exception: - raise ValidationError(self._errors) + raise ValidationFailed(self._errors) return not bool(self._errors) @@ -175,24 +176,34 @@ class BoundField(object): def __getattr__(self, attr_name): return getattr(self._field, attr_name) + @property + def _proxy_class(self): + return self._field.__class__ + + def __repr__(self): + return '<%s value=%s errors=%s>' % ( + self.__class__.__name__, self.value, self.errors + ) + + +class NestedBoundField(BoundField): + """ + This BoundField additionally implements __iter__ and __getitem__ + in order to support nested bound fields. This class is the type of + BoundField that is used for serializer fields. + """ def __iter__(self): for field in self.fields.values(): yield self[field.field_name] def __getitem__(self, key): - assert hasattr(self, 'fields'), '"%s" is not a nested field. Cannot perform indexing.' % self.name field = self.fields[key] value = self.value.get(key) if self.value else None error = self.errors.get(key) if self.errors else None + if isinstance(field, Serializer): + return NestedBoundField(field, value, error, prefix=self.name + '.') return BoundField(field, value, error, prefix=self.name + '.') - @property - def _proxy_class(self): - return self._field.__class__ - - def __repr__(self): - return '<%s value=%s errors=%s>' % (self.__class__.__name__, self.value, self.errors) - class BindingDict(object): """ @@ -308,7 +319,7 @@ class Serializer(BaseSerializer): return None if not isinstance(data, dict): - raise ValidationError({ + raise ValidationFailed({ api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data'] }) @@ -317,9 +328,9 @@ class Serializer(BaseSerializer): self.run_validators(value) value = self.validate(value) assert value is not None, '.validate() should return the validated data' - except ValidationError as exc: - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: exc.messages + except ValidationFailed as exc: + raise ValidationFailed({ + api_settings.NON_FIELD_ERRORS_KEY: exc.detail }) return value @@ -338,15 +349,15 @@ class Serializer(BaseSerializer): validated_value = field.run_validation(primitive_value) if validate_method is not None: validated_value = validate_method(validated_value) - except ValidationError as exc: - errors[field.field_name] = exc.messages + except ValidationFailed as exc: + errors[field.field_name] = exc.detail except SkipField: pass else: set_value(ret, field.source_attrs, validated_value) if errors: - raise ValidationError(errors) + raise ValidationFailed(errors) return ret @@ -385,6 +396,8 @@ class Serializer(BaseSerializer): 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) @@ -538,9 +551,12 @@ class ModelSerializer(Serializer): ret = SortedDict() 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. @@ -551,12 +567,6 @@ class ModelSerializer(Serializer): fields = self._get_default_field_names(declared_fields, info) exclude = getattr(self.Meta, 'exclude', None) if exclude is not None: - warnings.warn( - "The `Meta.exclude` option is pending deprecation. " - "Use the explicit `Meta.fields` instead.", - PendingDeprecationWarning, - stacklevel=3 - ) for field_name in exclude: fields.remove(field_name) -- cgit v1.2.3 From d8a8987ab1eb6abbaee1a0de8cfea38eafe21293 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 Oct 2014 14:32:02 +0100 Subject: Tweaks --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2f683562..0f6cf2bc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -555,7 +555,7 @@ class ModelSerializer(Serializer): depth = getattr(self.Meta, 'depth', 0) extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) - assert not fields and exclude, "Cannot set both 'fields' and 'exclude'." + assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'." extra_kwargs = self._include_additional_options(extra_kwargs) -- cgit v1.2.3 From b5a4216aff06bfb36238d0f587d8645db0ee4a69 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 Oct 2014 15:08:43 +0100 Subject: Flake8 --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f6cf2bc..f3f5c837 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -555,7 +555,7 @@ class ModelSerializer(Serializer): depth = getattr(self.Meta, 'depth', 0) extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) - assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'." + assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'." extra_kwargs = self._include_additional_options(extra_kwargs) -- cgit v1.2.3 From 826b5a889704452c53c05a44905f9fa62889ff34 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 Oct 2014 15:34:00 +0100 Subject: Relations in 'read_only_fields' should not include a queryset kwarg --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f3f5c837..bc9c15eb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -640,7 +640,7 @@ class ModelSerializer(Serializer): for attr in [ 'required', 'default', 'allow_blank', 'allow_null', 'min_length', 'max_length', 'min_value', 'max_value', - 'validators' + 'validators', 'queryset' ]: kwargs.pop(attr, None) kwargs.update(extras) -- cgit v1.2.3 From e272a36c9b444c1da3a3d8bc809070deb26d9c64 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 15 Oct 2014 09:24:49 +0100 Subject: Fix 'lookup_field' on ModelSerializer. Closes #1944. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bc9c15eb..c844605f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -680,7 +680,7 @@ class ModelSerializer(Serializer): PendingDeprecationWarning, stacklevel=3 ) - kwargs = extra_kwargs.get(field_name, {}) + kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) kwargs['view_name'] = view_name extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs @@ -692,7 +692,7 @@ class ModelSerializer(Serializer): PendingDeprecationWarning, stacklevel=3 ) - kwargs = extra_kwargs.get(field_name, {}) + kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) kwargs['lookup_field'] = lookup_field extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs -- cgit v1.2.3 From b4f3379c7002f0c80a26605fdd9c69d7cef2f16f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 15 Oct 2014 15:13:28 +0100 Subject: Support fields that reference a simple callable --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c844605f..534be6f9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -115,7 +115,7 @@ 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) else: self._data = self.get_initial() @@ -339,7 +339,7 @@ class Serializer(BaseSerializer): Dict of native values <- Dict of primitive datatypes. """ ret = {} - errors = {} + errors = ReturnDict(serializer=self) fields = [field for field in self.fields.values() if not field.read_only] for field in fields: -- cgit v1.2.3 From 4248a6c499cf31d2e38199f05a42e7a131dc014e Mon Sep 17 00:00:00 2001 From: Marty Alchin Date: Wed, 15 Oct 2014 17:54:58 -0700 Subject: Add a keys method to BindingDict --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 534be6f9..2066fcf7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -230,6 +230,9 @@ class BindingDict(object): def items(self): return self.fields.items() + def keys(self): + return self.fields.keys() + def values(self): return self.fields.values() -- cgit v1.2.3 From 7b666e982c461e237567435851dcc93bc76581e5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 16 Oct 2014 20:45:18 +0100 Subject: Stricter checking for failure cases. --- rest_framework/serializers.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2066fcf7..f72ecb0b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -77,6 +77,13 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): + assert not hasattr(self, 'restore_object'), ( + 'Serializer %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__.__name__ + ) + validated_data = self.validated_data if kwargs: validated_data = dict( @@ -494,6 +501,16 @@ class ModelSerializer(Serializer): self._kwargs['validators'] = validators def create(self, validated_attrs): + 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, or set `read_only=True` on nested serializer fields.' % + self.__class__.__name__ + ) + ModelClass = self.Meta.model # Remove many-to-many relationships from validated_attrs. @@ -515,6 +532,16 @@ class ModelSerializer(Serializer): return instance 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, or set `read_only=True` on nested serializer fields.' % + self.__class__.__name__ + ) + for attr, value in validated_attrs.items(): setattr(instance, attr, value) instance.save() -- cgit v1.2.3 From 32fd82ba0d6082418e5ca5633f9e9709bd44e86b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 16 Oct 2014 20:45:36 +0100 Subject: get_attribute method on fields --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f72ecb0b..30e6bfeb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -379,7 +379,11 @@ class Serializer(BaseSerializer): fields = [field for field in self.fields.values() if not field.write_only] for field in fields: - value = field.get_field_representation(instance) + attribute = field.get_attribute(instance) + if attribute is None: + value = None + else: + value = field.to_representation(attribute) transform_method = getattr(self, 'transform_' + field.field_name, None) if transform_method is not None: value = transform_method(value) -- cgit v1.2.3 From 05cbec9dd7f9f0b6a9b59b29ac6c9272b6ae50d8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 17 Oct 2014 13:23:14 +0100 Subject: Use serializers.ValidationError --- rest_framework/serializers.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 30e6bfeb..d29dc684 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils import six from django.utils.datastructures import SortedDict -from rest_framework.exceptions import ValidationFailed +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 @@ -77,13 +77,6 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): - assert not hasattr(self, 'restore_object'), ( - 'Serializer %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__.__name__ - ) - validated_data = self.validated_data if kwargs: validated_data = dict( @@ -105,17 +98,24 @@ class BaseSerializer(Field): return self.instance def is_valid(self, raise_exception=False): + assert not hasattr(self, 'restore_object'), ( + 'Serializer %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__.__name__ + ) + if not hasattr(self, '_validated_data'): try: self._validated_data = self.run_validation(self._initial_data) - except ValidationFailed as exc: + except ValidationError as exc: self._validated_data = {} self._errors = exc.detail else: self._errors = {} if self._errors and raise_exception: - raise ValidationFailed(self._errors) + raise ValidationError(self._errors) return not bool(self._errors) @@ -124,6 +124,8 @@ class BaseSerializer(Field): if not hasattr(self, '_data'): if self.instance is not None and not getattr(self, '_errors', None): self._data = self.to_representation(self.instance) + 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 @@ -329,7 +331,7 @@ class Serializer(BaseSerializer): return None if not isinstance(data, dict): - raise ValidationFailed({ + raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data'] }) @@ -338,8 +340,8 @@ class Serializer(BaseSerializer): self.run_validators(value) value = self.validate(value) assert value is not None, '.validate() should return the validated data' - except ValidationFailed as exc: - raise ValidationFailed({ + except ValidationError as exc: + raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: exc.detail }) return value @@ -359,7 +361,7 @@ class Serializer(BaseSerializer): validated_value = field.run_validation(primitive_value) if validate_method is not None: validated_value = validate_method(validated_value) - except ValidationFailed as exc: + except ValidationError as exc: errors[field.field_name] = exc.detail except SkipField: pass @@ -367,7 +369,7 @@ class Serializer(BaseSerializer): set_value(ret, field.source_attrs, validated_value) if errors: - raise ValidationFailed(errors) + raise ValidationError(errors) return ret -- cgit v1.2.3 From c5d1be8eac6cdb5cce000ec7c55e1847bfcf2359 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Oct 2014 10:32:32 +0100 Subject: .validate() can raise field errors or non-field errors --- rest_framework/serializers.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d29dc684..59f38a73 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -99,10 +99,10 @@ class BaseSerializer(Field): def is_valid(self, raise_exception=False): assert not hasattr(self, 'restore_object'), ( - 'Serializer %s has old-style version 2 `.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__.__name__ + (self.__class__.__module__, self.__class__.__name__) ) if not hasattr(self, '_validated_data'): @@ -341,9 +341,22 @@ class Serializer(BaseSerializer): value = self.validate(value) assert value is not None, '.validate() should return the validated data' except ValidationError as exc: - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: exc.detail - }) + 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): @@ -507,14 +520,17 @@ class ModelSerializer(Serializer): self._kwargs['validators'] = validators 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, or set `read_only=True` on nested serializer fields.' % - self.__class__.__name__ + '`%s.%s`, or set `read_only=True` on nested serializer fields.' % + (self.__class__.__module__, self.__class__.__name__) ) ModelClass = self.Meta.model @@ -544,8 +560,8 @@ class ModelSerializer(Serializer): ), ( 'The `.update()` method does not suport nested writable fields ' 'by default. Write an explicit `.update()` method for serializer ' - '%s, or set `read_only=True` on nested serializer fields.' % - self.__class__.__name__ + '`%s.%s`, or set `read_only=True` on nested serializer fields.' % + (self.__class__.__module__, self.__class__.__name__) ) for attr, value in validated_attrs.items(): -- cgit v1.2.3 From ae53fdff9c6bb3e81a1ec005134462f0d629688f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Oct 2014 13:30:28 +0100 Subject: First pass at unique_for_date, unique_for_month, unique_for_year --- rest_framework/serializers.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 59f38a73..5770fcf6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -23,7 +23,9 @@ from rest_framework.utils.field_mapping import ( get_relation_kwargs, get_nested_relation_kwargs, ClassLookupDict ) -from rest_framework.validators import UniqueTogetherValidator +from rest_framework.validators import ( + UniqueForDateValidator, UniqueTogetherValidator +) import copy import inspect import warnings @@ -578,15 +580,9 @@ class ModelSerializer(Serializer): validators = [] model_class = self.Meta.model - for unique_together in model_class._meta.unique_together: - if field_names.issuperset(set(unique_together)): - validator = UniqueTogetherValidator( - queryset=model_class._default_manager, - fields=unique_together - ) - validators.append(validator) - - for parent_class in model_class._meta.parents.keys(): + # 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( @@ -595,6 +591,16 @@ class ModelSerializer(Serializer): ) validators.append(validator) + 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) + return validators def _get_base_fields(self): -- cgit v1.2.3 From f9c0e6ee1b045f69920cf9c3aceda4e44f110bfb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Oct 2014 16:29:09 +0100 Subject: unique_for_month, unique_for_year --- rest_framework/serializers.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5770fcf6..b45f343a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -24,7 +24,8 @@ from rest_framework.utils.field_mapping import ( ClassLookupDict ) from rest_framework.validators import ( - UniqueForDateValidator, UniqueTogetherValidator + UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, + UniqueTogetherValidator ) import copy import inspect @@ -601,6 +602,22 @@ class ModelSerializer(Serializer): ) 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_base_fields(self): -- cgit v1.2.3 From 9ebaabd6eb31e18cf0bb1c70893f719f18ecb0f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 28 Oct 2014 16:21:49 +0000 Subject: unique_for_date/unique_for_month/unique_for_year --- rest_framework/serializers.py | 82 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b45f343a..6aab020e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,6 +12,7 @@ response content is handled by parsers and renderers. """ 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 rest_framework.exceptions import ValidationError @@ -368,7 +369,10 @@ class Serializer(BaseSerializer): """ ret = {} errors = ReturnDict(serializer=self) - fields = [field for field in self.fields.values() if not field.read_only] + 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) @@ -517,7 +521,7 @@ class ModelSerializer(Serializer): def __init__(self, *args, **kwargs): super(ModelSerializer, self).__init__(*args, **kwargs) if 'validators' not in kwargs: - validators = self.get_unique_together_validators() + validators = self.get_default_validators() if validators: self.validators.extend(validators) self._kwargs['validators'] = validators @@ -572,7 +576,7 @@ class ModelSerializer(Serializer): instance.save() return instance - def get_unique_together_validators(self): + def get_default_validators(self): field_names = set([ field.source for field in self.fields.values() if (field.source != '*') and ('.' not in field.source) @@ -592,6 +596,7 @@ class ModelSerializer(Serializer): ) 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: @@ -637,7 +642,7 @@ class ModelSerializer(Serializer): # 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) @@ -645,6 +650,72 @@ class ModelSerializer(Serializer): 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. + unique_fields = {} + for model_field_name, field_name in model_field_mapping.items(): + try: + model_field = model._meta.get_field(model_field_name) + except FieldDoesNotExist: + continue + + # Deal with each of the `unique_for_*` cases. + for date_field_name in ( + model_field.unique_for_date, + model_field.unique_for_month, + model_field.unique_for_year + ): + if date_field_name is None: + continue + + # Get the model field that is refered too. + date_field = model._meta.get_field(date_field_name) + + if date_field.auto_now_add: + default = CreateOnlyDefault(timezone.now) + elif date_field.auto_now: + default = timezone.now + elif date_field.has_default(): + default = model_field.default + else: + default = empty + + if date_field_name in model_field_mapping: + # The corresponding date field is present in the serializer + if date_field_name not in extra_kwargs: + extra_kwargs[date_field_name] = {} + if default is empty: + if 'required' not in extra_kwargs[date_field_name]: + extra_kwargs[date_field_name]['required'] = True + else: + if 'default' not in extra_kwargs[date_field_name]: + extra_kwargs[date_field_name]['default'] = default + else: + # The corresponding date field is not present in the, + # serializer. We have a default to use for the date, so + # add in a hidden field that populates it. + unique_fields[date_field_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. @@ -723,6 +794,9 @@ class ModelSerializer(Serializer): # Create the serializer field. ret[field_name] = field_cls(**kwargs) + for field_name, field in unique_fields.items(): + ret[field_name] = field + return ret def _include_additional_options(self, extra_kwargs): -- cgit v1.2.3 From 27622058872c00e357deb7d7e86619a793ef4b41 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 31 Oct 2014 13:47:36 +0000 Subject: Validator documentation and tweaks --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6aab020e..c9f70f2d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -292,6 +292,8 @@ class Serializer(BaseSerializer): for key, value in self._get_base_fields().items(): self.fields[key] = value + self.validators = getattr(getattr(self, 'Meta', None), 'validators', []) + def _get_base_fields(self): return copy.deepcopy(self._declared_fields) -- cgit v1.2.3 From 207208fedff2457e921ef7d825ea7c3933b5dd6e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 31 Oct 2014 16:38:39 +0000 Subject: Lazy loading of fields and validators. Closes #1963. --- rest_framework/serializers.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c9f70f2d..82e932dd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -282,21 +282,23 @@ class SerializerMetaclass(type): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): - def __init__(self, *args, **kwargs): - super(Serializer, self).__init__(*args, **kwargs) - + @property + def fields(self): + 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): # 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 = BindingDict(self) - for key, value in self._get_base_fields().items(): - self.fields[key] = value - - self.validators = getattr(getattr(self, 'Meta', None), 'validators', []) - - def _get_base_fields(self): return copy.deepcopy(self._declared_fields) + def get_validators(self): + return getattr(getattr(self, 'Meta', None), 'validators', []) + def get_initial(self): if self._initial_data is not None: return ReturnDict([ @@ -520,14 +522,6 @@ class ModelSerializer(Serializer): }) _related_class = PrimaryKeyRelatedField - def __init__(self, *args, **kwargs): - super(ModelSerializer, self).__init__(*args, **kwargs) - if 'validators' not in kwargs: - validators = self.get_default_validators() - if validators: - self.validators.extend(validators) - self._kwargs['validators'] = validators - 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 @@ -578,13 +572,13 @@ class ModelSerializer(Serializer): instance.save() return instance - def get_default_validators(self): + def get_validators(self): field_names = set([ field.source for field in self.fields.values() if (field.source != '*') and ('.' not in field.source) ]) - validators = [] + validators = getattr(getattr(self, 'Meta', None), 'validators', []) model_class = self.Meta.model # Note that we make sure to check `unique_together` both on the @@ -627,7 +621,7 @@ class ModelSerializer(Serializer): return validators - def _get_base_fields(self): + def get_fields(self): declared_fields = copy.deepcopy(self._declared_fields) ret = SortedDict() -- cgit v1.2.3 From 003c42b0f51f9bfa93964be69fb8cb68b7394280 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 3 Nov 2014 14:01:02 +0000 Subject: Use invalid_data key for error message. Closes #2002. --- rest_framework/serializers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 82e932dd..f00b685f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,6 +15,7 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six from django.utils.datastructures import SortedDict +from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import ValidationError from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings @@ -282,6 +283,10 @@ class SerializerMetaclass(type): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): + default_error_messages = { + 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') + } + @property def fields(self): if not hasattr(self, '_fields'): @@ -339,8 +344,11 @@ class Serializer(BaseSerializer): 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] }) value = self.to_internal_value(data) -- cgit v1.2.3 From d048d32876e4a2f4acb6848b809097b9b9d48e50 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Nov 2014 13:40:21 +0000 Subject: Minor cleanup --- rest_framework/serializers.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f00b685f..9e5d0cab 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -306,11 +306,7 @@ class Serializer(BaseSerializer): def get_initial(self): if self._initial_data is not None: - return ReturnDict([ - (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 - ], serializer=self) + return self.to_representation(self._initial_data) return ReturnDict([ (field.field_name, field.get_initial()) -- cgit v1.2.3 From 49fae230005bba4607f425d90de77363d6b8659e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Nov 2014 15:23:13 +0000 Subject: Pass through kwargs to both Serializer and ListSerializer --- rest_framework/serializers.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9e5d0cab..dfac75fc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -43,6 +43,12 @@ import warnings from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA +LIST_SERIALIZER_KWARGS = ( + 'read_only', 'write_only', 'required', 'default', 'initial', 'source', + 'label', 'help_text', 'style', 'error_messages', + 'instance', 'data', 'partial', 'context' +) + # BaseSerializer # -------------- @@ -52,7 +58,6 @@ 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): self.instance = instance self._initial_data = data @@ -65,8 +70,11 @@ class BaseSerializer(Field): # 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) + list_kwargs = {'child': cls(*args, **kwargs)} + for key in kwargs.keys(): + if key in LIST_SERIALIZER_KWARGS: + list_kwargs[key] = kwargs[key] + return ListSerializer(*args, **list_kwargs) return super(BaseSerializer, cls).__new__(cls, *args, **kwargs) def to_internal_value(self, data): -- cgit v1.2.3 From ed541864e637681e1aca3a808be1f26202b4c271 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 10:34:59 +0000 Subject: Support for bulk create. Closes #1965. --- rest_framework/serializers.py | 65 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 11 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index dfac75fc..cbac3992 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -90,12 +90,10 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): - validated_data = self.validated_data - if kwargs: - validated_data = dict( - list(validated_data.items()) + - list(kwargs.items()) - ) + validated_data = dict( + list(self.validated_data.items()) + + list(kwargs.items()) + ) if self.instance is not None: self.instance = self.update(self.instance, validated_data) @@ -210,9 +208,9 @@ class BoundField(object): class NestedBoundField(BoundField): """ - This BoundField additionally implements __iter__ and __getitem__ + This `BoundField` additionally implements __iter__ and __getitem__ in order to support nested bound fields. This class is the type of - BoundField that is used for serializer fields. + `BoundField` that is used for serializer fields. """ def __iter__(self): for field in self.fields.values(): @@ -460,6 +458,10 @@ class ListSerializer(BaseSerializer): child = None 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.' @@ -485,7 +487,31 @@ 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 = ReturnList(serializer=self) + + for item in data: + try: + validated = self.child.run_validation(item) + except ValidationError, exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret def to_representation(self, data): """ @@ -497,8 +523,25 @@ class ListSerializer(BaseSerializer): serializer=self ) - def create(self, attrs_list): - return [self.child.create(attrs) for attrs in attrs_list] + def save(self, **kwargs): + assert self.instance is None, ( + "Serializers do not support multiple update by default, because " + "it would be unclear how to deal with insertions, updates and " + "deletions. If you need to support multiple update, use a " + "`ListSerializer` class and override `.save()` so you can specify " + "the behavior exactly." + ) + + validated_data = [ + dict(list(attrs.items()) + list(kwargs.items())) + for attrs in self.validated_data + ] + + self.instance = [ + self.child.create(attrs) for attrs in validated_data + ] + + return self.instance def __repr__(self): return representation.list_repr(self, indent=1) -- cgit v1.2.3 From 4482be46ae8c9ed9864123903e114fcc833441ae Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 11:10:36 +0000 Subject: More precise assertion error for bulk update --- rest_framework/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cbac3992..68f9e362 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -525,11 +525,11 @@ class ListSerializer(BaseSerializer): def save(self, **kwargs): assert self.instance is None, ( - "Serializers do not support multiple update by default, because " - "it would be unclear how to deal with insertions, updates and " - "deletions. If you need to support multiple update, use a " - "`ListSerializer` class and override `.save()` so you can specify " - "the behavior exactly." + "Serializers 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 `.save()` so you can " + "specify the behavior exactly." ) validated_data = [ -- cgit v1.2.3 From a919068c5df3677469064fa2e300d27ae4d6e39f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 11:35:34 +0000 Subject: Fix exception style for py3 --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 68f9e362..7cd206c9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -502,7 +502,7 @@ class ListSerializer(BaseSerializer): for item in data: try: validated = self.child.run_validation(item) - except ValidationError, exc: + except ValidationError as exc: errors.append(exc.detail) else: ret.append(validated) -- cgit v1.2.3 From 4e001dbb7ac0bc13d6d5fbb4524e905184610aa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 12:00:30 +0000 Subject: Drop usage of SortedDict. Closes #2027. --- rest_framework/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7cd206c9..0efa9524 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,8 +14,8 @@ 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 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 @@ -159,7 +159,7 @@ class BaseSerializer(Field): # Serializer & ListSerializer classes # ----------------------------------- -class ReturnDict(SortedDict): +class ReturnDict(OrderedDict): """ Return object from `serialier.data` for the `Serializer` class. Includes a backlink to the serializer instance for renderers @@ -235,7 +235,7 @@ class BindingDict(object): """ def __init__(self, serializer): self.serializer = serializer - self.fields = SortedDict() + self.fields = OrderedDict() def __setitem__(self, key, field): self.fields[key] = field @@ -280,7 +280,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) @@ -679,7 +679,7 @@ class ModelSerializer(Serializer): 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) -- cgit v1.2.3 From 9b19b5a59485c9dad4a18538a8a86f1ae4ea2a55 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Nov 2014 10:13:46 +0000 Subject: Serializer cleanup --- rest_framework/serializers.py | 128 ++++++++++-------------------------------- 1 file changed, 30 insertions(+), 98 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0efa9524..68ea27ab 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -25,6 +25,9 @@ from rest_framework.utils.field_mapping import ( get_relation_kwargs, get_nested_relation_kwargs, ClassLookupDict ) +from rest_framework.utils.serializer_helpers import ( + ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict +) from rest_framework.validators import ( UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, UniqueTogetherValidator @@ -159,104 +162,6 @@ class BaseSerializer(Field): # Serializer & ListSerializer classes # ----------------------------------- -class ReturnDict(OrderedDict): - """ - Return object from `serialier.data` for the `Serializer` class. - Includes a backlink to the serializer instance for renderers - to use if they need richer field information. - """ - def __init__(self, *args, **kwargs): - self.serializer = kwargs.pop('serializer') - super(ReturnDict, self).__init__(*args, **kwargs) - - -class ReturnList(list): - """ - Return object from `serialier.data` for the `SerializerList` class. - Includes a backlink to the serializer instance for renderers - to use if they need richer field information. - """ - def __init__(self, *args, **kwargs): - self.serializer = kwargs.pop('serializer') - super(ReturnList, self).__init__(*args, **kwargs) - - -class BoundField(object): - """ - A field object that also includes `.value` and `.error` properties. - Returned when iterating over a serializer instance, - providing an API similar to Django forms and form fields. - """ - def __init__(self, field, value, errors, prefix=''): - self._field = field - self.value = value - self.errors = errors - self.name = prefix + self.field_name - - def __getattr__(self, attr_name): - return getattr(self._field, attr_name) - - @property - def _proxy_class(self): - return self._field.__class__ - - def __repr__(self): - return '<%s value=%s errors=%s>' % ( - self.__class__.__name__, self.value, self.errors - ) - - -class NestedBoundField(BoundField): - """ - This `BoundField` additionally implements __iter__ and __getitem__ - in order to support nested bound fields. This class is the type of - `BoundField` that is used for serializer fields. - """ - def __iter__(self): - for field in self.fields.values(): - yield self[field.field_name] - - def __getitem__(self, key): - field = self.fields[key] - value = self.value.get(key) if self.value else None - error = self.errors.get(key) if self.errors else None - if isinstance(field, Serializer): - return NestedBoundField(field, value, error, prefix=self.name + '.') - return BoundField(field, value, error, prefix=self.name + '.') - - -class BindingDict(object): - """ - This dict-like object is used to store fields on a serializer. - - This ensures that whenever fields are added to the serializer we call - `field.bind()` so that the `field_name` and `parent` attributes - can be set correctly. - """ - def __init__(self, serializer): - self.serializer = serializer - self.fields = OrderedDict() - - def __setitem__(self, key, field): - self.fields[key] = field - field.bind(field_name=key, parent=self.serializer) - - def __getitem__(self, key): - return self.fields[key] - - def __delitem__(self, key): - del self.fields[key] - - def items(self): - return self.fields.items() - - def keys(self): - return self.fields.keys() - - def values(self): - return self.fields.values() - - class SerializerMetaclass(type): """ This metaclass sets a dictionary named `base_fields` on the class. @@ -295,6 +200,12 @@ class Serializer(BaseSerializer): @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(): @@ -302,12 +213,19 @@ class Serializer(BaseSerializer): 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. return copy.deepcopy(self._declared_fields) 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): @@ -551,6 +469,13 @@ class ListSerializer(BaseSerializer): # -------------------------------------------- class ModelSerializer(Serializer): + """ + 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, @@ -915,6 +840,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): -- cgit v1.2.3 From e399140031a0738a054f5f07e42ef7208f9e45f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Nov 2014 10:51:08 +0000 Subject: Minor tweaks --- rest_framework/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 68ea27ab..b00f9b7a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -393,6 +393,9 @@ class ListSerializer(BaseSerializer): return ReturnList(serializer=self) 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 html.is_html_input(dictionary): @@ -442,6 +445,9 @@ class ListSerializer(BaseSerializer): ) def save(self, **kwargs): + """ + Save and return a list of object instances. + """ assert self.instance is None, ( "Serializers do not support multiple update by default, only " "multiple create. For updates it is unclear how to deal with " -- cgit v1.2.3 From 55c44fc92192f63867c6c7ebcb7f75bf0569c900 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Nov 2014 14:13:50 +0000 Subject: Fix serializer initial data --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b00f9b7a..833a4e48 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -230,7 +230,11 @@ class Serializer(BaseSerializer): def get_initial(self): if self._initial_data is not None: - return self.to_representation(self._initial_data) + return ReturnDict([ + (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 + ], serializer=self) return ReturnDict([ (field.field_name, field.get_initial()) -- cgit v1.2.3 From 3e878a3207ba79785d41b4d6af5cd956156d45a3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Nov 2014 15:38:27 +0000 Subject: Fix initial data on serializers to reflect writable fields --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 833a4e48..d83367f4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -234,12 +234,13 @@ class Serializer(BaseSerializer): (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 ], serializer=self) return ReturnDict([ (field.field_name, field.get_initial()) for field in self.fields.values() - if not field.write_only + if not field.read_only ], serializer=self) def get_value(self, dictionary): -- cgit v1.2.3 From 78a741be27f5007d6fa2f73c6cedf04bfe638f9c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Nov 2014 20:24:48 +0000 Subject: Split out .create and .update on ListSerializer --- rest_framework/serializers.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d83367f4..a4aeeeb7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -449,26 +449,39 @@ class ListSerializer(BaseSerializer): serializer=self ) + 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, validated_data): + return [ + self.child.create(attrs) for attrs in validated_data + ] + def save(self, **kwargs): """ Save and return a list of object instances. """ - assert self.instance is None, ( - "Serializers 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 `.save()` so you can " - "specify the behavior exactly." - ) - validated_data = [ dict(list(attrs.items()) + list(kwargs.items())) for attrs in self.validated_data ] - self.instance = [ - self.child.create(attrs) for attrs in validated_data - ] + if self.instance is not None: + 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 -- cgit v1.2.3 From 992330055eeb5d787ddd7d62dfc9121a2256fd9b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Nov 2014 21:11:13 +0000 Subject: Refactor many --- rest_framework/serializers.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a4aeeeb7..70bba8ab 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -46,6 +46,9 @@ import warnings from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA + +# 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', @@ -73,13 +76,25 @@ class BaseSerializer(Field): # We override this method in order to automagically create # `ListSerializer` classes instead when `many=True` is set. if kwargs.pop('many', False): - list_kwargs = {'child': cls(*args, **kwargs)} - for key in kwargs.keys(): - if key in LIST_SERIALIZER_KWARGS: - list_kwargs[key] = kwargs[key] - return ListSerializer(*args, **list_kwargs) + 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. + """ + 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 + ])) + return ListSerializer(*args, **list_kwargs) + def to_internal_value(self, data): raise NotImplementedError('`to_internal_value()` must be implemented.') -- cgit v1.2.3 From 7394dcec9e625e7eee5c041276f81745359ed7c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Nov 2014 23:05:44 +0000 Subject: ReturnList and ReturnDict wrapped at nicer point --- rest_framework/serializers.py | 53 ++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 13 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 70bba8ab..e7e93f38 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -245,18 +245,18 @@ class Serializer(BaseSerializer): def get_initial(self): if self._initial_data is not None: - return ReturnDict([ + 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 - ], serializer=self) + ]) - return ReturnDict([ + return OrderedDict([ (field.field_name, field.get_initial()) for field in self.fields.values() if not field.read_only - ], serializer=self) + ]) def get_value(self, dictionary): # We override the default field access in order to support @@ -319,8 +319,8 @@ class Serializer(BaseSerializer): """ Dict of native values <- Dict of primitive datatypes. """ - ret = {} - errors = ReturnDict(serializer=self) + ret = OrderedDict() + errors = OrderedDict() fields = [ field for field in self.fields.values() if (not field.read_only) or (field.default is not empty) @@ -349,7 +349,7 @@ class Serializer(BaseSerializer): """ Object instance -> Dict of primitive datatypes. """ - ret = ReturnDict(serializer=self) + ret = OrderedDict() fields = [field for field in self.fields.values() if not field.write_only] for field in fields: @@ -388,6 +388,19 @@ class Serializer(BaseSerializer): 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. @@ -410,7 +423,7 @@ class ListSerializer(BaseSerializer): def get_initial(self): if self._initial_data is not None: return self.to_representation(self._initial_data) - return ReturnList(serializer=self) + return [] def get_value(self, dictionary): """ @@ -438,7 +451,7 @@ class ListSerializer(BaseSerializer): }) ret = [] - errors = ReturnList(serializer=self) + errors = [] for item in data: try: @@ -459,10 +472,9 @@ class ListSerializer(BaseSerializer): List of object instances -> List of dicts of primitive datatypes. """ iterable = data.all() if (hasattr(data, 'all')) else data - return ReturnList( - [self.child.to_representation(item) for item in iterable], - serializer=self - ) + return [ + self.child.to_representation(item) for item in iterable + ] def update(self, instance, validated_data): raise NotImplementedError( @@ -503,6 +515,21 @@ class ListSerializer(BaseSerializer): 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 # -------------------------------------------- -- cgit v1.2.3 From ad060aa360fa2ed33bd83cbb419d7b996a428726 Mon Sep 17 00:00:00 2001 From: Gregor Müllegger Date: Sat, 15 Nov 2014 15:23:58 +0100 Subject: More helpful error message when default `.create` fails. Closes #2013. --- rest_framework/serializers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e7e93f38..8dafea4d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -34,6 +34,7 @@ from rest_framework.validators import ( ) import copy import inspect +import sys import warnings # Note: We do the following so that users of the framework can use this style: @@ -593,7 +594,18 @@ class ModelSerializer(Serializer): 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(**validated_attrs) + try: + instance = ModelClass.objects.create(**validated_attrs) + except TypeError as exc: + msg = ( + 'The mentioned argument might be a field on the serializer ' + 'that is not part of the model. You need to override the ' + 'create() method in your ModelSerializer subclass to support ' + 'this.') + six.reraise( + type(exc), + type(exc)(str(exc) + '. ' + msg), + sys.exc_info()[2]) # Save many-to-many relationships after the instance is created. if many_to_many: -- cgit v1.2.3 From 5e74f02d61e05d38bf4e22c6227144def2a96128 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Nov 2014 14:49:00 +0000 Subject: Note removal of 'save_object' and fail loudly if it exists --- rest_framework/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e7e93f38..84282cdb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -108,6 +108,13 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') 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()) -- cgit v1.2.3 From 8586290df80ac8448d71cdb3326bc822c399cad1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 13:55:10 +0000 Subject: Apply defaults and requiredness to unique_together fields. Closes #2092. --- rest_framework/serializers.py | 83 +++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 34 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 84282cdb..2e34dbe7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -720,49 +720,60 @@ class ModelSerializer(Serializer): # 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_fields = {} + hidden_fields = {} + for model_field_name, field_name in model_field_mapping.items(): try: model_field = model._meta.get_field(model_field_name) except FieldDoesNotExist: continue - # Deal with each of the `unique_for_*` cases. - for date_field_name in ( + # 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 - ): - if date_field_name is None: - continue - - # Get the model field that is refered too. - date_field = model._meta.get_field(date_field_name) - - if date_field.auto_now_add: - default = CreateOnlyDefault(timezone.now) - elif date_field.auto_now: - default = timezone.now - elif date_field.has_default(): - default = model_field.default - else: - default = empty - - if date_field_name in model_field_mapping: - # The corresponding date field is present in the serializer - if date_field_name not in extra_kwargs: - extra_kwargs[date_field_name] = {} - if default is empty: - if 'required' not in extra_kwargs[date_field_name]: - extra_kwargs[date_field_name]['required'] = True - else: - if 'default' not in extra_kwargs[date_field_name]: - extra_kwargs[date_field_name]['default'] = default + ]) + 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 = model_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: - # The corresponding date field is not present in the, - # serializer. We have a default to use for the date, so - # add in a hidden field that populates it. - unique_fields[date_field_name] = HiddenField(default=default) + 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: @@ -838,12 +849,16 @@ class ModelSerializer(Serializer): '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 unique_fields.items(): + for field_name, field in hidden_fields.items(): ret[field_name] = field return ret -- cgit v1.2.3 From 851628107842a5bf84725247a42cae1ac90decf6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 14:40:30 +0000 Subject: Minor fix for #2092. --- rest_framework/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2e34dbe7..3189619e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -736,12 +736,12 @@ class ModelSerializer(Serializer): ]) 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) + # 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=...' -- cgit v1.2.3 From 40b1ea919b00bd8bf1f53f8eb8bf33a498b237b8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 14:51:49 +0000 Subject: Fix non-determanistic unique constraint mapping. Refs #2092. --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3189619e..ce0d14d6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -721,6 +721,7 @@ class ModelSerializer(Serializer): # 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: @@ -729,12 +730,13 @@ class ModelSerializer(Serializer): continue # Include each of the `unique_for_*` field names. - unique_constraint_names = set([ + unique_constraint_names |= set([ model_field.unique_for_date, model_field.unique_for_month, model_field.unique_for_year ]) - unique_constraint_names -= set([None]) + + 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. -- cgit v1.2.3 From bde725541359de1fef785801fc5dad98e70a8e2f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Nov 2014 09:30:49 +0000 Subject: Fix non-determanistic default bug. Closes #2099. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ce0d14d6..2d5c843e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -757,7 +757,7 @@ class ModelSerializer(Serializer): elif getattr(unique_constraint_field, 'auto_now', None): default = timezone.now elif unique_constraint_field.has_default(): - default = model_field.default + default = unique_constraint_field.default else: default = empty -- cgit v1.2.3 From fd980be39be7f09d4cf0ceb16688ad0157d4df35 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 25 Nov 2014 10:39:58 +0000 Subject: Documentation in 'many_init' docstring. Refs #2120. --- rest_framework/serializers.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2d5c843e..00362dbb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -86,6 +86,15 @@ class BaseSerializer(Field): 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} -- cgit v1.2.3 From 6b2033f7894319c827c04a5caded7187797fc0b3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Nov 2014 16:40:58 +0000 Subject: Drop transform_ --- rest_framework/serializers.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 00362dbb..e86e67f7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -371,14 +371,9 @@ class Serializer(BaseSerializer): for field in fields: attribute = field.get_attribute(instance) if attribute is None: - value = None + ret[field.field_name] = None else: - value = field.to_representation(attribute) - transform_method = getattr(self, 'transform_' + field.field_name, None) - if transform_method is not None: - value = transform_method(value) - - ret[field.field_name] = value + ret[field.field_name] = field.to_representation(attribute) return ret -- cgit v1.2.3 From 34ca8cd2a5c030d9acc89720876ba9583c1dc988 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Nov 2014 09:56:44 +0000 Subject: Moar docs. Amazing. --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 00362dbb..14078e64 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -102,7 +102,9 @@ class BaseSerializer(Field): (key, value) for key, value in kwargs.items() if key in LIST_SERIALIZER_KWARGS ])) - return ListSerializer(*args, **list_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.') -- cgit v1.2.3 From b9503cd603613e4ae72b7718ba70a00b1537b289 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 10:48:45 +0000 Subject: Support Django's core ValidationError for backwards compat. Refs #2145. --- rest_framework/serializers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f7aa3a7d..de0d026d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -11,6 +11,7 @@ python primitives. response content is handled by parsers and renderers. """ from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six @@ -330,6 +331,14 @@ class Serializer(BaseSerializer): raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] }) + except DjangoValidationError as exc: + # Normally you should raise `serializers.ValidationError` + # inside your codebase, but we handle Django's validation + # exception class as well for simpler compat. + # Eg. Calling Model.clean() explictily inside Serializer.validate() + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) + }) return value @@ -353,6 +362,8 @@ class Serializer(BaseSerializer): validated_value = validate_method(validated_value) except ValidationError as exc: errors[field.field_name] = exc.detail + except DjangoValidationError as exc: + errors[field.field_name] = list(exc.messages) except SkipField: pass else: -- cgit v1.2.3 From 22c5b863bc395a6df84ec2536849540d3e92da7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 11:37:38 +0000 Subject: More descriptive docstring on ModelSerializer --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index de0d026d..3783b285 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -565,6 +565,14 @@ class ModelSerializer(Serializer): * A set of default fields are automatically populated. * A set of default validators are automatically populated. * Default `.create()` and `.update()` implementations are provided. + + The process of automatically determining a set of serializer fields + based on the model fields is reasonably complex, but you almost certainly + don't need to dig into the implemention. + + If the `ModelSerializer` class *doesn't* generate the set of fields that + you need you should either declare the extra/differing fields explicitly on + the serializer class, or simply use a `Serializer` class. """ _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, -- cgit v1.2.3 From 270c7acdd75e9dd0b7a600e5648b0ec364c9a510 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 11:59:04 +0000 Subject: Minor validtors tweak --- rest_framework/serializers.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3783b285..0d0a4d9a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -601,6 +601,26 @@ class ModelSerializer(Serializer): _related_class = PrimaryKeyRelatedField def create(self, validated_attrs): + """ + We have a bit of extra checking around this in order to provide + descriptive messages when something goes wrong, but this method is + essentially just: + + return ExampleModel.objects.create(**validated_attrs) + + If there are many to many fields present on the instance then they + cannot be set until the model is instantiated, in which case the + implementation is like so: + + example_relationship = validated_attrs.pop('example_relationship') + instance = ExampleModel.objects.create(**validated_attrs) + instance.example_relationship = example_relationship + return instance + + The default implementation also does not handle nested relationships. + If you want to support writable nested relationships you'll need + to write an explicit `.create()` method. + """ # 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()`. @@ -651,14 +671,19 @@ 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 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) ]) - 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()): -- cgit v1.2.3 From 79e18a2a06178e8c00dfafc1cfd062f2528ec2c1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 09:27:40 +0000 Subject: Raise assertion error if calling .save() on a serializer with errors. Closes #2098. --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0d0a4d9a..a4140c0f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -127,6 +127,14 @@ class BaseSerializer(Field): (self.__class__.__module__, self.__class__.__name__) ) + assert hasattr(self, '_errors'), ( + 'You must call `.is_valid()` before calling `.save()`.' + ) + + assert not self.errors, ( + 'You cannot call `.save()` on a serializer with invalid data.' + ) + validated_data = dict( list(self.validated_data.items()) + list(kwargs.items()) -- cgit v1.2.3 From 76ac641fbd6c9d7dff5da3c551c3fd1ef7dedd2e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 13:04:49 +0000 Subject: Minor tweaks for helpful message on Model.objects.create() failure. --- rest_framework/serializers.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 143d205d..d417ca80 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -35,7 +35,6 @@ from rest_framework.validators import ( ) import copy import inspect -import sys import warnings # Note: We do the following so that users of the framework can use this style: @@ -658,14 +657,20 @@ class ModelSerializer(Serializer): instance = ModelClass.objects.create(**validated_attrs) except TypeError as exc: msg = ( - 'The mentioned argument might be a field on the serializer ' - 'that is not part of the model. You need to override the ' - 'create() method in your ModelSerializer subclass to support ' - 'this.') - six.reraise( - type(exc), - type(exc)(str(exc) + '. ' + msg), - sys.exc_info()[2]) + 'Got a `TypeError` when calling `%s.objects.create()`. ' + 'This may be because you have a writable field on the ' + 'serializer class that is not a valid argument to ' + '`%s.objects.create()`. You may need to make the field ' + 'read-only, or override the %s.create() method to handle ' + 'this correctly.\nOriginal exception text was: %s.' % + ( + ModelClass.__name__, + ModelClass.__name__, + self.__class__.__name__, + exc + ) + ) + raise TypeError(msg) # Save many-to-many relationships after the instance is created. if many_to_many: -- cgit v1.2.3 From f2dd05a6e661525908fe5ec99b52b5274b04a198 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Dec 2014 22:43:40 +0000 Subject: Improved nested update test in update(). Closes #2194. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d417ca80..b1175b5b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -681,8 +681,8 @@ class ModelSerializer(Serializer): def update(self, instance, validated_attrs): assert not any( - isinstance(field, BaseSerializer) and not field.read_only - for field in self.fields.values() + isinstance(field, BaseSerializer) and (key in validated_attrs) + for key, field in self.fields.values() ), ( 'The `.update()` method does not suport nested writable fields ' 'by default. Write an explicit `.update()` method for serializer ' -- cgit v1.2.3 From e1d98f77563abf49c4b19dcfb95f263515ae4087 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Dec 2014 22:45:44 +0000 Subject: Improve nested update and create testing. --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b1175b5b..c7f04b40 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,8 +633,8 @@ class ModelSerializer(Serializer): # 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() + isinstance(field, BaseSerializer) and (key in validated_attrs) + for key, field in self.fields.items() ), ( 'The `.create()` method does not suport nested writable fields ' 'by default. Write an explicit `.create()` method for serializer ' @@ -682,7 +682,7 @@ class ModelSerializer(Serializer): def update(self, instance, validated_attrs): assert not any( isinstance(field, BaseSerializer) and (key in validated_attrs) - for key, field in self.fields.values() + for key, field in self.fields.items() ), ( 'The `.update()` method does not suport nested writable fields ' 'by default. Write an explicit `.update()` method for serializer ' -- cgit v1.2.3 From ab25d706c78627dfd582fe9d142ada510c4d6d90 Mon Sep 17 00:00:00 2001 From: Martin Tschammer Date: Wed, 3 Dec 2014 23:52:35 +0100 Subject: Renamed validated_attrs to validated_data to be more in line with other similar code. --- rest_framework/serializers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d417ca80..a289b021 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -608,20 +608,20 @@ class ModelSerializer(Serializer): }) _related_class = PrimaryKeyRelatedField - def create(self, validated_attrs): + def create(self, validated_data): """ We have a bit of extra checking around this in order to provide descriptive messages when something goes wrong, but this method is essentially just: - return ExampleModel.objects.create(**validated_attrs) + return ExampleModel.objects.create(**validated_data) If there are many to many fields present on the instance then they cannot be set until the model is instantiated, in which case the implementation is like so: - example_relationship = validated_attrs.pop('example_relationship') - instance = ExampleModel.objects.create(**validated_attrs) + example_relationship = validated_data.pop('example_relationship') + instance = ExampleModel.objects.create(**validated_data) instance.example_relationship = example_relationship return instance @@ -644,17 +644,17 @@ class ModelSerializer(Serializer): ModelClass = self.Meta.model - # Remove many-to-many relationships from validated_attrs. + # Remove many-to-many relationships from validated_data. # 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 validated_attrs): - many_to_many[field_name] = validated_attrs.pop(field_name) + if relation_info.to_many and (field_name in validated_data): + many_to_many[field_name] = validated_data.pop(field_name) try: - instance = ModelClass.objects.create(**validated_attrs) + instance = ModelClass.objects.create(**validated_data) except TypeError as exc: msg = ( 'Got a `TypeError` when calling `%s.objects.create()`. ' @@ -679,7 +679,7 @@ class ModelSerializer(Serializer): return instance - def update(self, instance, validated_attrs): + def update(self, instance, validated_data): assert not any( isinstance(field, BaseSerializer) and not field.read_only for field in self.fields.values() @@ -690,7 +690,7 @@ class ModelSerializer(Serializer): (self.__class__.__module__, self.__class__.__name__) ) - for attr, value in validated_attrs.items(): + for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance -- cgit v1.2.3 From d9930181ee157f51e2fcea33a3af5ea397647324 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Fri, 5 Dec 2014 00:29:28 +0100 Subject: Removed unused imports, pep8 fixes, typo fixes --- rest_framework/serializers.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index af8aeb48..e1851ddd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,17 +10,13 @@ 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 -from django.core.exceptions import ValidationError as DjangoValidationError +import warnings + from django.db import models from django.db.models.fields import FieldDoesNotExist -from django.utils import six 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 import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, get_relation_kwargs, get_nested_relation_kwargs, @@ -33,9 +29,7 @@ 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: # @@ -65,6 +59,7 @@ 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): self.instance = instance self._initial_data = data @@ -245,7 +240,7 @@ class Serializer(BaseSerializer): """ A dictionary of {field_name: field_instance}. """ - # `fields` is evalutated lazily. We do this to ensure that we don't + # `fields` is evaluated 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'): @@ -343,7 +338,7 @@ class Serializer(BaseSerializer): # Normally you should raise `serializers.ValidationError` # inside your codebase, but we handle Django's validation # exception class as well for simpler compat. - # Eg. Calling Model.clean() explictily inside Serializer.validate() + # Eg. Calling Model.clean() explicitly inside Serializer.validate() raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) }) @@ -576,7 +571,7 @@ class ModelSerializer(Serializer): The process of automatically determining a set of serializer fields based on the model fields is reasonably complex, but you almost certainly - don't need to dig into the implemention. + don't need to dig into the implementation. If the `ModelSerializer` class *doesn't* generate the set of fields that you need you should either declare the extra/differing fields explicitly on @@ -636,7 +631,7 @@ class ModelSerializer(Serializer): isinstance(field, BaseSerializer) and (key in validated_attrs) for key, field in self.fields.items() ), ( - 'The `.create()` method does not suport nested writable fields ' + 'The `.create()` method does not support 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__) @@ -684,7 +679,7 @@ class ModelSerializer(Serializer): isinstance(field, BaseSerializer) and (key in validated_attrs) for key, field in self.fields.items() ), ( - 'The `.update()` method does not suport nested writable fields ' + 'The `.update()` method does not support 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__) @@ -824,7 +819,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. for unique_constraint_name in unique_constraint_names: - # Get the model field that is refered too. + # 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): @@ -907,7 +902,7 @@ class ModelSerializer(Serializer): ) # Check that any fields declared on the class are - # also explicity included in `Meta.fields`. + # also explicitly included in `Meta.fields`. missing_fields = set(declared_fields.keys()) - set(fields) if missing_fields: missing_field = list(missing_fields)[0] @@ -1001,6 +996,7 @@ class ModelSerializer(Serializer): class Meta: model = relation_info.related depth = nested_depth + return NestedSerializer @@ -1027,4 +1023,5 @@ class HyperlinkedModelSerializer(ModelSerializer): class Meta: model = relation_info.related depth = nested_depth + return NestedSerializer -- cgit v1.2.3 From d68c61450440a522b08b64fdd21028cc739e6ead Mon Sep 17 00:00:00 2001 From: BrickXu Date: Fri, 5 Dec 2014 14:50:53 +0800 Subject: Add validation for fields & exclude type. --- rest_framework/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index af8aeb48..96cd51e3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -759,6 +759,12 @@ class ModelSerializer(Serializer): depth = getattr(self.Meta, 'depth', 0) extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) + if fields and not isinstance(fields, (list, tuple)): + raise TypeError('`fields` must be a list or tuple') + + if exclude and not isinstance(exclude, (list, tuple)): + raise TypeError('`exclude` must be a list or tuple') + assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'." extra_kwargs = self._include_additional_options(extra_kwargs) -- cgit v1.2.3 From ca74fa989dd5a3236894736c838fe0a21c312e2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Dec 2014 13:50:28 +0000 Subject: Better serializer errors for nested writes. Closes #2202 --- rest_framework/serializers.py | 81 ++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 21 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e1851ddd..68d0b8cc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -561,6 +561,64 @@ class ListSerializer(BaseSerializer): # ModelSerializer & HyperlinkedModelSerializer # -------------------------------------------- +def raise_errors_on_nested_writes(method_name, serializer): + """ + Give explicit errors when users attempt to pass writable nested data. + + If we don't do this explicitly they'd get a less helpful error when + calling `.save()` on the serializer. + + We don't *automatically* support these sorts of nested writes brecause + there are too many ambiguities to define a default behavior. + + Eg. Suppose we have a `UserSerializer` with a nested profile. How should + we handle the case of an update, where the `profile` realtionship does + not exist? Any of the following might be valid: + + * Raise an application error. + * Silently ignore the nested part of the update. + * Automatically create a profile instance. + """ + + # Ensure we don't have a writable nested field. For example: + # + # class UserSerializer(ModelSerializer): + # ... + # profile = ProfileSerializer() + assert not any( + isinstance(field, BaseSerializer) and (key in validated_attrs) + for key, field in serializer.fields.items() + ), ( + 'The `.{method_name}()` method does not support nested writable ' + 'fields by default. Write an explicit `.{method_name}()` method for ' + 'serializer `{module}.{class_name}`, or set `read_only=True` on ' + 'nested serializer fields.'.format( + method_name=method_name, + module=serializer.__class__.__module__, + class_name=serializer.__class__.__name__ + ) + ) + + # Ensure we don't have a writable dotted-source field. For example: + # + # class UserSerializer(ModelSerializer): + # ... + # address = serializer.CharField('profile.address') + assert not any( + '.' in field.source and (key in validated_attrs) + for key, field in serializer.fields.items() + ), ( + 'The `.{method_name}()` method does not support writable dotted-source ' + 'fields by default. Write an explicit `.{method_name}()` method for ' + 'serializer `{module}.{class_name}`, or set `read_only=True` on ' + 'dotted-source serializer fields.'.format( + method_name=method_name, + module=serializer.__class__.__module__, + class_name=serializer.__class__.__name__ + ) + ) + + class ModelSerializer(Serializer): """ A `ModelSerializer` is just a regular `Serializer`, except that: @@ -624,18 +682,7 @@ class ModelSerializer(Serializer): If you want to support writable nested relationships you'll need to write an explicit `.create()` method. """ - # 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 (key in validated_attrs) - for key, field in self.fields.items() - ), ( - 'The `.create()` method does not support 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__) - ) + raise_errors_on_nested_writes('create', self) ModelClass = self.Meta.model @@ -675,15 +722,7 @@ class ModelSerializer(Serializer): return instance def update(self, instance, validated_data): - assert not any( - isinstance(field, BaseSerializer) and (key in validated_attrs) - for key, field in self.fields.items() - ), ( - 'The `.update()` method does not support 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__) - ) + raise_errors_on_nested_writes('update', self) for attr, value in validated_data.items(): setattr(instance, attr, value) -- cgit v1.2.3 From 88900a0844f1b0cd996235ae0f99105563ae6473 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Dec 2014 13:58:39 +0000 Subject: Minor tweaks --- rest_framework/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 68d0b8cc..c022cad3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -589,8 +589,8 @@ def raise_errors_on_nested_writes(method_name, serializer): isinstance(field, BaseSerializer) and (key in validated_attrs) for key, field in serializer.fields.items() ), ( - 'The `.{method_name}()` method does not support nested writable ' - 'fields by default. Write an explicit `.{method_name}()` method for ' + 'The `.{method_name}()` method does not support writable nested' + 'fields by default.\nWrite an explicit `.{method_name}()` method for ' 'serializer `{module}.{class_name}`, or set `read_only=True` on ' 'nested serializer fields.'.format( method_name=method_name, @@ -609,7 +609,7 @@ def raise_errors_on_nested_writes(method_name, serializer): for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable dotted-source ' - 'fields by default. Write an explicit `.{method_name}()` method for ' + 'fields by default.\nWrite an explicit `.{method_name}()` method for ' 'serializer `{module}.{class_name}`, or set `read_only=True` on ' 'dotted-source serializer fields.'.format( method_name=method_name, @@ -727,6 +727,7 @@ class ModelSerializer(Serializer): for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() + return instance def get_validators(self): -- cgit v1.2.3 From 544967f36ed6e29819018428d48da00de74958b9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Dec 2014 14:15:58 +0000 Subject: Test tweaks --- rest_framework/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8784b303..d8e544d4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -795,10 +795,16 @@ class ModelSerializer(Serializer): extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) if fields and not isinstance(fields, (list, tuple)): - raise TypeError('`fields` must be a list or 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('`exclude` must be a list or 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'." -- cgit v1.2.3 From a257b04928f07656ac4541e0a3fae0afad2848bb Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 7 Dec 2014 12:12:40 +0100 Subject: Fix missing validated_data in `raise_errors_on_nested_writes` (#2221) --- rest_framework/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d8e544d4..39523077 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -561,7 +561,7 @@ class ListSerializer(BaseSerializer): # ModelSerializer & HyperlinkedModelSerializer # -------------------------------------------- -def raise_errors_on_nested_writes(method_name, serializer): +def raise_errors_on_nested_writes(method_name, serializer, validated_data): """ Give explicit errors when users attempt to pass writable nested data. @@ -586,7 +586,7 @@ def raise_errors_on_nested_writes(method_name, serializer): # ... # profile = ProfileSerializer() assert not any( - isinstance(field, BaseSerializer) and (key in validated_attrs) + isinstance(field, BaseSerializer) and (key in validated_data) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable nested' @@ -605,7 +605,7 @@ def raise_errors_on_nested_writes(method_name, serializer): # ... # address = serializer.CharField('profile.address') assert not any( - '.' in field.source and (key in validated_attrs) + '.' in field.source and (key in validated_data) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable dotted-source ' @@ -682,7 +682,7 @@ class ModelSerializer(Serializer): If you want to support writable nested relationships you'll need to write an explicit `.create()` method. """ - raise_errors_on_nested_writes('create', self) + raise_errors_on_nested_writes('create', self, validated_data) ModelClass = self.Meta.model @@ -722,7 +722,7 @@ class ModelSerializer(Serializer): return instance def update(self, instance, validated_data): - raise_errors_on_nested_writes('update', self) + raise_errors_on_nested_writes('update', self, validated_data) for attr, value in validated_data.items(): setattr(instance, attr, value) -- cgit v1.2.3 From eee02a47d997bd4439fe5fbdc01979d8f372247a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Dec 2014 14:56:45 +0000 Subject: Added ListSerializer.validate(). Closes #2168. --- rest_framework/serializers.py | 108 +++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 43 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 39523077..fb6c826b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -229,6 +229,35 @@ class SerializerMetaclass(type): return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) +def get_validation_error_detail(exc): + assert isinstance(exc, (ValidationError, DjangoValidationError)) + + if isinstance(exc, DjangoValidationError): + # Normally you should raise `serializers.ValidationError` + # inside your codebase, but we handle Django's validation + # exception class as well for simpler compat. + # Eg. Calling Model.clean() explicitly inside Serializer.validate() + return { + api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) + } + elif isinstance(exc.detail, dict): + # If errors may be a dict we use the standard {key: list of values}. + # Here we ensure that all the values are *lists* of errors. + return dict([ + (key, value if isinstance(value, list) else [value]) + for key, value in exc.detail.items() + ]) + elif isinstance(exc.detail, list): + # Errors raised as a list are non-field errors. + return { + api_settings.NON_FIELD_ERRORS_KEY: exc.detail + } + # Errors raised as a string are non-field errors. + return { + api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] + } + + @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): default_error_messages = { @@ -293,55 +322,17 @@ class Serializer(BaseSerializer): 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: [message] - }) + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data 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] - }) - except DjangoValidationError as exc: - # Normally you should raise `serializers.ValidationError` - # inside your codebase, but we handle Django's validation - # exception class as well for simpler compat. - # Eg. Calling Model.clean() explicitly inside Serializer.validate() - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) - }) + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=get_validation_error_detail(exc)) return value @@ -349,6 +340,14 @@ class Serializer(BaseSerializer): """ Dict of native values <- Dict of primitive datatypes. """ + if not isinstance(data, dict): + message = self.error_messages['invalid'].format( + datatype=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }) + ret = OrderedDict() errors = OrderedDict() fields = [ @@ -462,6 +461,26 @@ class ListSerializer(BaseSerializer): return html.parse_html_list(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) + def run_validation(self, data=empty): + """ + 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. + """ + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data + + 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, DjangoValidationError) as exc: + raise ValidationError(detail=get_validation_error_detail(exc)) + + return value + def to_internal_value(self, data): """ List of dicts of native values <- List of dicts of primitive datatypes. @@ -503,6 +522,9 @@ class ListSerializer(BaseSerializer): self.child.to_representation(item) for item in iterable ] + def validate(self, attrs): + return attrs + def update(self, instance, validated_data): raise NotImplementedError( "Serializers with many=True do not support multiple update by " -- cgit v1.2.3 From afe7ed9333e37384f8ddc57e891da9632c8714c3 Mon Sep 17 00:00:00 2001 From: José Padilla Date: Tue, 9 Dec 2014 09:25:06 -0400 Subject: Add allow_blank for ChoiceField #2184 This makes a ChoiceField optional in HTML if model field has `blank=True` set.--- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fb6c826b..b0c0efa7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -942,7 +942,7 @@ 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): + 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) -- cgit v1.2.3 From 8d6b0b1f2d3a5014d43f1314d96bc9197709b542 Mon Sep 17 00:00:00 2001 From: Julio Iván Alegre Date: Tue, 9 Dec 2014 16:13:18 +0100 Subject: Update serializers.py Treat the input queryset as it comes (maybe it has been changed in a higher level). Evaluating .all() does nothing if or if not is a queryset.--- rest_framework/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b0c0efa7..5c73c5f2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -517,9 +517,8 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ - iterable = data.all() if (hasattr(data, 'all')) else data return [ - self.child.to_representation(item) for item in iterable + self.child.to_representation(item) for item in data ] def validate(self, attrs): -- cgit v1.2.3 From 59470667db1b95eef63ff4308c1b41561438bb8e Mon Sep 17 00:00:00 2001 From: J. Iván Alegre Date: Tue, 9 Dec 2014 17:49:07 +0100 Subject: Take in care that input data in serializer can be a Manager --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c73c5f2..9857995a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -517,8 +517,9 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ + iterable = data.all() if (isinstance(data, models.Manager) and hasattr(data, 'all')) else data return [ - self.child.to_representation(item) for item in data + self.child.to_representation(item) for item in iterable ] def validate(self, attrs): -- cgit v1.2.3 From 9161e5a9276b185375e9ab7ec188112f6fe49cf0 Mon Sep 17 00:00:00 2001 From: J. Iván Alegre Date: Wed, 10 Dec 2014 09:19:27 +0100 Subject: Remove unnecessary hasattr all and add comment for nested relationships --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9857995a..e1711259 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -517,7 +517,9 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ - iterable = data.all() if (isinstance(data, models.Manager) and hasattr(data, 'all')) else data + # Dealing with nested relationships, data can be a Manager, + # so, first get a queryset from the Manager if needed + iterable = data.all() if isinstance(data, models.Manager) else data return [ self.child.to_representation(item) for item in iterable ] -- cgit v1.2.3 From 428630c19702172beba94a3381d91340aa5e3bd6 Mon Sep 17 00:00:00 2001 From: J. Iván Alegre Date: Wed, 10 Dec 2014 10:13:15 +0100 Subject: Fix trailing space --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e1711259..9226895e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -517,7 +517,7 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ - # Dealing with nested relationships, data can be a Manager, + # Dealing with nested relationships, data can be a Manager, # so, first get a queryset from the Manager if needed iterable = data.all() if isinstance(data, models.Manager) else data return [ -- cgit v1.2.3 From c0b9115beca00fda36a50532fbe62e3b39b4e972 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 13 Dec 2014 14:17:24 +0000 Subject: Improve check for nested writes --- rest_framework/serializers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9226895e..5adbca3b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -611,6 +611,7 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # profile = ProfileSerializer() assert not any( isinstance(field, BaseSerializer) and (key in validated_data) + and isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable nested' @@ -630,6 +631,7 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # address = serializer.CharField('profile.address') assert not any( '.' in field.source and (key in validated_data) + and isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable dotted-source ' -- cgit v1.2.3 From 72e08a3e8b6427cb93f0f98b42724e31e5b3d8f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Dec 2014 11:55:17 +0000 Subject: Use unicode internally everywhere for 'repr' --- rest_framework/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5adbca3b..e9860a2f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,12 +10,11 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -import warnings - +from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.translation import ugettext_lazy as _ - +from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, @@ -29,6 +28,7 @@ from rest_framework.validators import ( UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, UniqueTogetherValidator ) +import warnings # Note: We do the following so that users of the framework can use this style: @@ -396,7 +396,7 @@ class Serializer(BaseSerializer): return attrs def __repr__(self): - return representation.serializer_repr(self, indent=1) + return unicode_to_repr(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 @@ -564,7 +564,7 @@ class ListSerializer(BaseSerializer): return self.instance def __repr__(self): - return representation.list_repr(self, indent=1) + return unicode_to_repr(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. -- cgit v1.2.3 From c6137bbf5aa7ca800e4afc06657e5196b2e0e481 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Dec 2014 14:14:51 +0000 Subject: Serializer API restrictions. --- rest_framework/serializers.py | 51 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e9860a2f..8de22f4b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -58,11 +58,31 @@ class BaseSerializer(Field): """ The BaseSerializer class provides a minimal class which may be used for writing custom serializer implementations. + + Note that we strongly restrict the ordering of operations/properties + that may be used on the serializer in order to enforce correct usage. + + In particular, if a `data=` argument is passed then: + + .is_valid() - Available. + .initial_data - Available. + .validated_data - Only available after calling `is_valid()` + .errors - Only available after calling `is_valid()` + .data - Only available after calling `is_valid()` + + If a `data=` argument is not passed then: + + .is_valid() - Not available. + .initial_data - Not available. + .validated_data - Not available. + .errors - Not available. + .data - Available. """ - def __init__(self, instance=None, data=None, **kwargs): + def __init__(self, instance=None, data=empty, **kwargs): self.instance = instance - self._initial_data = data + if data is not empty: + self.initial_data = data self.partial = kwargs.pop('partial', False) self._context = kwargs.pop('context', {}) kwargs.pop('many', None) @@ -156,9 +176,14 @@ class BaseSerializer(Field): (self.__class__.__module__, self.__class__.__name__) ) + assert hasattr(self, 'initial_data'), ( + 'Cannot call `.is_valid()` as no `data=` keyword argument was' + 'passed when instantiating the serializer instance.' + ) + if not hasattr(self, '_validated_data'): try: - self._validated_data = self.run_validation(self._initial_data) + self._validated_data = self.run_validation(self.initial_data) except ValidationError as exc: self._validated_data = {} self._errors = exc.detail @@ -172,6 +197,16 @@ class BaseSerializer(Field): @property def data(self): + if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'): + msg = ( + 'When a serializer is passed a `data` keyword argument you ' + 'must call `.is_valid()` before attempting to access the ' + 'serialized `.data` representation.\n' + 'You should either call `.is_valid()` first, ' + 'or access `.initial_data` instead.' + ) + raise AssertionError(msg) + if not hasattr(self, '_data'): if self.instance is not None and not getattr(self, '_errors', None): self._data = self.to_representation(self.instance) @@ -295,11 +330,11 @@ class Serializer(BaseSerializer): return getattr(getattr(self, 'Meta', None), 'validators', []) def get_initial(self): - if self._initial_data is not None: + if hasattr(self, 'initial_data'): return OrderedDict([ - (field_name, field.get_value(self._initial_data)) + (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 + if field.get_value(self.initial_data) is not empty and not field.read_only ]) @@ -447,8 +482,8 @@ class ListSerializer(BaseSerializer): self.child.bind(field_name='', parent=self) def get_initial(self): - if self._initial_data is not None: - return self.to_representation(self._initial_data) + if hasattr(self, 'initial_data'): + return self.to_representation(self.initial_data) return [] def get_value(self, dictionary): -- cgit v1.2.3 From d60ecfc4982ff5ccfacd285404fc54e3e0fd6567 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 18 Dec 2014 16:03:15 +0100 Subject: Assert fields in `exclude` are model fields --- rest_framework/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8de22f4b..ed709d84 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -880,6 +880,10 @@ class ModelSerializer(Serializer): 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) # Determine the set of model fields, and the fields that they map to. -- cgit v1.2.3 From 5830f7e13817210f5c6d955ad4fedfaa492aa209 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 10:15:36 +0000 Subject: get_unique_together_validators and get_unique_for_date_validators --- rest_framework/serializers.py | 51 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8de22f4b..55828b03 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 ) -- cgit v1.2.3 From 6d907cde9a90aad76acb00482a1d70550bb95ccd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 12:18:40 +0000 Subject: get_field_names, get_default_field_names --- rest_framework/serializers.py | 102 +++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 30 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 52ea5b0b..b391a94e 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()) + -- cgit v1.2.3 From 1a84943a006abffb7e1b3b3ff55441c7a1132fa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 12:27:50 +0000 Subject: get_extra_kwargs --- rest_framework/serializers.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b391a94e..d4b0926e 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: -- cgit v1.2.3 From caa13181244ce3c074f647510bb38d7b0c8b4c70 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 13:13:20 +0000 Subject: get_uniqueness_field_options first pass --- rest_framework/serializers.py | 174 +++++++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 78 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d4b0926e..5e9cbe36 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 -- cgit v1.2.3 From 4a112fc3a616238b7995b3a442ae236116364ceb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 14:51:45 +0000 Subject: Clean up --- rest_framework/serializers.py | 63 ++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 21 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5e9cbe36..093b0eb5 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): -- cgit v1.2.3 From 80bacc5fb00682b589b3280c7082af73e3aaa8f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 14:52:53 +0000 Subject: depth should reduce by one on each nesting level. Closes #2287. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ed709d84..6f89df0d 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 -- cgit v1.2.3 From f72928ea982cfe2127288dd6dc52f8006638b0c3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:09:57 +0000 Subject: build_field, build_final_kwargs --- rest_framework/serializers.py | 135 ++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 59 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1f76c4c1..80ad10f0 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 -- cgit v1.2.3 From 75e81b82545704bac8afdf3270ba9f6c8da09c27 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:35:52 +0000 Subject: build_*_field methods --- rest_framework/serializers.py | 611 +++++++++++++++++++++++------------------- 1 file changed, 337 insertions(+), 274 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 80ad10f0..a983d3fc 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,98 +793,13 @@ class ModelSerializer(Serializer): return instance - # Determine the validators to apply... - - def get_validators(self): - """ - Determine the set of validators to use when instantiating serializer. - """ - # If the validators have been declared explicitly then use that. - validators = getattr(getattr(self, 'Meta', None), 'validators', None) - if validators is not None: - return validators - - # Otherwise use the default set of validators. - return ( - self.get_unique_together_validators() + - self.get_unique_for_date_validators() - ) - - def get_unique_together_validators(self): - """ - Determine a default set of validators for any unique_together contraints. - """ - model_class_inheritance_tree = ( - [self.Meta.model] + - list(self.Meta.model._meta.parents.keys()) - ) - - # The field names we're passing though here only include fields - # which may map onto a model field. Any dotted field name lookups - # cannot map to a field, and must be a traversal, so we're not - # including those. - field_names = set([ - field.source for field in self.fields.values() - if (field.source != '*') and ('.' not in field.source) - ]) - - # Note that we make sure to check `unique_together` both on the - # base model class, but also on any parent classes. - validators = [] - for parent_class in model_class_inheritance_tree: - for unique_together in parent_class._meta.unique_together: - if field_names.issuperset(set(unique_together)): - validator = UniqueTogetherValidator( - queryset=parent_class._default_manager, - fields=unique_together - ) - validators.append(validator) - return validators - - def get_unique_for_date_validators(self): - """ - Determine a default set of validators for the following contraints: - - * unique_for_date - * unique_for_month - * unique_for_year - """ - info = model_meta.get_field_info(self.Meta.model) - default_manager = self.Meta.model._default_manager - field_names = [field.source for field in self.fields.values()] - - validators = [] - - for field_name, field in info.fields_and_pk.items(): - if field.unique_for_date and field_name in field_names: - validator = UniqueForDateValidator( - queryset=default_manager, - field=field_name, - date_field=field.unique_for_date - ) - validators.append(validator) - - if field.unique_for_month and field_name in field_names: - validator = UniqueForMonthValidator( - queryset=default_manager, - field=field_name, - date_field=field.unique_for_month - ) - validators.append(validator) - - if field.unique_for_year and field_name in field_names: - validator = UniqueForYearValidator( - queryset=default_manager, - field=field_name, - date_field=field.unique_for_year - ) - validators.append(validator) - - return validators - # Determine the fields to apply... 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) @@ -912,7 +827,7 @@ class ModelSerializer(Serializer): 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) + kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name) # Create the serializer field. ret[field_name] = field_cls(**kwargs) @@ -922,57 +837,188 @@ class ModelSerializer(Serializer): return ret - def build_field(self, field_name, info, model, depth): + # Methods for determining the set of field names to include... + + def get_field_names(self, declared_fields, info): + """ + Returns the list of all field names that should be created when + instantiating this serializer class. This is based on the default + set of fields, but also takes into account the `Meta.fields` or + `Meta.exclude` options if they have been specified. + """ + fields = getattr(self.Meta, 'fields', None) + exclude = getattr(self.Meta, 'exclude', None) + + if fields and not isinstance(fields, (list, tuple)): + raise TypeError( + 'The `fields` option must be a list or tuple. Got %s.' % + type(fields).__name__ + ) + + if exclude and not isinstance(exclude, (list, tuple)): + raise TypeError( + 'The `exclude` option must be a list or tuple. Got %s.' % + type(exclude).__name__ + ) + + assert not (fields and exclude), ( + "Cannot set both 'fields' and 'exclude' options on " + "serializer {serializer_class}.".format( + serializer_class=self.__class__.__name__ + ) + ) + + if fields is not None: + # Ensure that all declared fields have also been included in the + # `Meta.fields` option. + for field_name in declared_fields: + assert field_name in fields, ( + "The field '{field_name}' was declared on serializer " + "{serializer_class}, but has not been included in the " + "'fields' option.".format( + field_name=field_name, + serializer_class=self.__class__.__name__ + ) + ) + return fields + + # Use the default set of field names if `Meta.fields` is not specified. + fields = self.get_default_field_names(declared_fields, info) + + if exclude is not None: + # If `Meta.exclude` is included, then remove those fields. + for field_name in exclude: + assert field_name in fields, ( + "The field '{field_name}' was include on serializer " + "{serializer_class} in the 'exclude' option, but does " + "not match any model field.".format( + field_name=field_name, + serializer_class=self.__class__.__name__ + ) + ) + fields.remove(field_name) + + return fields + + def get_default_field_names(self, declared_fields, model_info): + """ + Return the default list of field names that will be used if the + `Meta.fields` option is not specified. + """ + return ( + [model_info.pk.name] + + list(declared_fields.keys()) + + list(model_info.fields.keys()) + + list(model_info.forward_relations.keys()) + ) + + # Methods for constructing serializer fields... + + def build_field(self, field_name, info, model, nested_depth): + """ + Return a two tuple of (cls, kwargs) to build a serializer field with. + """ if field_name in info.fields_and_pk: - # 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) + return self.build_standard_field(field_name, info, model) 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) + if not nested_depth: + return self.build_relational_field(field_name, info, model) 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) + return self.build_nested_field(field_name, info, model, nested_depth) elif hasattr(model, field_name): - # Create a read only field for model methods and properties. - field_cls = ReadOnlyField - kwargs = {} + return self.build_property_field(field_name, info, model) elif field_name == api_settings.URL_FIELD_NAME: - # Create the URL field. - field_cls = HyperlinkedIdentityField - kwargs = get_url_kwargs(model) + return self.build_url_field(field_name, info, model) - else: - raise ImproperlyConfigured( - 'Field name `%s` is not valid for model `%s`.' % - (field_name, model.__class__.__name__) - ) + 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_final_kwargs(self, kwargs, extra_kwargs, field_name): + def build_unknown_field(self, field_name, info, model): + """ + Raise an error on any unknown fields. + """ + raise ImproperlyConfigured( + 'Field name `%s` is not valid for model `%s`.' % + (field_name, model.__class__.__name__) + ) + + def build_field_kwargs(self, kwargs, extra_kwargs, field_name): """ Include an 'extra_kwargs' that have been included for this field, possibly removing any incompatible existing keyword arguments. @@ -994,38 +1040,61 @@ class ModelSerializer(Serializer): return kwargs - def _get_model_fields(self, field_names, declared_fields, extra_kwargs): + # Methods for determining additional keyword arguments to apply... + + def get_extra_kwargs(self): """ - Returns all the model fields that are being mapped to by fields - on the serializer class. - Returned as a dict of 'model field name' -> 'model field'. - Used internally by `get_uniqueness_field_options`. + Return a dictionary mapping field names to a dictionary of + additional keyword arguments. """ - model = getattr(self.Meta, 'model') - model_fields = {} + extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) - 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 + 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 - if '.' in source or source == '*': - # Model fields will always have a simple source mapping, - # they can't be nested attribute lookups. - continue + # 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 - try: - model_fields[source] = model._meta.get_field(source) - except FieldDoesNotExist: - pass + 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 - return model_fields + lookup_field = getattr(self.Meta, 'lookup_field', None) + if lookup_field is not None: + warnings.warn( + "The `Meta.lookup_field` option is pending deprecation. " + "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.", + PendingDeprecationWarning, + stacklevel=3 + ) + kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) + kwargs['lookup_field'] = lookup_field + extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs + + return extra_kwargs def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs): """ @@ -1102,140 +1171,127 @@ class ModelSerializer(Serializer): return extra_kwargs, hidden_fields - def get_extra_kwargs(self): + def _get_model_fields(self, field_names, declared_fields, extra_kwargs): """ - Return a dictionary mapping field names to a dictionary of - additional keyword arguments. + 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`. """ - extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) + model = getattr(self.Meta, 'model') + model_fields = {} - 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 + 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 - # 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 + if '.' in source or source == '*': + # Model fields will always have a simple source mapping, + # they can't be nested attribute lookups. + continue - 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 + try: + model_fields[source] = model._meta.get_field(source) + except FieldDoesNotExist: + pass - 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 model_fields - return extra_kwargs + # Determine the validators to apply... - def get_field_names(self, declared_fields, info): + def get_validators(self): """ - Returns the list of all field names that should be created when - 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. + Determine the set of validators to use when instantiating serializer. """ - 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__ - ) + # 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 - assert not (fields and exclude), ( - "Cannot set both 'fields' and 'exclude' options on " - "serializer {serializer_class}.".format( - serializer_class=self.__class__.__name__ - ) + # Otherwise use the default set of validators. + return ( + self.get_unique_together_validators() + + self.get_unique_for_date_validators() ) - 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 + 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()) + ) - # Use the default set of field names if `Meta.fields` is not specified. - fields = self.get_default_field_names(declared_fields, info) + # 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) + ]) - 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__ + # 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 ) - ) - fields.remove(field_name) - - return fields + validators.append(validator) + return validators - def get_default_field_names(self, declared_fields, model_info): + def get_unique_for_date_validators(self): """ - Return the default list of field names that will be used if the - `Meta.fields` option is not specified. + Determine a default set of validators for the following contraints: + + * unique_for_date + * unique_for_month + * unique_for_year """ - return ( - [model_info.pk.name] + - list(declared_fields.keys()) + - list(model_info.fields.keys()) + - list(model_info.forward_relations.keys()) - ) + 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()] - def _get_nested_class(self, nested_depth, relation_info): - class NestedSerializer(ModelSerializer): - class Meta: - model = relation_info.related - depth = nested_depth - 1 + validators = [] - return NestedSerializer + for field_name, field in info.fields_and_pk.items(): + if field.unique_for_date and field_name in field_names: + validator = UniqueForDateValidator( + queryset=default_manager, + field=field_name, + date_field=field.unique_for_date + ) + validators.append(validator) + + if field.unique_for_month and field_name in field_names: + validator = UniqueForMonthValidator( + queryset=default_manager, + field=field_name, + date_field=field.unique_for_month + ) + validators.append(validator) + + if field.unique_for_year and field_name in field_names: + validator = UniqueForYearValidator( + queryset=default_manager, + field=field_name, + date_field=field.unique_for_year + ) + validators.append(validator) + + return validators class HyperlinkedModelSerializer(ModelSerializer): @@ -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 -- cgit v1.2.3 From 62f78dfbf1b1dfa2d6406a4be5b83bc69267e851 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 15:50:29 +0000 Subject: Copy validators lists on instantiation. --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a983d3fc..8adbafe4 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 ( -- cgit v1.2.3 From 2a1485e00943b8280245d19e1e1f8514b1ef18ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 21:32:43 +0000 Subject: Final bits of docs for ModelSerializer fields API --- rest_framework/serializers.py | 140 +++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 62 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8adbafe4..623ed586 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 -- cgit v1.2.3 From b6ca7248ebcf95a95e1911aa0b130f653b8bf690 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 5 Jan 2015 14:32:12 +0000 Subject: required=False allows omission of value for output. Closes #2342 --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6f89df0d..53f092d7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -419,8 +419,14 @@ class Serializer(BaseSerializer): fields = [field for field in self.fields.values() if not field.write_only] for field in fields: - attribute = field.get_attribute(instance) + try: + attribute = field.get_attribute(instance) + except SkipField: + continue + if attribute is None: + # We skip `to_representation` for `None` values so that + # fields do not have to explicitly deal with that case. ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) -- cgit v1.2.3 From 49dc037a961b618baf8eb189b094633238867b41 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 5 Jan 2015 15:03:09 +0000 Subject: Update docstring --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 623ed586..08a58433 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -236,11 +236,11 @@ class BaseSerializer(Field): class SerializerMetaclass(type): """ - This metaclass sets a dictionary named `base_fields` on the class. + This metaclass sets a dictionary named `_declared_fields` on the class. Any instances of `Field` included as attributes on either the class or on any of its superclasses will be include in the - `base_fields` dictionary. + `_declared_fields` dictionary. """ @classmethod -- cgit v1.2.3 From 6fd33ddea9e5b8f9e979e573a27873131846ea48 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 5 Jan 2015 15:04:01 +0000 Subject: Udpate docstring --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 53f092d7..e373cd10 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -236,11 +236,11 @@ class BaseSerializer(Field): class SerializerMetaclass(type): """ - This metaclass sets a dictionary named `base_fields` on the class. + This metaclass sets a dictionary named `_declared_fields` on the class. Any instances of `Field` included as attributes on either the class or on any of its superclasses will be include in the - `base_fields` dictionary. + `_declared_fields` dictionary. """ @classmethod -- cgit v1.2.3 From 4c32083b8b59a50877633910055313dad7bb117e Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Wed, 7 Jan 2015 12:01:11 +0000 Subject: use double quotes for user visible strings; end user visible strings in full stops; add some missing translation tags --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 623ed586..9d7c8884 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -296,7 +296,7 @@ def get_validation_error_detail(exc): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): default_error_messages = { - 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') + 'invalid': _("Invalid data. Expected a dictionary, but got {datatype}.") } @property @@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer): many = True default_error_messages = { - 'not_a_list': _('Expected a list of items but got type `{input_type}`.') + 'not_a_list': _("Expected a list of items but got type `{input_type}`.") } def __init__(self, *args, **kwargs): -- cgit v1.2.3 From 91e316f7810157474d6246cd0024bd7f7cc31ff7 Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Wed, 7 Jan 2015 12:46:23 +0000 Subject: prefer single quotes in source and double quotes in user visible strings; add some missing full stops to user visible strings --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9d7c8884..623ed586 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -296,7 +296,7 @@ def get_validation_error_detail(exc): @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): default_error_messages = { - 'invalid': _("Invalid data. Expected a dictionary, but got {datatype}.") + 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') } @property @@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer): many = True default_error_messages = { - 'not_a_list': _("Expected a list of items but got type `{input_type}`.") + 'not_a_list': _('Expected a list of items but got type `{input_type}`.') } def __init__(self, *args, **kwargs): -- cgit v1.2.3 From 58ec7669aed9ebd58fd6095c6a6437bf9f3cf7f1 Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Wed, 7 Jan 2015 18:22:30 +0000 Subject: swap backticks for double quotes --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 623ed586..5bfbd235 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer): many = True default_error_messages = { - 'not_a_list': _('Expected a list of items but got type `{input_type}`.') + 'not_a_list': _('Expected a list of items but got type "{input_type}".') } def __init__(self, *args, **kwargs): -- cgit v1.2.3 From da6ef3d0b0f3a8e688524bbd446d4350a74fd05a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 13:03:37 +0000 Subject: Allow missing fields option for inherited serializers. Closes #2388. --- rest_framework/serializers.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e373cd10..6320a075 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -253,7 +253,7 @@ class SerializerMetaclass(type): # If this class is subclassing another Serializer, add that Serializer's # fields. Note that we loop over the bases in *reverse*. This is necessary # in order to maintain the correct order of fields. - for base in bases[::-1]: + for base in reversed(bases): if hasattr(base, '_declared_fields'): fields = list(base._declared_fields.items()) + fields @@ -880,8 +880,8 @@ class ModelSerializer(Serializer): # 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: + # Use the default set of field names if none is supplied explicitly. fields = self._get_default_field_names(declared_fields, info) exclude = getattr(self.Meta, 'exclude', None) if exclude is not None: @@ -891,6 +891,23 @@ class ModelSerializer(Serializer): field_name ) fields.remove(field_name) + else: + # Check that any fields declared on the class are + # also explicitly included in `Meta.fields`. + + # Note that we ignore any fields that were declared on a parent + # class, in order to support only including a subset of fields + # when subclassing serializers. + declared_field_names = set(declared_fields.keys()) + for cls in self.__class__.__bases__: + declared_field_names -= set(getattr(cls, '_declared_fields', [])) + + missing_fields = declared_field_names - set(fields) + assert not missing_fields, ( + 'Field `%s` has been declared on serializer `%s`, but ' + 'is missing from `Meta.fields`.' % + (list(missing_fields)[0], self.__class__.__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 @@ -1024,17 +1041,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): -- cgit v1.2.3 From 857185cf07bb539083a90bc75a6dd951da8e2206 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Jan 2015 19:29:40 +0100 Subject: Workaround Django issue 24198. --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6320a075..b1474562 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import FieldDoesNotExist, Field from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -939,6 +939,9 @@ class ModelSerializer(Serializer): except FieldDoesNotExist: continue + if not isinstance(model_field, Field): + continue + # Include each of the `unique_for_*` field names. unique_constraint_names |= set([ model_field.unique_for_date, -- cgit v1.2.3 From 15f797fd3ec61947aaecc05e6fd040e1e3e8776a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Jan 2015 19:46:31 +0100 Subject: Owned by import * --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b1474562..cf797bdc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist, Field +from django.db.models.fields import FieldDoesNotExist, Field as DjangoField from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -939,7 +939,7 @@ class ModelSerializer(Serializer): except FieldDoesNotExist: continue - if not isinstance(model_field, Field): + if not isinstance(model_field, DjangoField): continue # Include each of the `unique_for_*` field names. -- cgit v1.2.3 From 8f25c0c53c24c88afc86d99bbb3ca4edc3a4e0a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 14:56:15 +0000 Subject: Add 1.8 support --- rest_framework/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b91ecebc..d9a67441 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -1231,7 +1231,9 @@ class ModelSerializer(Serializer): continue try: - model_fields[source] = model._meta.get_field(source) + field = model._meta.get_field(source) + if isinstance(field, DjangoModelField): + model_fields[source] = field except FieldDoesNotExist: pass -- cgit v1.2.3 From e8db1834d3a3f6ba05276b64e5681288aa8f9820 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 15:24:06 +0000 Subject: Added UUIDField. --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cf797bdc..dca612ca 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -702,6 +702,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({ models.AutoField: IntegerField, models.BigIntegerField: IntegerField, @@ -724,7 +725,8 @@ class ModelSerializer(Serializer): models.SmallIntegerField: IntegerField, models.TextField: CharField, models.TimeField: TimeField, - models.URLField: URLField, + models.URLField: URLField + # Note: Some version-specific mappings also defined below. }) _related_class = PrimaryKeyRelatedField @@ -1132,6 +1134,10 @@ class ModelSerializer(Serializer): return NestedSerializer +if hasattr(models, 'UUIDField'): + ModelSerializer._field_mapping[models.UUIDField] = UUIDField + + class HyperlinkedModelSerializer(ModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead -- cgit v1.2.3 From 35f6a8246299d31ecce4f791f9527bf34cebe6e2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 16:27:23 +0000 Subject: Added DictField and support for HStoreField. --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index dca612ca..42d1e370 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,7 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoField from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import unicode_to_repr +from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, @@ -1137,6 +1137,12 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): ModelSerializer._field_mapping[models.UUIDField] = UUIDField +if postgres_fields: + class CharMappingField(DictField): + child = CharField() + + ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + class HyperlinkedModelSerializer(ModelSerializer): """ -- cgit v1.2.3 From 760b25bc20a1434cbdd69dc0b13bacdc3bbedd7c Mon Sep 17 00:00:00 2001 From: José Padilla Date: Fri, 30 Jan 2015 11:36:03 -0400 Subject: Fix AttributeError on renamed _field_mapping --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a3b8196b..a91fe23e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1330,13 +1330,13 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): - ModelSerializer._field_mapping[models.UUIDField] = UUIDField + ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField if postgres_fields: class CharMappingField(DictField): child = CharField() - ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField class HyperlinkedModelSerializer(ModelSerializer): -- cgit v1.2.3 From 37dce89354ab2c94fefeb0a20b6265fef98caddc Mon Sep 17 00:00:00 2001 From: José Padilla Date: Sun, 1 Feb 2015 15:33:34 -0400 Subject: Add support for Django 1.8’s ArrayField --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a91fe23e..520b9774 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1337,6 +1337,7 @@ if postgres_fields: child = CharField() ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField class HyperlinkedModelSerializer(ModelSerializer): -- cgit v1.2.3 From 8f1d42e7d5146e19842d2837259284f8730b451d Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 2 Feb 2015 10:50:54 +0200 Subject: Fixed typos in docstrings. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 42d1e370..2fd907ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,11 +633,11 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): If we don't do this explicitly they'd get a less helpful error when calling `.save()` on the serializer. - We don't *automatically* support these sorts of nested writes brecause + We don't *automatically* support these sorts of nested writes because there are too many ambiguities to define a default behavior. Eg. Suppose we have a `UserSerializer` with a nested profile. How should - we handle the case of an update, where the `profile` realtionship does + we handle the case of an update, where the `profile` relationship does not exist? Any of the following might be valid: * Raise an application error. -- cgit v1.2.3 From 4b65e9e42be068ad3e742692262451f8836f09d3 Mon Sep 17 00:00:00 2001 From: Jason Yan Date: Mon, 2 Feb 2015 16:14:34 -0800 Subject: Fixed missing whitespace in error string. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2fd907ec..d76658b0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -177,7 +177,7 @@ class BaseSerializer(Field): ) assert hasattr(self, 'initial_data'), ( - 'Cannot call `.is_valid()` as no `data=` keyword argument was' + 'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'passed when instantiating the serializer instance.' ) -- cgit v1.2.3 From 48fa77c09e2198c7877a724a46230caedcc7b529 Mon Sep 17 00:00:00 2001 From: José Padilla Date: Wed, 4 Feb 2015 23:33:59 -0400 Subject: Add child to ListField when using ArrayField --- rest_framework/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 520b9774..84e4961b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -986,15 +986,25 @@ class ModelSerializer(Serializer): # 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) + if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): + child_model_field = model_field.base_field.base_field + child_field_class, child_field_kwargs = self.build_standard_field( + 'child', child_model_field + ) + + field_kwargs['child'] = child_field_class(**child_field_kwargs) + return field_class, field_kwargs def build_relational_field(self, field_name, relation_info): -- cgit v1.2.3 From c696b0ba0ced9527c8f4ad1bf6f71546d8fa65c2 Mon Sep 17 00:00:00 2001 From: José Padilla Date: Thu, 5 Feb 2015 10:12:14 -0400 Subject: Fix possible nested array fields --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 84e4961b..18821958 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -998,7 +998,7 @@ class ModelSerializer(Serializer): field_kwargs.pop('allow_blank', None) if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): - child_model_field = model_field.base_field.base_field + child_model_field = model_field.base_field child_field_class, child_field_kwargs = self.build_standard_field( 'child', child_model_field ) -- cgit v1.2.3 From 1f996128458570a909d13f15c3d739fb12111984 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 13:21:35 +0000 Subject: Upgrade pending deprecations to deprecations --- rest_framework/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 18821958..18b810df 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1103,9 +1103,9 @@ class ModelSerializer(Serializer): 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. " + "The `Meta.write_only_fields` option is deprecated. " "Use `Meta.extra_kwargs={: {'write_only': True}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) for field_name in write_only_fields: @@ -1116,9 +1116,9 @@ class ModelSerializer(Serializer): view_name = getattr(self.Meta, 'view_name', None) if view_name is not None: warnings.warn( - "The `Meta.view_name` option is pending deprecation. " + "The `Meta.view_name` option is deprecated. " "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) @@ -1128,9 +1128,9 @@ class ModelSerializer(Serializer): lookup_field = getattr(self.Meta, 'lookup_field', None) if lookup_field is not None: warnings.warn( - "The `Meta.lookup_field` option is pending deprecation. " + "The `Meta.lookup_field` option is deprecated. " "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) -- cgit v1.2.3 From 670723f0216e5aea3aa133c99703949900be3d20 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 15:45:02 +0000 Subject: Minor cleanups/improvements to ModelSerializer API --- rest_framework/serializers.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 18b810df..7f3fd078 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -728,7 +728,9 @@ class ModelSerializer(Serializer): models.TimeField: TimeField, models.URLField: URLField, } - serializer_related_class = PrimaryKeyRelatedField + serializer_related_field = PrimaryKeyRelatedField + serializer_url_field = HyperlinkedIdentityField + serializer_choice_field = ChoiceField # Default `create` and `update` behavior... @@ -985,7 +987,7 @@ class ModelSerializer(Serializer): if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. - field_class = ChoiceField + field_class = self.serializer_choice_field if not issubclass(field_class, ModelField): # `model_field` is only valid for the fallback case of @@ -998,11 +1000,12 @@ class ModelSerializer(Serializer): field_kwargs.pop('allow_blank', None) if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): + # Populate the `child` argument on `ListField` instances generated + # for the PostgrSQL specfic `ArrayField`. child_model_field = model_field.base_field child_field_class, child_field_kwargs = self.build_standard_field( 'child', child_model_field ) - field_kwargs['child'] = child_field_class(**child_field_kwargs) return field_class, field_kwargs @@ -1011,7 +1014,7 @@ class ModelSerializer(Serializer): """ Create fields for forward and reverse relationships. """ - field_class = self.serializer_related_class + field_class = self.serializer_related_field field_kwargs = get_relation_kwargs(field_name, relation_info) # `view_name` is only valid for hyperlinked relationships. @@ -1047,7 +1050,7 @@ class ModelSerializer(Serializer): """ Create a field representing the object's own URL. """ - field_class = HyperlinkedIdentityField + field_class = self.serializer_url_field field_kwargs = get_url_kwargs(model_class) return field_class, field_kwargs @@ -1358,7 +1361,7 @@ class HyperlinkedModelSerializer(ModelSerializer): * A 'url' field is included instead of the 'id' field. * Relationships to other instances are hyperlinks, instead of primary keys. """ - serializer_related_class = HyperlinkedRelatedField + serializer_related_field = HyperlinkedRelatedField def get_default_field_names(self, declared_fields, model_info): """ -- cgit v1.2.3 From 0240df1a383342aa6fbb8ea3effa0482ae213d76 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 16:15:10 +0000 Subject: Minor internal API cleanpu --- rest_framework/serializers.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7f3fd078..7235d8c5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -855,8 +855,9 @@ class ModelSerializer(Serializer): ) # Include any kwargs defined in `Meta.extra_kwargs` - field_kwargs = self.build_field_kwargs( - field_kwargs, extra_kwargs, field_name + extra_field_kwargs = extra_kwargs.get(field_name, {}) + field_kwargs = self.include_extra_kwargs( + field_kwargs, extra_field_kwargs ) # Create the serializer field. @@ -1064,14 +1065,12 @@ class ModelSerializer(Serializer): (field_name, model_class.__name__) ) - def build_field_kwargs(self, kwargs, extra_kwargs, field_name): + def include_extra_kwargs(self, kwargs, extra_kwargs): """ - Include an 'extra_kwargs' that have been included for this field, + Include any '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): + if extra_kwargs.get('read_only', False): for attr in [ 'required', 'default', 'allow_blank', 'allow_null', 'min_length', 'max_length', 'min_value', 'max_value', @@ -1079,10 +1078,10 @@ class ModelSerializer(Serializer): ]: kwargs.pop(attr, None) - if extras.get('default') and kwargs.get('required') is False: + if extra_kwargs.get('default') and kwargs.get('required') is False: kwargs.pop('required') - kwargs.update(extras) + kwargs.update(extra_kwargs) return kwargs -- cgit v1.2.3 From dbd23521656b366cbaa1382a0d222f8fe4e3a326 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 10:58:00 +0000 Subject: Fixes for latest pep8 updates. Refs #2563. --- rest_framework/serializers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c60574d4..9475e119 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -336,8 +336,8 @@ class Serializer(BaseSerializer): 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 + if (field.get_value(self.initial_data) is not empty) and + not field.read_only ]) return OrderedDict([ @@ -653,8 +653,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # ... # profile = ProfileSerializer() assert not any( - isinstance(field, BaseSerializer) and (key in validated_data) - and isinstance(validated_data[key], (list, dict)) + isinstance(field, BaseSerializer) and + (key in validated_data) and + isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable nested' @@ -673,8 +674,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # ... # address = serializer.CharField('profile.address') assert not any( - '.' in field.source and (key in validated_data) - and isinstance(validated_data[key], (list, dict)) + '.' in field.source and + (key in validated_data) and + isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable dotted-source ' -- cgit v1.2.3 From 86c5fa240131fe20121db707b0324a32967987ab Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 25 Feb 2015 16:20:45 +0100 Subject: Force-evaluate querysets (see #2602) --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/serializers.py') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9475e119..2eef6eeb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,6 +13,7 @@ response content is handled by parsers and renderers. from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField +from django.db.models import query from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta @@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer): """ # Dealing with nested relationships, data can be a Manager, # so, first get a queryset from the Manager if needed - iterable = data.all() if isinstance(data, models.Manager) else data + iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data return [ self.child.to_representation(item) for item in iterable ] -- cgit v1.2.3