Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merged soc2009/model-validation to trunk. Thanks, Honza!

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12098 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 471596fc1afcb9c6258d317c619eaf5fd394e797 1 parent 4e89105
Joseph Kocherhans authored January 05, 2010

Showing 63 changed files with 1,549 additions and 638 deletions. Show diff stats Hide diff stats

  1. 1  AUTHORS
  2. 16  django/contrib/admin/options.py
  3. 16  django/contrib/auth/forms.py
  4. 24  django/contrib/contenttypes/generic.py
  5. 3  django/contrib/localflavor/ar/forms.py
  6. 5  django/contrib/localflavor/au/forms.py
  7. 3  django/contrib/localflavor/br/forms.py
  8. 5  django/contrib/localflavor/ca/forms.py
  9. 3  django/contrib/localflavor/ch/forms.py
  10. 3  django/contrib/localflavor/cl/forms.py
  11. 3  django/contrib/localflavor/cz/forms.py
  12. 3  django/contrib/localflavor/de/forms.py
  13. 3  django/contrib/localflavor/es/forms.py
  14. 3  django/contrib/localflavor/fi/forms.py
  15. 3  django/contrib/localflavor/fr/forms.py
  16. 3  django/contrib/localflavor/id/forms.py
  17. 3  django/contrib/localflavor/in_/forms.py
  18. 3  django/contrib/localflavor/is_/forms.py
  19. 3  django/contrib/localflavor/it/forms.py
  20. 4  django/contrib/localflavor/kw/forms.py
  21. 3  django/contrib/localflavor/nl/forms.py
  22. 3  django/contrib/localflavor/no/forms.py
  23. 3  django/contrib/localflavor/pe/forms.py
  24. 3  django/contrib/localflavor/pt/forms.py
  25. 2  django/contrib/localflavor/ro/forms.py
  26. 2  django/contrib/localflavor/se/forms.py
  27. 3  django/contrib/localflavor/us/forms.py
  28. 3  django/contrib/localflavor/uy/forms.py
  29. 3  django/contrib/localflavor/za/forms.py
  30. 38  django/core/exceptions.py
  31. 137  django/core/validators.py
  32. 179  django/db/models/base.py
  33. 243  django/db/models/fields/__init__.py
  34. 33  django/db/models/fields/related.py
  35. 2  django/forms/__init__.py
  36. 374  django/forms/fields.py
  37. 3  django/forms/forms.py
  38. 3  django/forms/formsets.py
  39. 231  django/forms/models.py
  40. 24  django/forms/util.py
  41. 11  docs/ref/forms/fields.txt
  42. 113  docs/ref/forms/validation.txt
  43. 22  docs/ref/models/fields.txt
  44. 25  docs/ref/models/instances.txt
  45. 83  tests/modeltests/model_forms/models.py
  46. 17  tests/modeltests/model_formsets/models.py
  47. 21  tests/modeltests/validation/__init__.py
  48. 53  tests/modeltests/validation/models.py
  49. 13  tests/modeltests/validation/test_custom_messages.py
  50. 58  tests/modeltests/validation/test_unique.py
  51. 58  tests/modeltests/validation/tests.py
  52. 18  tests/modeltests/validation/validators.py
  53. 146  tests/modeltests/validators/tests.py
  54. 24  tests/regressiontests/forms/error_messages.py
  55. 6  tests/regressiontests/forms/fields.py
  56. 8  tests/regressiontests/forms/localflavor/ar.py
  57. 10  tests/regressiontests/forms/localflavor/is_.py
  58. 1  tests/regressiontests/forms/tests.py
  59. 17  tests/regressiontests/forms/util.py
  60. 17  tests/regressiontests/forms/validators.py
  61. 10  tests/regressiontests/inline_formsets/tests.py
  62. 52  tests/regressiontests/model_fields/tests.py
  63. 2  tests/regressiontests/views/views.py
1  AUTHORS
@@ -254,6 +254,7 @@ answer newbie questions, and generally made Django that much better:
254 254
     Gasper Koren
255 255
     Martin Kosír <martin@martinkosir.net>
256 256
     Arthur Koziel <http://arthurkoziel.com>
  257
+    Honza Kral <honza.kral@gmail.com>
257 258
     Meir Kriheli <http://mksoft.co.il/>
258 259
     Bruce Kroeze <http://coderseye.com/>
259 260
     krzysiek.pawlik@silvermedia.pl
16  django/contrib/admin/options.py
@@ -578,12 +578,12 @@ def message_user(self, request, message):
578 578
         """
579 579
         messages.info(request, message)
580 580
 
581  
-    def save_form(self, request, form, change):
  581
+    def save_form(self, request, form, change, commit=False):
582 582
         """
583 583
         Given a ModelForm return an unsaved instance. ``change`` is True if
584 584
         the object is being changed, and False if it's being added.
585 585
         """
586  
-        return form.save(commit=False)
  586
+        return form.save(commit=commit)
587 587
 
588 588
     def save_model(self, request, obj, form, change):
589 589
         """
@@ -757,8 +757,12 @@ def add_view(self, request, form_url='', extra_context=None):
757 757
         if request.method == 'POST':
758 758
             form = ModelForm(request.POST, request.FILES)
759 759
             if form.is_valid():
  760
+                # Save the object, even if inline formsets haven't been
  761
+                # validated yet. We need to pass the valid model to the
  762
+                # formsets for validation. If the formsets do not validate, we
  763
+                # will delete the object.
  764
+                new_object = self.save_form(request, form, change=False, commit=True)
760 765
                 form_validated = True
761  
-                new_object = self.save_form(request, form, change=False)
762 766
             else:
763 767
                 form_validated = False
764 768
                 new_object = self.model()
@@ -774,13 +778,15 @@ def add_view(self, request, form_url='', extra_context=None):
774 778
                                   prefix=prefix, queryset=inline.queryset(request))
775 779
                 formsets.append(formset)
776 780
             if all_valid(formsets) and form_validated:
777  
-                self.save_model(request, new_object, form, change=False)
778  
-                form.save_m2m()
779 781
                 for formset in formsets:
780 782
                     self.save_formset(request, form, formset, change=False)
781 783
 
782 784
                 self.log_addition(request, new_object)
783 785
                 return self.response_add(request, new_object)
  786
+            elif form_validated:
  787
+                # The form was valid, but formsets were not, so delete the
  788
+                # object we saved above.
  789
+                new_object.delete()
784 790
         else:
785 791
             # Prepare the dict of initial data from the request.
786 792
             # We have to special-case M2Ms as a list of comma-separated PKs.
16  django/contrib/auth/forms.py
... ...
@@ -1,4 +1,4 @@
1  
-from django.contrib.auth.models import User
  1
+from django.contrib.auth.models import User, UNUSABLE_PASSWORD
2 2
 from django.contrib.auth import authenticate
3 3
 from django.contrib.auth.tokens import default_token_generator
4 4
 from django.contrib.sites.models import Site
@@ -21,6 +21,12 @@ class Meta:
21 21
         model = User
22 22
         fields = ("username",)
23 23
 
  24
+    def clean(self):
  25
+        # Fill the password field so model validation won't complain about it
  26
+        # being blank. We'll set it with the real value below.
  27
+        self.instance.password = UNUSABLE_PASSWORD
  28
+        super(UserCreationForm, self).clean()
  29
+
24 30
     def clean_username(self):
25 31
         username = self.cleaned_data["username"]
26 32
         try:
@@ -34,15 +40,9 @@ def clean_password2(self):
34 40
         password2 = self.cleaned_data["password2"]
35 41
         if password1 != password2:
36 42
             raise forms.ValidationError(_("The two password fields didn't match."))
  43
+        self.instance.set_password(password1)
37 44
         return password2
38 45
 
39  
-    def save(self, commit=True):
40  
-        user = super(UserCreationForm, self).save(commit=False)
41  
-        user.set_password(self.cleaned_data["password1"])
42  
-        if commit:
43  
-            user.save()
44  
-        return user
45  
-
46 46
 class UserChangeForm(forms.ModelForm):
47 47
     username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
48 48
         help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
24  django/contrib/contenttypes/generic.py
@@ -297,7 +297,11 @@ def __init__(self, data=None, files=None, instance=None, save_as_new=None,
297 297
         # Avoid a circular import.
298 298
         from django.contrib.contenttypes.models import ContentType
299 299
         opts = self.model._meta
300  
-        self.instance = instance
  300
+        if instance is None:
  301
+            self.instance = self.model()
  302
+        else:
  303
+            self.instance = instance
  304
+        self.save_as_new = save_as_new
301 305
         self.rel_name = '-'.join((
302 306
             opts.app_label, opts.object_name.lower(),
303 307
             self.ct_field.name, self.ct_fk_field.name,
@@ -324,15 +328,19 @@ def get_default_prefix(cls):
324 328
         ))
325 329
     get_default_prefix = classmethod(get_default_prefix)
326 330
 
327  
-    def save_new(self, form, commit=True):
  331
+    def _construct_form(self, i, **kwargs):
328 332
         # Avoid a circular import.
329 333
         from django.contrib.contenttypes.models import ContentType
330  
-        kwargs = {
331  
-            self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
332  
-            self.ct_fk_field.get_attname(): self.instance.pk,
333  
-        }
334  
-        new_obj = self.model(**kwargs)
335  
-        return save_instance(form, new_obj, commit=commit)
  334
+        form = super(BaseGenericInlineFormSet, self)._construct_form(i, **kwargs)
  335
+        if self.save_as_new:
  336
+            # Remove the key from the form's data, we are only creating new instances.
  337
+            form.data[form.add_prefix(self.ct_fk_field.name)] = None
  338
+            form.data[form.add_prefix(self.ct_field.name)] = None
  339
+
  340
+        # Set the GenericForeignKey value here so that the form can do its validation.
  341
+        setattr(form.instance, self.ct_fk_field.attname, self.instance.pk)
  342
+        setattr(form.instance, self.ct_field.attname, ContentType.objects.get_for_model(self.instance).pk)
  343
+        return form
336 344
 
337 345
 def generic_inlineformset_factory(model, form=ModelForm,
338 346
                                   formset=BaseGenericInlineFormSet,
3  django/contrib/localflavor/ar/forms.py
@@ -4,7 +4,8 @@
4 4
 """
5 5
 
6 6
 from django.forms import ValidationError
7  
-from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
  7
+from django.core.validators import EMPTY_VALUES
  8
+from django.forms.fields import RegexField, CharField, Select
8 9
 from django.utils.encoding import smart_unicode
9 10
 from django.utils.translation import ugettext_lazy as _
10 11
 
5  django/contrib/localflavor/au/forms.py
@@ -2,9 +2,10 @@
2 2
 Australian-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
7  
-from django.forms.util import smart_unicode
  7
+from django.forms.fields import Field, RegexField, Select
  8
+from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
10 11
 
3  django/contrib/localflavor/br/forms.py
@@ -3,8 +3,9 @@
3 3
 BR-specific Form helpers
4 4
 """
5 5
 
  6
+from django.core.validators import EMPTY_VALUES
6 7
 from django.forms import ValidationError
7  
-from django.forms.fields import Field, RegexField, CharField, Select, EMPTY_VALUES
  8
+from django.forms.fields import Field, RegexField, CharField, Select
8 9
 from django.utils.encoding import smart_unicode
9 10
 from django.utils.translation import ugettext_lazy as _
10 11
 import re
5  django/contrib/localflavor/ca/forms.py
@@ -2,9 +2,10 @@
2 2
 Canada-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
7  
-from django.forms.util import smart_unicode
  7
+from django.forms.fields import Field, RegexField, Select
  8
+from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
10 11
 
3  django/contrib/localflavor/ch/forms.py
@@ -2,8 +2,9 @@
2 2
 Swiss-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
3  django/contrib/localflavor/cl/forms.py
@@ -2,8 +2,9 @@
2 2
 Chile specific form helpers.
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import RegexField, Select
7 8
 from django.utils.translation import ugettext_lazy as _
8 9
 from django.utils.encoding import smart_unicode
9 10
 
3  django/contrib/localflavor/cz/forms.py
@@ -2,8 +2,9 @@
2 2
 Czech-specific form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Select, RegexField, Field, EMPTY_VALUES
  7
+from django.forms.fields import Select, RegexField, Field
7 8
 from django.utils.translation import ugettext_lazy as _
8 9
 import re
9 10
 
3  django/contrib/localflavor/de/forms.py
@@ -2,8 +2,9 @@
2 2
 DE-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.translation import ugettext_lazy as _
8 9
 import re
9 10
 
3  django/contrib/localflavor/es/forms.py
@@ -3,8 +3,9 @@
3 3
 Spanish-specific Form helpers
4 4
 """
5 5
 
  6
+from django.core.validators import EMPTY_VALUES
6 7
 from django.forms import ValidationError
7  
-from django.forms.fields import RegexField, Select, EMPTY_VALUES
  8
+from django.forms.fields import RegexField, Select
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
10 11
 
3  django/contrib/localflavor/fi/forms.py
@@ -3,8 +3,9 @@
3 3
 """
4 4
 
5 5
 import re
  6
+from django.core.validators import EMPTY_VALUES
6 7
 from django.forms import ValidationError
7  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  8
+from django.forms.fields import Field, RegexField, Select
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 
10 11
 class FIZipCodeField(RegexField):
3  django/contrib/localflavor/fr/forms.py
@@ -2,8 +2,9 @@
2 2
 FR-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
3  django/contrib/localflavor/id/forms.py
@@ -5,8 +5,9 @@
5 5
 import re
6 6
 import time
7 7
 
  8
+from django.core.validators import EMPTY_VALUES
8 9
 from django.forms import ValidationError
9  
-from django.forms.fields import Field, Select, EMPTY_VALUES
  10
+from django.forms.fields import Field, Select
10 11
 from django.utils.translation import ugettext_lazy as _
11 12
 from django.utils.encoding import smart_unicode
12 13
 
3  django/contrib/localflavor/in_/forms.py
@@ -2,8 +2,9 @@
2 2
 India-specific Form helpers.
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import gettext
9 10
 import re
3  django/contrib/localflavor/is_/forms.py
@@ -2,8 +2,9 @@
2 2
 Iceland specific form helpers.
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import RegexField, EMPTY_VALUES
  7
+from django.forms.fields import RegexField
7 8
 from django.forms.widgets import Select
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 from django.utils.encoding import smart_unicode
3  django/contrib/localflavor/it/forms.py
@@ -2,8 +2,9 @@
2 2
 IT-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.translation import ugettext_lazy as _
8 9
 from django.utils.encoding import smart_unicode
9 10
 from django.contrib.localflavor.it.util import ssn_check_digit, vat_number_check_digit
4  django/contrib/localflavor/kw/forms.py
@@ -3,8 +3,10 @@
3 3
 """
4 4
 import re
5 5
 from datetime import date
  6
+
  7
+from django.core.validators import EMPTY_VALUES
6 8
 from django.forms import ValidationError
7  
-from django.forms.fields import Field, RegexField, EMPTY_VALUES
  9
+from django.forms.fields import Field, RegexField
8 10
 from django.utils.translation import gettext as _
9 11
 
10 12
 id_re = re.compile(r'^(?P<initial>\d{1})(?P<yy>\d\d)(?P<mm>\d\d)(?P<dd>\d\d)(?P<mid>\d{4})(?P<checksum>\d{1})')
3  django/contrib/localflavor/nl/forms.py
@@ -4,8 +4,9 @@
4 4
 
5 5
 import re
6 6
 
  7
+from django.core.validators import EMPTY_VALUES
7 8
 from django.forms import ValidationError
8  
-from django.forms.fields import Field, Select, EMPTY_VALUES
  9
+from django.forms.fields import Field, Select
9 10
 from django.utils.translation import ugettext_lazy as _
10 11
 from django.utils.encoding import smart_unicode
11 12
 
3  django/contrib/localflavor/no/forms.py
@@ -3,8 +3,9 @@
3 3
 """
4 4
 
5 5
 import re, datetime
  6
+from django.core.validators import EMPTY_VALUES
6 7
 from django.forms import ValidationError
7  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  8
+from django.forms.fields import Field, RegexField, Select
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 
10 11
 class NOZipCodeField(RegexField):
3  django/contrib/localflavor/pe/forms.py
@@ -3,8 +3,9 @@
3 3
 PE-specific Form helpers.
4 4
 """
5 5
 
  6
+from django.core.validators import EMPTY_VALUES
6 7
 from django.forms import ValidationError
7  
-from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
  8
+from django.forms.fields import RegexField, CharField, Select
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 
10 11
 class PERegionSelect(Select):
3  django/contrib/localflavor/pt/forms.py
@@ -2,8 +2,9 @@
2 2
 PT-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField, Select
7 8
 from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
2  django/contrib/localflavor/ro/forms.py
@@ -5,8 +5,8 @@
5 5
 
6 6
 import re
7 7
 
  8
+from django.core.validators import EMPTY_VALUES
8 9
 from django.forms import ValidationError, Field, RegexField, Select
9  
-from django.forms.fields import EMPTY_VALUES
10 10
 from django.utils.translation import ugettext_lazy as _
11 11
 
12 12
 class ROCIFField(RegexField):
2  django/contrib/localflavor/se/forms.py
@@ -5,7 +5,7 @@
5 5
 import re
6 6
 from django import forms
7 7
 from django.utils.translation import ugettext_lazy as _
8  
-from django.forms.fields import EMPTY_VALUES
  8
+from django.core.validators import EMPTY_VALUES
9 9
 from django.contrib.localflavor.se.utils import (id_number_checksum,
10 10
     validate_id_birthday, format_personal_id_number, valid_organisation,
11 11
     format_organisation_number)
3  django/contrib/localflavor/us/forms.py
@@ -2,8 +2,9 @@
2 2
 USA-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES, CharField
  7
+from django.forms.fields import Field, RegexField, Select, CharField
7 8
 from django.utils.encoding import smart_unicode
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 import re
3  django/contrib/localflavor/uy/forms.py
@@ -4,7 +4,8 @@
4 4
 """
5 5
 import re
6 6
 
7  
-from django.forms.fields import Select, RegexField, EMPTY_VALUES
  7
+from django.core.validators import EMPTY_VALUES
  8
+from django.forms.fields import Select, RegexField
8 9
 from django.forms import ValidationError
9 10
 from django.utils.translation import ugettext_lazy as _
10 11
 from django.contrib.localflavor.uy.util import get_validation_digit
3  django/contrib/localflavor/za/forms.py
@@ -2,8 +2,9 @@
2 2
 South Africa-specific Form helpers
3 3
 """
4 4
 
  5
+from django.core.validators import EMPTY_VALUES
5 6
 from django.forms import ValidationError
6  
-from django.forms.fields import Field, RegexField, EMPTY_VALUES
  7
+from django.forms.fields import Field, RegexField
7 8
 from django.utils.checksums import luhn
8 9
 from django.utils.translation import gettext as _
9 10
 import re
38  django/core/exceptions.py
@@ -32,6 +32,42 @@ class FieldError(Exception):
32 32
     """Some kind of problem with a model field."""
33 33
     pass
34 34
 
35  
-class ValidationError(Exception):
  35
+NON_FIELD_ERRORS = '__all__'
  36
+class BaseValidationError(Exception):
36 37
     """An error while validating data."""
  38
+    def __init__(self, message, code=None, params=None):
  39
+        import operator
  40
+        from django.utils.encoding import force_unicode
  41
+        """
  42
+        ValidationError can be passed any object that can be printed (usually
  43
+        a string), a list of objects or a dictionary.
  44
+        """
  45
+        if isinstance(message, dict):
  46
+            self.message_dict = message
  47
+            # Reduce each list of messages into a single list.
  48
+            message = reduce(operator.add, message.values())
  49
+
  50
+        if isinstance(message, list):
  51
+            self.messages = [force_unicode(msg) for msg in message]
  52
+        else:
  53
+            self.code = code
  54
+            self.params = params
  55
+            message = force_unicode(message)
  56
+            self.messages = [message]
  57
+
  58
+    def __str__(self):
  59
+        # This is needed because, without a __str__(), printing an exception
  60
+        # instance would result in this:
  61
+        # AttributeError: ValidationError instance has no attribute 'args'
  62
+        # See http://www.python.org/doc/current/tut/node10.html#handling
  63
+        if hasattr(self, 'message_dict'):
  64
+            return repr(self.message_dict)
  65
+        return repr(self.messages)
  66
+
  67
+class ValidationError(BaseValidationError):
37 68
     pass
  69
+
  70
+class UnresolvableValidationError(BaseValidationError):
  71
+    """Validation error that cannot be resolved by the user."""
  72
+    pass
  73
+
137  django/core/validators.py
... ...
@@ -0,0 +1,137 @@
  1
+import re
  2
+
  3
+from django.core.exceptions import ValidationError
  4
+from django.utils.translation import ugettext_lazy as _
  5
+from django.utils.encoding import smart_unicode
  6
+
  7
+# These values, if given to validate(), will trigger the self.required check.
  8
+EMPTY_VALUES = (None, '', [], (), {})
  9
+
  10
+try:
  11
+    from django.conf import settings
  12
+    URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
  13
+except ImportError:
  14
+    # It's OK if Django settings aren't configured.
  15
+    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
  16
+
  17
+class RegexValidator(object):
  18
+    regex = ''
  19
+    message = _(u'Enter a valid value.')
  20
+    code = 'invalid'
  21
+
  22
+    def __init__(self, regex=None, message=None, code=None):
  23
+        if regex is not None:
  24
+            self.regex = regex
  25
+        if message is not None:
  26
+            self.message = message
  27
+        if code is not None:
  28
+            self.code = code
  29
+
  30
+        if isinstance(self.regex, basestring):
  31
+            self.regex = re.compile(regex)
  32
+
  33
+    def __call__(self, value):
  34
+        """
  35
+        Validates that the input matches the regular expression.
  36
+        """
  37
+        if not self.regex.search(smart_unicode(value)):
  38
+            raise ValidationError(self.message, code=self.code)
  39
+
  40
+class URLValidator(RegexValidator):
  41
+    regex = re.compile(
  42
+        r'^https?://' # http:// or https://
  43
+        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
  44
+        r'localhost|' #localhost...
  45
+        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
  46
+        r'(?::\d+)?' # optional port
  47
+        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
  48
+
  49
+    def __init__(self, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT):
  50
+        super(URLValidator, self).__init__()
  51
+        self.verify_exists = verify_exists
  52
+        self.user_agent = validator_user_agent
  53
+
  54
+    def __call__(self, value):
  55
+        super(URLValidator, self).__call__(value)
  56
+        if self.verify_exists:
  57
+            import urllib2
  58
+            headers = {
  59
+                "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
  60
+                "Accept-Language": "en-us,en;q=0.5",
  61
+                "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
  62
+                "Connection": "close",
  63
+                "User-Agent": self.user_agent,
  64
+            }
  65
+            try:
  66
+                req = urllib2.Request(value, None, headers)
  67
+                u = urllib2.urlopen(req)
  68
+            except ValueError:
  69
+                raise ValidationError(_(u'Enter a valid URL.'), code='invalid')
  70
+            except: # urllib2.URLError, httplib.InvalidURL, etc.
  71
+                raise ValidationError(_(u'This URL appears to be a broken link.'), code='invalid_link')
  72
+
  73
+
  74
+def validate_integer(value):
  75
+    try:
  76
+        int(value)
  77
+    except (ValueError, TypeError), e:
  78
+        raise ValidationError('')
  79
+
  80
+
  81
+email_re = re.compile(
  82
+    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
  83
+    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
  84
+    r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
  85
+validate_email = RegexValidator(email_re, _(u'Enter a valid e-mail address.'), 'invalid')
  86
+
  87
+slug_re = re.compile(r'^[-\w]+$')
  88
+validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid')
  89
+
  90
+ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
  91
+validate_ipv4_address = RegexValidator(ipv4_re, _(u'Enter a valid IPv4 address.'), 'invalid')
  92
+
  93
+comma_separated_int_list_re = re.compile('^[\d,]+$')
  94
+validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid')
  95
+
  96
+
  97
+class BaseValidator(object):
  98
+    compare = lambda self, a, b: a is not b
  99
+    clean   = lambda self, x: x
  100
+    message = _(u'Ensure this value is %(limit_value)s (it is %(show_value)s).')
  101
+    code = 'limit_value'
  102
+
  103
+    def __init__(self, limit_value):
  104
+        self.limit_value = limit_value
  105
+
  106
+    def __call__(self, value):
  107
+        cleaned = self.clean(value)
  108
+        params = {'limit_value': self.limit_value, 'show_value': cleaned}
  109
+        if self.compare(cleaned, self.limit_value):
  110
+            raise ValidationError(
  111
+                self.message % params,
  112
+                code=self.code,
  113
+                params=params,
  114
+            )
  115
+
  116
+class MaxValueValidator(BaseValidator):
  117
+    compare = lambda self, a, b: a > b
  118
+    message = _(u'Ensure this value is less than or equal to %(limit_value)s.')
  119
+    code = 'max_value'
  120
+
  121
+class MinValueValidator(BaseValidator):
  122
+    compare = lambda self, a, b: a < b
  123
+    message = _(u'Ensure this value is greater than or equal to %(limit_value)s.')
  124
+    code = 'min_value'
  125
+
  126
+class MinLengthValidator(BaseValidator):
  127
+    compare = lambda self, a, b: a < b
  128
+    clean   = lambda self, x: len(x)
  129
+    message = _(u'Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).')
  130
+    code = 'min_length'
  131
+
  132
+class MaxLengthValidator(BaseValidator):
  133
+    compare = lambda self, a, b: a > b
  134
+    clean   = lambda self, x: len(x)
  135
+    message = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).')
  136
+    code = 'max_length'
  137
+
179  django/db/models/base.py
@@ -3,7 +3,8 @@
3 3
 import os
4 4
 from itertools import izip
5 5
 import django.db.models.manager     # Imported to register signal handler.
6  
-from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
  6
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
  7
+from django.core import validators
7 8
 from django.db.models.fields import AutoField, FieldDoesNotExist
8 9
 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
9 10
 from django.db.models.query import delete_objects, Q
@@ -12,9 +13,11 @@
12 13
 from django.db import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS
13 14
 from django.db.models import signals
14 15
 from django.db.models.loading import register_models, get_model
  16
+from django.utils.translation import ugettext_lazy as _
15 17
 import django.utils.copycompat as copy
16 18
 from django.utils.functional import curry
17 19
 from django.utils.encoding import smart_str, force_unicode, smart_unicode
  20
+from django.utils.text import get_text_list, capfirst
18 21
 from django.conf import settings
19 22
 
20 23
 class ModelBase(type):
@@ -639,6 +642,180 @@ def _get_next_or_previous_in_order(self, is_next):
639 642
     def prepare_database_save(self, unused):
640 643
         return self.pk
641 644
 
  645
+    def validate(self):
  646
+        """
  647
+        Hook for doing any extra model-wide validation after clean() has been
  648
+        called on every field. Any ValidationError raised by this method will
  649
+        not be associated with a particular field; it will have a special-case
  650
+        association with the field defined by NON_FIELD_ERRORS.
  651
+        """
  652
+        self.validate_unique()
  653
+
  654
+    def validate_unique(self):
  655
+        unique_checks, date_checks = self._get_unique_checks()
  656
+
  657
+        errors = self._perform_unique_checks(unique_checks)
  658
+        date_errors = self._perform_date_checks(date_checks)
  659
+
  660
+        for k, v in date_errors.items():
  661
+             errors.setdefault(k, []).extend(v)
  662
+
  663
+        if errors:
  664
+            raise ValidationError(errors)
  665
+
  666
+    def _get_unique_checks(self):
  667
+        from django.db.models.fields import FieldDoesNotExist, Field as ModelField
  668
+
  669
+        unique_checks = list(self._meta.unique_together)
  670
+        # these are checks for the unique_for_<date/year/month>
  671
+        date_checks = []
  672
+
  673
+        # Gather a list of checks for fields declared as unique and add them to
  674
+        # the list of checks. Again, skip empty fields and any that did not validate.
  675
+        for f in self._meta.fields:
  676
+            name = f.name
  677
+            if f.unique:
  678
+                unique_checks.append((name,))
  679
+            if f.unique_for_date:
  680
+                date_checks.append(('date', name, f.unique_for_date))
  681
+            if f.unique_for_year:
  682
+                date_checks.append(('year', name, f.unique_for_year))
  683
+            if f.unique_for_month:
  684
+                date_checks.append(('month', name, f.unique_for_month))
  685
+        return unique_checks, date_checks
  686
+
  687
+
  688
+    def _perform_unique_checks(self, unique_checks):
  689
+        errors = {}
  690
+
  691
+        for unique_check in unique_checks:
  692
+            # Try to look up an existing object with the same values as this
  693
+            # object's values for all the unique field.
  694
+
  695
+            lookup_kwargs = {}
  696
+            for field_name in unique_check:
  697
+                f = self._meta.get_field(field_name)
  698
+                lookup_value = getattr(self, f.attname)
  699
+                if f.null and lookup_value is None:
  700
+                    # no value, skip the lookup
  701
+                    continue
  702
+                if f.primary_key and not getattr(self, '_adding', False):
  703
+                    # no need to check for unique primary key when editting 
  704
+                    continue
  705
+                lookup_kwargs[str(field_name)] = lookup_value
  706
+
  707
+            # some fields were skipped, no reason to do the check
  708
+            if len(unique_check) != len(lookup_kwargs.keys()):
  709
+                continue
  710
+
  711
+            qs = self.__class__._default_manager.filter(**lookup_kwargs)
  712
+
  713
+            # Exclude the current object from the query if we are editing an
  714
+            # instance (as opposed to creating a new one)
  715
+            if not getattr(self, '_adding', False) and self.pk is not None:
  716
+                qs = qs.exclude(pk=self.pk)
  717
+
  718
+            # This cute trick with extra/values is the most efficient way to
  719
+            # tell if a particular query returns any results.
  720
+            if qs.extra(select={'a': 1}).values('a').order_by():
  721
+                if len(unique_check) == 1:
  722
+                    key = unique_check[0]
  723
+                else:
  724
+                    key = NON_FIELD_ERRORS
  725
+                errors.setdefault(key, []).append(self.unique_error_message(unique_check))
  726
+
  727
+        return errors
  728
+
  729
+    def _perform_date_checks(self, date_checks):
  730
+        errors = {}
  731
+        for lookup_type, field, unique_for in date_checks:
  732
+            lookup_kwargs = {}
  733
+            # there's a ticket to add a date lookup, we can remove this special
  734
+            # case if that makes it's way in
  735
+            date = getattr(self, unique_for)
  736
+            if lookup_type == 'date':
  737
+                lookup_kwargs['%s__day' % unique_for] = date.day
  738
+                lookup_kwargs['%s__month' % unique_for] = date.month
  739
+                lookup_kwargs['%s__year' % unique_for] = date.year
  740
+            else:
  741
+                lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(date, lookup_type)
  742
+            lookup_kwargs[field] = getattr(self, field)
  743
+
  744
+            qs = self.__class__._default_manager.filter(**lookup_kwargs)
  745
+            # Exclude the current object from the query if we are editing an
  746
+            # instance (as opposed to creating a new one)
  747
+            if not getattr(self, '_adding', False) and self.pk is not None:
  748
+                qs = qs.exclude(pk=self.pk)
  749
+
  750
+            # This cute trick with extra/values is the most efficient way to
  751
+            # tell if a particular query returns any results.
  752
+            if qs.extra(select={'a': 1}).values('a').order_by():
  753
+                errors.setdefault(field, []).append(
  754
+                    self.date_error_message(lookup_type, field, unique_for)
  755
+                )
  756
+        return errors
  757
+
  758
+    def date_error_message(self, lookup_type, field, unique_for):
  759
+        opts = self._meta
  760
+        return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
  761
+            'field_name': unicode(capfirst(opts.get_field(field).verbose_name)),
  762
+            'date_field': unicode(capfirst(opts.get_field(unique_for).verbose_name)),
  763
+            'lookup': lookup_type,
  764
+        }
  765
+
  766
+    def unique_error_message(self, unique_check):
  767
+        opts = self._meta
  768
+        model_name = capfirst(opts.verbose_name)
  769
+
  770
+        # A unique field
  771
+        if len(unique_check) == 1:
  772
+            field_name = unique_check[0]
  773
+            field_label = capfirst(opts.get_field(field_name).verbose_name)
  774
+            # Insert the error into the error dict, very sneaky
  775
+            return _(u"%(model_name)s with this %(field_label)s already exists.") %  {
  776
+                'model_name': unicode(model_name),
  777
+                'field_label': unicode(field_label)
  778
+            }
  779
+        # unique_together
  780
+        else:
  781
+            field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check)
  782
+            field_labels = get_text_list(field_labels, _('and'))
  783
+            return _(u"%(model_name)s with this %(field_label)s already exists.") %  {
  784
+                'model_name': unicode(model_name),
  785
+                'field_label': unicode(field_labels)
  786
+            }
  787
+
  788
+    def full_validate(self, exclude=[]):
  789
+        """
  790
+        Cleans all fields and raises ValidationError containing message_dict
  791
+        of all validation errors if any occur.
  792
+        """
  793
+        errors = {}
  794
+        for f in self._meta.fields:
  795
+            if f.name in exclude:
  796
+                continue
  797
+            try:
  798
+                setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
  799
+            except ValidationError, e:
  800
+                errors[f.name] = e.messages
  801
+
  802
+        # Form.clean() is run even if other validation fails, so do the
  803
+        # same with Model.validate() for consistency.
  804
+        try:
  805
+            self.validate()
  806
+        except ValidationError, e:
  807
+            if hasattr(e, 'message_dict'):
  808
+                if errors:
  809
+                    for k, v in e.message_dict.items():
  810
+                        errors.set_default(k, []).extend(v)
  811
+                else:
  812
+                    errors = e.message_dict
  813
+            else:
  814
+                errors[NON_FIELD_ERRORS] = e.messages
  815
+
  816
+        if errors:
  817
+            raise ValidationError(errors)
  818
+
642 819
 
643 820
 ############################################
644 821
 # HELPER FUNCTIONS (CURRIED MODEL METHODS) #
243  django/db/models/fields/__init__.py
@@ -13,12 +13,12 @@
13 13
 from django.dispatch import dispatcher
14 14
 from django.conf import settings
15 15
 from django import forms
16  
-from django.core import exceptions
  16
+from django.core import exceptions, validators
17 17
 from django.utils.datastructures import DictWrapper
18 18
 from django.utils.functional import curry
19 19
 from django.utils.itercompat import tee
20 20
 from django.utils.text import capfirst
21  
-from django.utils.translation import ugettext_lazy, ugettext as _
  21
+from django.utils.translation import ugettext_lazy as _, ugettext
22 22
 from django.utils.encoding import smart_unicode, force_unicode, smart_str
23 23
 from django.utils import datetime_safe
24 24
 
@@ -60,6 +60,12 @@ class Field(object):
60 60
     # creates, creation_counter is used for all user-specified fields.
61 61
     creation_counter = 0
62 62
     auto_creation_counter = -1
  63
+    default_validators = [] # Default set of validators
  64
+    default_error_messages = {
  65
+        'invalid_choice': _(u'Value %r is not a valid choice.'),
  66
+        'null': _(u'This field cannot be null.'),
  67
+        'blank': _(u'This field cannot be blank.'),
  68
+    }
63 69
 
64 70
     # Generic field type description, usually overriden by subclasses
65 71
     def _description(self):
@@ -73,7 +79,8 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
73 79
             db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
74 80
             serialize=True, unique_for_date=None, unique_for_month=None,
75 81
             unique_for_year=None, choices=None, help_text='', db_column=None,
76  
-            db_tablespace=None, auto_created=False):
  82
+            db_tablespace=None, auto_created=False, validators=[],
  83
+            error_messages=None):
77 84
         self.name = name
78 85
         self.verbose_name = verbose_name
79 86
         self.primary_key = primary_key
@@ -106,6 +113,42 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
106 113
             self.creation_counter = Field.creation_counter
107 114
             Field.creation_counter += 1
108 115
 
  116
+        self.validators = self.default_validators + validators
  117
+
  118
+        messages = {}
  119
+        for c in reversed(self.__class__.__mro__):
  120
+            messages.update(getattr(c, 'default_error_messages', {}))
  121
+        messages.update(error_messages or {})
  122
+        self.error_messages = messages
  123
+
  124
+    def __getstate__(self):
  125
+        """
  126
+        Pickling support.
  127
+        """
  128
+        from django.utils.functional import Promise
  129
+        obj_dict = self.__dict__.copy()
  130
+        items = []
  131
+        translated_keys = []
  132
+        for k, v in self.error_messages.items():
  133
+            if isinstance(v, Promise):
  134
+                args = getattr(v, '_proxy____args', None)
  135
+                if args:
  136
+                    translated_keys.append(k)
  137
+                    v = args[0]
  138
+            items.append((k,v))
  139
+        obj_dict['_translated_keys'] = translated_keys
  140
+        obj_dict['error_messages'] = dict(items)
  141
+        return obj_dict
  142
+
  143
+    def __setstate__(self, obj_dict):
  144
+        """
  145
+        Unpickling support.
  146
+        """
  147
+        translated_keys = obj_dict.pop('_translated_keys')
  148
+        self.__dict__.update(obj_dict)
  149
+        for k in translated_keys:
  150
+            self.error_messages[k] = _(self.error_messages[k])
  151
+
109 152
     def __cmp__(self, other):
110 153
         # This is needed because bisect does not take a comparison function.
111 154
         return cmp(self.creation_counter, other.creation_counter)
@@ -127,6 +170,54 @@ def to_python(self, value):
127 170
         """
128 171
         return value
129 172
 
  173
+    def run_validators(self, value):
  174
+        if value in validators.EMPTY_VALUES:
  175
+            return
  176
+
  177
+        errors = []
  178
+        for v in self.validators:
  179
+            try:
  180
+                v(value)
  181
+            except exceptions.ValidationError, e:
  182
+                if hasattr(e, 'code') and e.code in self.error_messages:
  183
+                    message = self.error_messages[e.code]
  184
+                    if e.params:
  185
+                        message = message % e.params
  186
+                    errors.append(message)
  187
+                else:
  188
+                    errors.extend(e.messages)
  189
+        if errors:
  190
+            raise exceptions.ValidationError(errors)
  191
+
  192
+    def validate(self, value, model_instance):
  193
+        """
  194
+        Validates value and throws ValidationError. Subclasses should override
  195
+        this to provide validation logic.
  196
+        """
  197
+        if not self.editable:
  198
+            # Skip validation for non-editable fields.
  199
+            return
  200
+        if self._choices and value:
  201
+            if not value in dict(self.choices):
  202
+                raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
  203
+
  204
+        if value is None and not self.null:
  205
+            raise exceptions.ValidationError(self.error_messages['null'])
  206
+
  207
+        if not self.blank and value in validators.EMPTY_VALUES:
  208
+            raise exceptions.ValidationError(self.error_messages['blank'])
  209
+
  210
+    def clean(self, value, model_instance):
  211
+        """
  212
+        Convert the value's type and run validation. Validation errors from to_python
  213
+        and validate are propagated. The correct value is returned if no error is
  214
+        raised.
  215
+        """
  216
+        value = self.to_python(value)
  217
+        self.validate(value, model_instance)
  218
+        self.run_validators(value)
  219
+        return value
  220
+
130 221
     def db_type(self, connection):
131 222
         """
132 223
         Returns the database column data type for this field, for the provided
@@ -377,9 +468,12 @@ def value_from_object(self, obj):
377 468
         return getattr(obj, self.attname)
378 469
 
379 470
 class AutoField(Field):
380  
-    description = ugettext_lazy("Integer")
  471
+    description = _("Integer")
381 472
 
382 473
     empty_strings_allowed = False
  474
+    default_error_messages = {
  475
+        'invalid': _(u'This value must be an integer.'),
  476
+    }
383 477
     def __init__(self, *args, **kwargs):
384 478
         assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
385 479
         kwargs['blank'] = True
@@ -391,8 +485,10 @@ def to_python(self, value):
391 485
         try:
392 486
             return int(value)
393 487
         except (TypeError, ValueError):
394  
-            raise exceptions.ValidationError(
395  
-                _("This value must be an integer."))
  488
+            raise exceptions.ValidationError(self.error_messages['invalid'])
  489
+
  490
+    def validate(self, value, model_instance):
  491
+        pass
396 492
 
397 493
     def get_prep_value(self, value):
398 494
         if value is None:
@@ -410,7 +506,10 @@ def formfield(self, **kwargs):
410 506
 
411 507
 class BooleanField(Field):
412 508
     empty_strings_allowed = False
413  
-    description = ugettext_lazy("Boolean (Either True or False)")
  509
+    default_error_messages = {
  510
+        'invalid': _(u'This value must be either True or False.'),
  511
+    }
  512
+    description = _("Boolean (Either True or False)")
414 513
     def __init__(self, *args, **kwargs):
415 514
         kwargs['blank'] = True
416 515
         if 'default' not in kwargs and not kwargs.get('null'):
@@ -424,8 +523,7 @@ def to_python(self, value):
424 523
         if value in (True, False): return value
425 524
         if value in ('t', 'True', '1'): return True
426 525
         if value in ('f', 'False', '0'): return False
427  
-        raise exceptions.ValidationError(
428  
-            _("This value must be either True or False."))
  526
+        raise exceptions.ValidationError(self.error_messages['invalid'])
429 527
 
430 528
     def get_prep_lookup(self, lookup_type, value):
431 529
         # Special-case handling for filters coming from a web request (e.g. the
@@ -453,36 +551,35 @@ def formfield(self, **kwargs):
453 551
         return super(BooleanField, self).formfield(**defaults)
454 552
 
455 553
 class CharField(Field):
456  
-    description = ugettext_lazy("String (up to %(max_length)s)")
  554
+    description = _("String (up to %(max_length)s)")
  555
+
  556
+    def __init__(self, *args, **kwargs):
  557
+        super(CharField, self).__init__(*args, **kwargs)
  558
+        self.validators.append(validators.MaxLengthValidator(self.max_length))
457 559
 
458 560
     def get_internal_type(self):
459 561
         return "CharField"
460 562
 
461 563
     def to_python(self, value):
462  
-        if isinstance(value, basestring):
  564
+        if isinstance(value, basestring) or value is None:
463 565
             return value
464