Repository: encode/django-rest-framework

Issue:
ModelSerializers should have ._post_clean()-like behavior
Currently, the coupling between ModelFields and serializer Fields is somewhat brittle. 

As @tomchristie suggested, we should actually be mimicking the [post_clean()](https://github.com/django/django/blob/master/django/forms/models.py#L307) behavior from Django's ModelForms. 
That way there would be no need to couple every possible ModelField to a serializer Field because all the necessary validation logic is already present on the model.

[This bit](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L434) where max_length is added to the kwargs could then also be dropped.



Discussion:

Resolving patch:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -118,6 +118,17 @@ def get_default_fields(self):
         """
         return {}
 
+    def get_excluded_fieldnames(self):
+        """
+        Returns the fieldnames that should not be validated.
+        """
+        excluded_fields = list(self.opts.exclude)
+        for field in self.fields.keys() + self.get_default_fields().keys():
+            if self.opts.fields:
+                if field not in self.opts.fields + self.opts.exclude:
+                    excluded_fields.append(field)
+        return excluded_fields
+
     def get_fields(self):
         """
         Returns the complete set of fields for the object as a dict.
@@ -217,10 +228,17 @@ def perform_validation(self, attrs):
             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
+        # 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
 
@@ -432,10 +450,6 @@ def get_field(self, model_field):
             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,
@@ -459,6 +473,16 @@ def get_field(self, model_field):
         except KeyError:
             return ModelField(model_field=model_field, **kwargs)
 
+    def validate(self, attrs):
+        copied_attrs = copy.deepcopy(attrs)
+        restored_object = self.restore_object(copied_attrs, instance=getattr(self, 'object', None))
+        self.perform_model_validation(restored_object)
+        return attrs
+
+    def perform_model_validation(self, restored_object):
+        # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique()
+        restored_object.full_clean(exclude=list(self.get_excluded_fieldnames()))
+
     def restore_object(self, attrs, instance=None):
         """
         Restore the model instance.


Test patch:
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -61,7 +61,7 @@ class BasicModel(RESTFrameworkModel):
 
 class SlugBasedModel(RESTFrameworkModel):
     text = models.CharField(max_length=100)
-    slug = models.SlugField(max_length=32)
+    slug = models.SlugField(max_length=32, blank=True)
 
 
 class DefaultValueModel(RESTFrameworkModel):
@@ -160,7 +160,7 @@ class Photo(RESTFrameworkModel):
 
 # Model for issue #324
 class BlankFieldModel(RESTFrameworkModel):
-    title = models.CharField(max_length=100, blank=True)
+    title = models.CharField(max_length=100, blank=True, null=True)
 
 
 # Model for issue #380
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,7 +1,7 @@
 import datetime
 from django.test import TestCase
 from rest_framework import serializers
-from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
+from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel,
     BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
     ManyToManyModel, Person, ReadOnlyManyToManyModel)
 
@@ -48,7 +48,7 @@ class Meta:
 
 
 class ActionItemSerializer(serializers.ModelSerializer):
-    
+
     class Meta:
         model = ActionItem
 
@@ -62,6 +62,12 @@ class Meta:
         read_only_fields = ('age',)
 
 
+class AlbumsSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = Album
+
+
 class BasicTests(TestCase):
     def setUp(self):
         self.comment = Comment(
@@ -169,7 +175,7 @@ def setUp(self):
             'content': 'x' * 1001,
             'created': datetime.datetime(2012, 1, 1)
         }
-        self.actionitem = ActionItem('Some to do item',
+        self.actionitem = ActionItem(title='Some to do item',
         )
 
     def test_create(self):
@@ -276,6 +282,17 @@ def test_default_modelfield_max_length_exceeded(self):
         self.assertEquals(serializer.is_valid(), False)
         self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
 
+    def test_validate_unique(self):
+        """
+        Just check if serializers.ModelSerializer.perform_model_validation() handles unique checks via .full_clean()
+        """
+        serializer = AlbumsSerializer(data={'title': 'a'})
+        serializer.is_valid()
+        serializer.save()
+        second_serializer = AlbumsSerializer(data={'title': 'a'})
+        self.assertFalse(second_serializer.is_valid())
+        self.assertEqual(second_serializer.errors,  {'title': [u'Album with this Title already exists.']})
+
 
 class RegexValidationTest(TestCase):
     def test_create_failed(self):