aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/serializers.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/serializers.py')
-rw-r--r--rest_framework/serializers.py123
1 files changed, 92 insertions, 31 deletions
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index cbb6b4df..91fbe176 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -16,7 +16,7 @@ from rest_framework.compat import get_concrete_model
# This helps keep the seperation between model fields, form fields, and
# serializer fields more explicit.
-
+from rest_framework.relations import *
from rest_framework.fields import *
@@ -24,7 +24,16 @@ class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
- pass
+ def __getstate__(self):
+ """
+ Used by pickle (e.g., caching).
+ Overriden to remove metadata from the dict, since it shouldn't be pickled
+ and may in some instances be unpickleable.
+ """
+ # return an instance of the first dict in MRO that isn't a DictWithMetadata
+ for base in self.__class__.__mro__:
+ if not isinstance(base, DictWithMetadata) and isinstance(base, dict):
+ return base(self)
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
@@ -93,7 +102,8 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
- def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
+ def __init__(self, instance=None, data=None, files=None,
+ context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None
@@ -133,8 +143,6 @@ class BaseSerializer(Field):
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
- # Set up the field
- field.initialize(parent=self, field_name=key)
# Add in the default fields
default_fields = self.get_default_fields()
@@ -154,6 +162,9 @@ class BaseSerializer(Field):
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
#####
@@ -186,6 +197,7 @@ class BaseSerializer(Field):
ret.fields = {}
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)
ret[key] = value
@@ -199,6 +211,7 @@ class BaseSerializer(Field):
"""
reverted_data = {}
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:
@@ -219,10 +232,18 @@ class BaseSerializer(Field):
except ValidationError as err:
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
- try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- self._errors['non_field_errors'] = 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
@@ -274,15 +295,23 @@ class BaseSerializer(Field):
Override default so that we can apply ModelSerializer as a nested
field to relationships.
"""
- obj = getattr(obj, self.source or field_name)
-
- if is_simple_callable(obj):
- obj = obj()
+ if self.source:
+ for component in self.source.split('.'):
+ obj = getattr(obj, component)
+ if is_simple_callable(obj):
+ obj = obj()
+ else:
+ obj = getattr(obj, field_name)
+ if is_simple_callable(obj):
+ obj = value()
# If the object has an "all" method, assume it's a relationship
if is_simple_callable(getattr(obj, 'all', None)):
return [self.to_native(item) for item in obj.all()]
+ if obj is None:
+ return None
+
return self.to_native(obj)
@property
@@ -366,7 +395,6 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
- field.initialize(parent=self, field_name=model_field.name)
ret[model_field.name] = field
for field_name in self.opts.read_only_fields:
@@ -398,10 +426,14 @@ class ModelSerializer(Serializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
- queryset = model_field.rel.to._default_manager
+ kwargs = {
+ 'null': model_field.null,
+ 'queryset': model_field.rel.to._default_manager
+ }
+
if to_many:
- return ManyPrimaryKeyRelatedField(queryset=queryset)
- return PrimaryKeyRelatedField(queryset=queryset)
+ return ManyPrimaryKeyRelatedField(**kwargs)
+ return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
@@ -411,7 +443,7 @@ class ModelSerializer(Serializer):
kwargs['blank'] = model_field.blank
- if model_field.null:
+ if model_field.null or model_field.blank:
kwargs['required'] = False
if model_field.has_default():
@@ -426,10 +458,6 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
- max_length = getattr(model_field, 'max_length', None)
- if max_length:
- kwargs['max_length'] = max_length
-
field_mapping = {
models.FloatField: FloatField,
models.IntegerField: IntegerField,
@@ -453,28 +481,56 @@ class ModelSerializer(Serializer):
except KeyError:
return ModelField(model_field=model_field, **kwargs)
+ def get_validation_exclusions(self):
+ """
+ Return a list of field names to exclude from model validation.
+ """
+ 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():
+ if field_name in exclusions and not field.read_only:
+ exclusions.remove(field_name)
+ return exclusions
+
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
"""
self.m2m_data = {}
+ self.related_data = {}
- if instance is not None:
- for key, val in attrs.items():
- setattr(instance, key, val)
- return instance
+ # Reverse fk relations
+ for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
+ field_name = obj.field.related_query_name()
+ if field_name in attrs:
+ self.related_data[field_name] = attrs.pop(field_name)
- # Reverse relations
+ # Reverse m2m relations
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.m2m_data[field_name] = attrs.pop(field_name)
- # Forward relations
+ # Forward m2m relations
for field in self.opts.model._meta.many_to_many:
if field.name in attrs:
self.m2m_data[field.name] = attrs.pop(field.name)
- return self.opts.model(**attrs)
+
+ if instance is not None:
+ for key, val in attrs.items():
+ setattr(instance, key, val)
+
+ else:
+ instance = self.opts.model(**attrs)
+
+ try:
+ instance.full_clean(exclude=self.get_validation_exclusions())
+ except ValidationError, err:
+ self._errors = err.message_dict
+ return None
+
+ return instance
def save(self, save_m2m=True):
"""
@@ -487,6 +543,11 @@ class ModelSerializer(Serializer):
setattr(self.object, accessor_name, object_list)
self.m2m_data = {}
+ if getattr(self, 'related_data', None):
+ for accessor_name, object_list in self.related_data.items():
+ setattr(self.object, accessor_name, object_list)
+ self.related_data = {}
+
return self.object
@@ -533,9 +594,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
- queryset = rel._default_manager
kwargs = {
- 'queryset': queryset,
+ 'null': model_field.null,
+ 'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
if to_many: