summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/options.py16
-rw-r--r--django/contrib/auth/forms.py16
-rw-r--r--django/contrib/contenttypes/generic.py24
-rw-r--r--django/contrib/localflavor/ar/forms.py3
-rw-r--r--django/contrib/localflavor/au/forms.py5
-rw-r--r--django/contrib/localflavor/br/forms.py3
-rw-r--r--django/contrib/localflavor/ca/forms.py5
-rw-r--r--django/contrib/localflavor/ch/forms.py3
-rw-r--r--django/contrib/localflavor/cl/forms.py3
-rw-r--r--django/contrib/localflavor/cz/forms.py3
-rw-r--r--django/contrib/localflavor/de/forms.py3
-rw-r--r--django/contrib/localflavor/es/forms.py3
-rw-r--r--django/contrib/localflavor/fi/forms.py3
-rw-r--r--django/contrib/localflavor/fr/forms.py3
-rw-r--r--django/contrib/localflavor/id/forms.py3
-rw-r--r--django/contrib/localflavor/in_/forms.py3
-rw-r--r--django/contrib/localflavor/is_/forms.py3
-rw-r--r--django/contrib/localflavor/it/forms.py3
-rw-r--r--django/contrib/localflavor/kw/forms.py4
-rw-r--r--django/contrib/localflavor/nl/forms.py3
-rw-r--r--django/contrib/localflavor/no/forms.py3
-rw-r--r--django/contrib/localflavor/pe/forms.py3
-rw-r--r--django/contrib/localflavor/pt/forms.py3
-rw-r--r--django/contrib/localflavor/ro/forms.py2
-rw-r--r--django/contrib/localflavor/se/forms.py2
-rw-r--r--django/contrib/localflavor/us/forms.py3
-rw-r--r--django/contrib/localflavor/uy/forms.py3
-rw-r--r--django/contrib/localflavor/za/forms.py3
-rw-r--r--django/core/exceptions.py38
-rw-r--r--django/core/validators.py137
-rw-r--r--django/db/models/base.py179
-rw-r--r--django/db/models/fields/__init__.py243
-rw-r--r--django/db/models/fields/related.py33
-rw-r--r--django/forms/__init__.py2
-rw-r--r--django/forms/fields.py370
-rw-r--r--django/forms/forms.py3
-rw-r--r--django/forms/formsets.py3
-rw-r--r--django/forms/models.py231
-rw-r--r--django/forms/util.py24
39 files changed, 875 insertions, 522 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 5c2a7c17f4..c18af6260a 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -578,12 +578,12 @@ class ModelAdmin(BaseModelAdmin):
"""
messages.info(request, message)
- def save_form(self, request, form, change):
+ def save_form(self, request, form, change, commit=False):
"""
Given a ModelForm return an unsaved instance. ``change`` is True if
the object is being changed, and False if it's being added.
"""
- return form.save(commit=False)
+ return form.save(commit=commit)
def save_model(self, request, obj, form, change):
"""
@@ -757,8 +757,12 @@ class ModelAdmin(BaseModelAdmin):
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
if form.is_valid():
+ # Save the object, even if inline formsets haven't been
+ # validated yet. We need to pass the valid model to the
+ # formsets for validation. If the formsets do not validate, we
+ # will delete the object.
+ new_object = self.save_form(request, form, change=False, commit=True)
form_validated = True
- new_object = self.save_form(request, form, change=False)
else:
form_validated = False
new_object = self.model()
@@ -774,13 +778,15 @@ class ModelAdmin(BaseModelAdmin):
prefix=prefix, queryset=inline.queryset(request))
formsets.append(formset)
if all_valid(formsets) and form_validated:
- self.save_model(request, new_object, form, change=False)
- form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=False)
self.log_addition(request, new_object)
return self.response_add(request, new_object)
+ elif form_validated:
+ # The form was valid, but formsets were not, so delete the
+ # object we saved above.
+ new_object.delete()
else:
# Prepare the dict of initial data from the request.
# We have to special-case M2Ms as a list of comma-separated PKs.
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 55e770e553..dbc55ca0f9 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,4 +1,4 @@
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, UNUSABLE_PASSWORD
from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
@@ -21,6 +21,12 @@ class UserCreationForm(forms.ModelForm):
model = User
fields = ("username",)
+ def clean(self):
+ # Fill the password field so model validation won't complain about it
+ # being blank. We'll set it with the real value below.
+ self.instance.password = UNUSABLE_PASSWORD
+ super(UserCreationForm, self).clean()
+
def clean_username(self):
username = self.cleaned_data["username"]
try:
@@ -34,15 +40,9 @@ class UserCreationForm(forms.ModelForm):
password2 = self.cleaned_data["password2"]
if password1 != password2:
raise forms.ValidationError(_("The two password fields didn't match."))
+ self.instance.set_password(password1)
return password2
- def save(self, commit=True):
- user = super(UserCreationForm, self).save(commit=False)
- user.set_password(self.cleaned_data["password1"])
- if commit:
- user.save()
- return user
-
class UserChangeForm(forms.ModelForm):
username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index 66fa9e6013..8a3ddfb52a 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -297,7 +297,11 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
# Avoid a circular import.
from django.contrib.contenttypes.models import ContentType
opts = self.model._meta
- self.instance = instance
+ if instance is None:
+ self.instance = self.model()
+ else:
+ self.instance = instance
+ self.save_as_new = save_as_new
self.rel_name = '-'.join((
opts.app_label, opts.object_name.lower(),
self.ct_field.name, self.ct_fk_field.name,
@@ -324,15 +328,19 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
))
get_default_prefix = classmethod(get_default_prefix)
- def save_new(self, form, commit=True):
+ def _construct_form(self, i, **kwargs):
# Avoid a circular import.
from django.contrib.contenttypes.models import ContentType
- kwargs = {
- self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
- self.ct_fk_field.get_attname(): self.instance.pk,
- }
- new_obj = self.model(**kwargs)
- return save_instance(form, new_obj, commit=commit)
+ form = super(BaseGenericInlineFormSet, self)._construct_form(i, **kwargs)
+ if self.save_as_new:
+ # Remove the key from the form's data, we are only creating new instances.
+ form.data[form.add_prefix(self.ct_fk_field.name)] = None
+ form.data[form.add_prefix(self.ct_field.name)] = None
+
+ # Set the GenericForeignKey value here so that the form can do its validation.
+ setattr(form.instance, self.ct_fk_field.attname, self.instance.pk)
+ setattr(form.instance, self.ct_field.attname, ContentType.objects.get_for_model(self.instance).pk)
+ return form
def generic_inlineformset_factory(model, form=ModelForm,
formset=BaseGenericInlineFormSet,
diff --git a/django/contrib/localflavor/ar/forms.py b/django/contrib/localflavor/ar/forms.py
index 8e8e1af387..53721a17b6 100644
--- a/django/contrib/localflavor/ar/forms.py
+++ b/django/contrib/localflavor/ar/forms.py
@@ -4,7 +4,8 @@ AR-specific Form helpers.
"""
from django.forms import ValidationError
-from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
+from django.core.validators import EMPTY_VALUES
+from django.forms.fields import RegexField, CharField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
diff --git a/django/contrib/localflavor/au/forms.py b/django/contrib/localflavor/au/forms.py
index afc3a0cc4c..87e04387f5 100644
--- a/django/contrib/localflavor/au/forms.py
+++ b/django/contrib/localflavor/au/forms.py
@@ -2,9 +2,10 @@
Australian-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
-from django.forms.util import smart_unicode
+from django.forms.fields import Field, RegexField, Select
+from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/br/forms.py b/django/contrib/localflavor/br/forms.py
index 6d0a0384c6..9f482cd01f 100644
--- a/django/contrib/localflavor/br/forms.py
+++ b/django/contrib/localflavor/br/forms.py
@@ -3,8 +3,9 @@
BR-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, CharField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, CharField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/ca/forms.py b/django/contrib/localflavor/ca/forms.py
index 542ff40670..ae3c76e24a 100644
--- a/django/contrib/localflavor/ca/forms.py
+++ b/django/contrib/localflavor/ca/forms.py
@@ -2,9 +2,10 @@
Canada-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
-from django.forms.util import smart_unicode
+from django.forms.fields import Field, RegexField, Select
+from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/ch/forms.py b/django/contrib/localflavor/ch/forms.py
index bd92fcae98..eb1ae68844 100644
--- a/django/contrib/localflavor/ch/forms.py
+++ b/django/contrib/localflavor/ch/forms.py
@@ -2,8 +2,9 @@
Swiss-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/cl/forms.py b/django/contrib/localflavor/cl/forms.py
index 61b3ab7aac..48219e88af 100644
--- a/django/contrib/localflavor/cl/forms.py
+++ b/django/contrib/localflavor/cl/forms.py
@@ -2,8 +2,9 @@
Chile specific form helpers.
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import RegexField, Select, EMPTY_VALUES
+from django.forms.fields import RegexField, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
diff --git a/django/contrib/localflavor/cz/forms.py b/django/contrib/localflavor/cz/forms.py
index 6c7a5bf5dd..e980569c70 100644
--- a/django/contrib/localflavor/cz/forms.py
+++ b/django/contrib/localflavor/cz/forms.py
@@ -2,8 +2,9 @@
Czech-specific form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Select, RegexField, Field, EMPTY_VALUES
+from django.forms.fields import Select, RegexField, Field
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/de/forms.py b/django/contrib/localflavor/de/forms.py
index 7a1b7c03c8..a785a7194e 100644
--- a/django/contrib/localflavor/de/forms.py
+++ b/django/contrib/localflavor/de/forms.py
@@ -2,8 +2,9 @@
DE-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/es/forms.py b/django/contrib/localflavor/es/forms.py
index a033f3e46b..8c4a19e614 100644
--- a/django/contrib/localflavor/es/forms.py
+++ b/django/contrib/localflavor/es/forms.py
@@ -3,8 +3,9 @@
Spanish-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import RegexField, Select, EMPTY_VALUES
+from django.forms.fields import RegexField, Select
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/fi/forms.py b/django/contrib/localflavor/fi/forms.py
index 2b82a796fe..9c163c9143 100644
--- a/django/contrib/localflavor/fi/forms.py
+++ b/django/contrib/localflavor/fi/forms.py
@@ -3,8 +3,9 @@ FI-specific Form helpers
"""
import re
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
class FIZipCodeField(RegexField):
diff --git a/django/contrib/localflavor/fr/forms.py b/django/contrib/localflavor/fr/forms.py
index 4cd84102a8..963eadc86a 100644
--- a/django/contrib/localflavor/fr/forms.py
+++ b/django/contrib/localflavor/fr/forms.py
@@ -2,8 +2,9 @@
FR-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/id/forms.py b/django/contrib/localflavor/id/forms.py
index 0d68fa32d5..834e588749 100644
--- a/django/contrib/localflavor/id/forms.py
+++ b/django/contrib/localflavor/id/forms.py
@@ -5,8 +5,9 @@ ID-specific Form helpers
import re
import time
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, Select, EMPTY_VALUES
+from django.forms.fields import Field, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
diff --git a/django/contrib/localflavor/in_/forms.py b/django/contrib/localflavor/in_/forms.py
index 270b0a09b1..0597623400 100644
--- a/django/contrib/localflavor/in_/forms.py
+++ b/django/contrib/localflavor/in_/forms.py
@@ -2,8 +2,9 @@
India-specific Form helpers.
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import gettext
import re
diff --git a/django/contrib/localflavor/is_/forms.py b/django/contrib/localflavor/is_/forms.py
index cab6eb18c0..abf858df2b 100644
--- a/django/contrib/localflavor/is_/forms.py
+++ b/django/contrib/localflavor/is_/forms.py
@@ -2,8 +2,9 @@
Iceland specific form helpers.
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import RegexField, EMPTY_VALUES
+from django.forms.fields import RegexField
from django.forms.widgets import Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
diff --git a/django/contrib/localflavor/it/forms.py b/django/contrib/localflavor/it/forms.py
index d2d651955a..baa56a21ba 100644
--- a/django/contrib/localflavor/it/forms.py
+++ b/django/contrib/localflavor/it/forms.py
@@ -2,8 +2,9 @@
IT-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from django.contrib.localflavor.it.util import ssn_check_digit, vat_number_check_digit
diff --git a/django/contrib/localflavor/kw/forms.py b/django/contrib/localflavor/kw/forms.py
index 32d6cb990a..94296255d6 100644
--- a/django/contrib/localflavor/kw/forms.py
+++ b/django/contrib/localflavor/kw/forms.py
@@ -3,8 +3,10 @@ Kuwait-specific Form helpers
"""
import re
from datetime import date
+
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, EMPTY_VALUES
+from django.forms.fields import Field, RegexField
from django.utils.translation import gettext as _
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})')
diff --git a/django/contrib/localflavor/nl/forms.py b/django/contrib/localflavor/nl/forms.py
index 6dc5319eb7..997c28f609 100644
--- a/django/contrib/localflavor/nl/forms.py
+++ b/django/contrib/localflavor/nl/forms.py
@@ -4,8 +4,9 @@ NL-specific Form helpers
import re
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, Select, EMPTY_VALUES
+from django.forms.fields import Field, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
diff --git a/django/contrib/localflavor/no/forms.py b/django/contrib/localflavor/no/forms.py
index 0fe55adf7e..61a269c0fe 100644
--- a/django/contrib/localflavor/no/forms.py
+++ b/django/contrib/localflavor/no/forms.py
@@ -3,8 +3,9 @@ Norwegian-specific Form helpers
"""
import re, datetime
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
class NOZipCodeField(RegexField):
diff --git a/django/contrib/localflavor/pe/forms.py b/django/contrib/localflavor/pe/forms.py
index d83a2225be..7a4ac9e8d9 100644
--- a/django/contrib/localflavor/pe/forms.py
+++ b/django/contrib/localflavor/pe/forms.py
@@ -3,8 +3,9 @@
PE-specific Form helpers.
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
+from django.forms.fields import RegexField, CharField, Select
from django.utils.translation import ugettext_lazy as _
class PERegionSelect(Select):
diff --git a/django/contrib/localflavor/pt/forms.py b/django/contrib/localflavor/pt/forms.py
index 86833fc85f..1f51679c4a 100644
--- a/django/contrib/localflavor/pt/forms.py
+++ b/django/contrib/localflavor/pt/forms.py
@@ -2,8 +2,9 @@
PT-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/ro/forms.py b/django/contrib/localflavor/ro/forms.py
index ca51d91839..dd86fce9f2 100644
--- a/django/contrib/localflavor/ro/forms.py
+++ b/django/contrib/localflavor/ro/forms.py
@@ -5,8 +5,8 @@ Romanian specific form helpers.
import re
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError, Field, RegexField, Select
-from django.forms.fields import EMPTY_VALUES
from django.utils.translation import ugettext_lazy as _
class ROCIFField(RegexField):
diff --git a/django/contrib/localflavor/se/forms.py b/django/contrib/localflavor/se/forms.py
index eebd580c45..951f4f8d55 100644
--- a/django/contrib/localflavor/se/forms.py
+++ b/django/contrib/localflavor/se/forms.py
@@ -5,7 +5,7 @@ Swedish specific Form helpers
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
-from django.forms.fields import EMPTY_VALUES
+from django.core.validators import EMPTY_VALUES
from django.contrib.localflavor.se.utils import (id_number_checksum,
validate_id_birthday, format_personal_id_number, valid_organisation,
format_organisation_number)
diff --git a/django/contrib/localflavor/us/forms.py b/django/contrib/localflavor/us/forms.py
index cd18a7da9f..c426d36c66 100644
--- a/django/contrib/localflavor/us/forms.py
+++ b/django/contrib/localflavor/us/forms.py
@@ -2,8 +2,9 @@
USA-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES, CharField
+from django.forms.fields import Field, RegexField, Select, CharField
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
diff --git a/django/contrib/localflavor/uy/forms.py b/django/contrib/localflavor/uy/forms.py
index 5b47ba1e24..3f70cf336a 100644
--- a/django/contrib/localflavor/uy/forms.py
+++ b/django/contrib/localflavor/uy/forms.py
@@ -4,7 +4,8 @@ UY-specific form helpers.
"""
import re
-from django.forms.fields import Select, RegexField, EMPTY_VALUES
+from django.core.validators import EMPTY_VALUES
+from django.forms.fields import Select, RegexField
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.contrib.localflavor.uy.util import get_validation_digit
diff --git a/django/contrib/localflavor/za/forms.py b/django/contrib/localflavor/za/forms.py
index 7b7b714398..9a54f1ecb2 100644
--- a/django/contrib/localflavor/za/forms.py
+++ b/django/contrib/localflavor/za/forms.py
@@ -2,8 +2,9 @@
South Africa-specific Form helpers
"""
+from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
-from django.forms.fields import Field, RegexField, EMPTY_VALUES
+from django.forms.fields import Field, RegexField
from django.utils.checksums import luhn
from django.utils.translation import gettext as _
import re
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
index 1c21031739..fee7db4778 100644
--- a/django/core/exceptions.py
+++ b/django/core/exceptions.py
@@ -32,6 +32,42 @@ class FieldError(Exception):
"""Some kind of problem with a model field."""
pass
-class ValidationError(Exception):
+NON_FIELD_ERRORS = '__all__'
+class BaseValidationError(Exception):
"""An error while validating data."""
+ def __init__(self, message, code=None, params=None):
+ import operator
+ from django.utils.encoding import force_unicode
+ """
+ ValidationError can be passed any object that can be printed (usually
+ a string), a list of objects or a dictionary.
+ """
+ if isinstance(message, dict):
+ self.message_dict = message
+ # Reduce each list of messages into a single list.
+ message = reduce(operator.add, message.values())
+
+ if isinstance(message, list):
+ self.messages = [force_unicode(msg) for msg in message]
+ else:
+ self.code = code
+ self.params = params
+ message = force_unicode(message)
+ self.messages = [message]
+
+ def __str__(self):
+ # This is needed because, without a __str__(), printing an exception
+ # instance would result in this:
+ # AttributeError: ValidationError instance has no attribute 'args'
+ # See http://www.python.org/doc/current/tut/node10.html#handling
+ if hasattr(self, 'message_dict'):
+ return repr(self.message_dict)
+ return repr(self.messages)
+
+class ValidationError(BaseValidationError):
pass
+
+class UnresolvableValidationError(BaseValidationError):
+ """Validation error that cannot be resolved by the user."""
+ pass
+
diff --git a/django/core/validators.py b/django/core/validators.py
new file mode 100644
index 0000000000..6cd290fba9
--- /dev/null
+++ b/django/core/validators.py
@@ -0,0 +1,137 @@
+import re
+
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from django.utils.encoding import smart_unicode
+
+# These values, if given to validate(), will trigger the self.required check.
+EMPTY_VALUES = (None, '', [], (), {})
+
+try:
+ from django.conf import settings
+ URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
+except ImportError:
+ # It's OK if Django settings aren't configured.
+ URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
+
+class RegexValidator(object):
+ regex = ''
+ message = _(u'Enter a valid value.')
+ code = 'invalid'
+
+ def __init__(self, regex=None, message=None, code=None):
+ if regex is not None:
+ self.regex = regex
+ if message is not None:
+ self.message = message
+ if code is not None:
+ self.code = code
+
+ if isinstance(self.regex, basestring):
+ self.regex = re.compile(regex)
+
+ def __call__(self, value):
+ """
+ Validates that the input matches the regular expression.
+ """
+ if not self.regex.search(smart_unicode(value)):
+ raise ValidationError(self.message, code=self.code)
+
+class URLValidator(RegexValidator):
+ regex = re.compile(
+ r'^https?://' # http:// or https://
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
+ r'localhost|' #localhost...
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+ r'(?::\d+)?' # optional port
+ r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+
+ def __init__(self, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT):
+ super(URLValidator, self).__init__()
+ self.verify_exists = verify_exists
+ self.user_agent = validator_user_agent
+
+ def __call__(self, value):
+ super(URLValidator, self).__call__(value)
+ if self.verify_exists:
+ import urllib2
+ headers = {
+ "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+ "Accept-Language": "en-us,en;q=0.5",
+ "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
+ "Connection": "close",
+ "User-Agent": self.user_agent,
+ }
+ try:
+ req = urllib2.Request(value, None, headers)
+ u = urllib2.urlopen(req)
+ except ValueError:
+ raise ValidationError(_(u'Enter a valid URL.'), code='invalid')
+ except: # urllib2.URLError, httplib.InvalidURL, etc.
+ raise ValidationError(_(u'This URL appears to be a broken link.'), code='invalid_link')
+
+
+def validate_integer(value):
+ try:
+ int(value)
+ except (ValueError, TypeError), e:
+ raise ValidationError('')
+
+
+email_re = re.compile(
+ r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
+ r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
+ r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE) # domain
+validate_email = RegexValidator(email_re, _(u'Enter a valid e-mail address.'), 'invalid')
+
+slug_re = re.compile(r'^[-\w]+$')
+validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid')
+
+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}$')
+validate_ipv4_address = RegexValidator(ipv4_re, _(u'Enter a valid IPv4 address.'), 'invalid')
+
+comma_separated_int_list_re = re.compile('^[\d,]+$')
+validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid')
+
+
+class BaseValidator(object):
+ compare = lambda self, a, b: a is not b
+ clean = lambda self, x: x
+ message = _(u'Ensure this value is %(limit_value)s (it is %(show_value)s).')
+ code = 'limit_value'
+
+ def __init__(self, limit_value):
+ self.limit_value = limit_value
+
+ def __call__(self, value):
+ cleaned = self.clean(value)
+ params = {'limit_value': self.limit_value, 'show_value': cleaned}
+ if self.compare(cleaned, self.limit_value):
+ raise ValidationError(
+ self.message % params,
+ code=self.code,
+ params=params,
+ )
+
+class MaxValueValidator(BaseValidator):
+ compare = lambda self, a, b: a > b
+ message = _(u'Ensure this value is less than or equal to %(limit_value)s.')
+ code = 'max_value'
+
+class MinValueValidator(BaseValidator):
+ compare = lambda self, a, b: a < b
+ message = _(u'Ensure this value is greater than or equal to %(limit_value)s.')
+ code = 'min_value'
+
+class MinLengthValidator(BaseValidator):
+ compare = lambda self, a, b: a < b
+ clean = lambda self, x: len(x)
+ message = _(u'Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).')
+ code = 'min_length'
+
+class MaxLengthValidator(BaseValidator):
+ compare = lambda self, a, b: a > b
+ clean = lambda self, x: len(x)
+ message = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).')
+ code = 'max_length'
+
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 3464ae6712..935933ae0e 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -3,7 +3,8 @@ import sys
import os
from itertools import izip
import django.db.models.manager # Imported to register signal handler.
-from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
+from django.core import validators
from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q
@@ -12,9 +13,11 @@ from django.db.models.options import Options
from django.db import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS
from django.db.models import signals
from django.db.models.loading import register_models, get_model
+from django.utils.translation import ugettext_lazy as _
import django.utils.copycompat as copy
from django.utils.functional import curry
from django.utils.encoding import smart_str, force_unicode, smart_unicode
+from django.utils.text import get_text_list, capfirst
from django.conf import settings
class ModelBase(type):
@@ -639,6 +642,180 @@ class Model(object):
def prepare_database_save(self, unused):
return self.pk
+ def validate(self):
+ """
+ Hook for doing any extra model-wide validation after clean() has been
+ called on every field. Any ValidationError raised by this method will
+ not be associated with a particular field; it will have a special-case
+ association with the field defined by NON_FIELD_ERRORS.
+ """
+ self.validate_unique()
+
+ def validate_unique(self):
+ unique_checks, date_checks = self._get_unique_checks()
+
+ errors = self._perform_unique_checks(unique_checks)
+ date_errors = self._perform_date_checks(date_checks)
+
+ for k, v in date_errors.items():
+ errors.setdefault(k, []).extend(v)
+
+ if errors:
+ raise ValidationError(errors)
+
+ def _get_unique_checks(self):
+ from django.db.models.fields import FieldDoesNotExist, Field as ModelField
+
+ unique_checks = list(self._meta.unique_together)
+ # these are checks for the unique_for_<date/year/month>
+ date_checks = []
+
+ # Gather a list of checks for fields declared as unique and add them to
+ # the list of checks. Again, skip empty fields and any that did not validate.
+ for f in self._meta.fields:
+ name = f.name
+ if f.unique:
+ unique_checks.append((name,))
+ if f.unique_for_date:
+ date_checks.append(('date', name, f.unique_for_date))
+ if f.unique_for_year:
+ date_checks.append(('year', name, f.unique_for_year))
+ if f.unique_for_month:
+ date_checks.append(('month', name, f.unique_for_month))
+ return unique_checks, date_checks
+
+
+ def _perform_unique_checks(self, unique_checks):
+ errors = {}
+
+ for unique_check in unique_checks:
+ # Try to look up an existing object with the same values as this
+ # object's values for all the unique field.
+
+ lookup_kwargs = {}
+ for field_name in unique_check:
+ f = self._meta.get_field(field_name)
+ lookup_value = getattr(self, f.attname)
+ if f.null and lookup_value is None:
+ # no value, skip the lookup
+ continue
+ if f.primary_key and not getattr(self, '_adding', False):
+ # no need to check for unique primary key when editting
+ continue
+ lookup_kwargs[str(field_name)] = lookup_value
+
+ # some fields were skipped, no reason to do the check
+ if len(unique_check) != len(lookup_kwargs.keys()):
+ continue
+
+ qs = self.__class__._default_manager.filter(**lookup_kwargs)
+
+ # Exclude the current object from the query if we are editing an
+ # instance (as opposed to creating a new one)
+ if not getattr(self, '_adding', False) and self.pk is not None:
+ qs = qs.exclude(pk=self.pk)
+
+ # This cute trick with extra/values is the most efficient way to
+ # tell if a particular query returns any results.
+ if qs.extra(select={'a': 1}).values('a').order_by():
+ if len(unique_check) == 1:
+ key = unique_check[0]
+ else:
+ key = NON_FIELD_ERRORS
+ errors.setdefault(key, []).append(self.unique_error_message(unique_check))
+
+ return errors
+
+ def _perform_date_checks(self, date_checks):
+ errors = {}
+ for lookup_type, field, unique_for in date_checks:
+ lookup_kwargs = {}
+ # there's a ticket to add a date lookup, we can remove this special
+ # case if that makes it's way in
+ date = getattr(self, unique_for)
+ if lookup_type == 'date':
+ lookup_kwargs['%s__day' % unique_for] = date.day
+ lookup_kwargs['%s__month' % unique_for] = date.month
+ lookup_kwargs['%s__year' % unique_for] = date.year
+ else:
+ lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(date, lookup_type)
+ lookup_kwargs[field] = getattr(self, field)
+
+ qs = self.__class__._default_manager.filter(**lookup_kwargs)
+ # Exclude the current object from the query if we are editing an
+ # instance (as opposed to creating a new one)
+ if not getattr(self, '_adding', False) and self.pk is not None:
+ qs = qs.exclude(pk=self.pk)
+
+ # This cute trick with extra/values is the most efficient way to
+ # tell if a particular query returns any results.
+ if qs.extra(select={'a': 1}).values('a').order_by():
+ errors.setdefault(field, []).append(
+ self.date_error_message(lookup_type, field, unique_for)
+ )
+ return errors
+
+ def date_error_message(self, lookup_type, field, unique_for):
+ opts = self._meta
+ return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
+ 'field_name': unicode(capfirst(opts.get_field(field).verbose_name)),
+ 'date_field': unicode(capfirst(opts.get_field(unique_for).verbose_name)),
+ 'lookup': lookup_type,
+ }
+
+ def unique_error_message(self, unique_check):
+ opts = self._meta
+ model_name = capfirst(opts.verbose_name)
+
+ # A unique field
+ if len(unique_check) == 1:
+ field_name = unique_check[0]
+ field_label = capfirst(opts.get_field(field_name).verbose_name)
+ # Insert the error into the error dict, very sneaky
+ return _(u"%(model_name)s with this %(field_label)s already exists.") % {
+ 'model_name': unicode(model_name),
+ 'field_label': unicode(field_label)
+ }
+ # unique_together
+ else:
+ field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check)
+ field_labels = get_text_list(field_labels, _('and'))
+ return _(u"%(model_name)s with this %(field_label)s already exists.") % {
+ 'model_name': unicode(model_name),
+ 'field_label': unicode(field_labels)
+ }
+
+ def full_validate(self, exclude=[]):
+ """
+ Cleans all fields and raises ValidationError containing message_dict
+ of all validation errors if any occur.
+ """
+ errors = {}
+ for f in self._meta.fields:
+ if f.name in exclude:
+ continue
+ try:
+ setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
+ except ValidationError, e:
+ errors[f.name] = e.messages
+
+ # Form.clean() is run even if other validation fails, so do the
+ # same with Model.validate() for consistency.
+ try:
+ self.validate()
+ except ValidationError, e:
+ if hasattr(e, 'message_dict'):
+ if errors:
+ for k, v in e.message_dict.items():
+ errors.set_default(k, []).extend(v)
+ else:
+ errors = e.message_dict
+ else:
+ errors[NON_FIELD_ERRORS] = e.messages
+
+ if errors:
+ raise ValidationError(errors)
+
############################################
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index b70f320df3..25f44a4d1c 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -13,12 +13,12 @@ from django.db.models.query_utils import QueryWrapper
from django.dispatch import dispatcher
from django.conf import settings
from django import forms
-from django.core import exceptions
+from django.core import exceptions, validators
from django.utils.datastructures import DictWrapper
from django.utils.functional import curry
from django.utils.itercompat import tee
from django.utils.text import capfirst
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils import datetime_safe
@@ -60,6 +60,12 @@ class Field(object):
# creates, creation_counter is used for all user-specified fields.
creation_counter = 0
auto_creation_counter = -1
+ default_validators = [] # Default set of validators
+ default_error_messages = {
+ 'invalid_choice': _(u'Value %r is not a valid choice.'),
+ 'null': _(u'This field cannot be null.'),
+ 'blank': _(u'This field cannot be blank.'),
+ }
# Generic field type description, usually overriden by subclasses
def _description(self):
@@ -73,7 +79,8 @@ class Field(object):
db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
serialize=True, unique_for_date=None, unique_for_month=None,
unique_for_year=None, choices=None, help_text='', db_column=None,
- db_tablespace=None, auto_created=False):
+ db_tablespace=None, auto_created=False, validators=[],
+ error_messages=None):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@@ -106,6 +113,42 @@ class Field(object):
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
+ self.validators = self.default_validators + validators
+
+ messages = {}
+ for c in reversed(self.__class__.__mro__):
+ messages.update(getattr(c, 'default_error_messages', {}))
+ messages.update(error_messages or {})
+ self.error_messages = messages
+
+ def __getstate__(self):
+ """
+ Pickling support.
+ """
+ from django.utils.functional import Promise
+ obj_dict = self.__dict__.copy()
+ items = []
+ translated_keys = []
+ for k, v in self.error_messages.items():
+ if isinstance(v, Promise):
+ args = getattr(v, '_proxy____args', None)
+ if args:
+ translated_keys.append(k)
+ v = args[0]
+ items.append((k,v))
+ obj_dict['_translated_keys'] = translated_keys
+ obj_dict['error_messages'] = dict(items)
+ return obj_dict
+
+ def __setstate__(self, obj_dict):
+ """
+ Unpickling support.
+ """
+ translated_keys = obj_dict.pop('_translated_keys')
+ self.__dict__.update(obj_dict)
+ for k in translated_keys:
+ self.error_messages[k] = _(self.error_messages[k])
+
def __cmp__(self, other):
# This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter)
@@ -127,6 +170,54 @@ class Field(object):
"""
return value
+ def run_validators(self, value):
+ if value in validators.EMPTY_VALUES:
+ return
+
+ errors = []
+ for v in self.validators:
+ try:
+ v(value)
+ except exceptions.ValidationError, e:
+ if hasattr(e, 'code') and e.code in self.error_messages:
+ message = self.error_messages[e.code]
+ if e.params:
+ message = message % e.params
+ errors.append(message)
+ else:
+ errors.extend(e.messages)
+ if errors:
+ raise exceptions.ValidationError(errors)
+
+ def validate(self, value, model_instance):
+ """
+ Validates value and throws ValidationError. Subclasses should override
+ this to provide validation logic.
+ """
+ if not self.editable:
+ # Skip validation for non-editable fields.
+ return
+ if self._choices and value:
+ if not value in dict(self.choices):
+ raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
+
+ if value is None and not self.null:
+ raise exceptions.ValidationError(self.error_messages['null'])
+
+ if not self.blank and value in validators.EMPTY_VALUES:
+ raise exceptions.ValidationError(self.error_messages['blank'])
+
+ def clean(self, value, model_instance):
+ """
+ Convert the value's type and run validation. Validation errors from to_python
+ and validate are propagated. The correct value is returned if no error is
+ raised.
+ """
+ value = self.to_python(value)
+ self.validate(value, model_instance)
+ self.run_validators(value)
+ return value
+
def db_type(self, connection):
"""
Returns the database column data type for this field, for the provided
@@ -377,9 +468,12 @@ class Field(object):
return getattr(obj, self.attname)
class AutoField(Field):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _(u'This value must be an integer.'),
+ }
def __init__(self, *args, **kwargs):
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
kwargs['blank'] = True
@@ -391,8 +485,10 @@ class AutoField(Field):
try:
return int(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be an integer."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
+
+ def validate(self, value, model_instance):
+ pass
def get_prep_value(self, value):
if value is None:
@@ -410,7 +506,10 @@ class AutoField(Field):
class BooleanField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Boolean (Either True or False)")
+ default_error_messages = {
+ 'invalid': _(u'This value must be either True or False.'),
+ }
+ description = _("Boolean (Either True or False)")
def __init__(self, *args, **kwargs):
kwargs['blank'] = True
if 'default' not in kwargs and not kwargs.get('null'):
@@ -424,8 +523,7 @@ class BooleanField(Field):
if value in (True, False): return value
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
- raise exceptions.ValidationError(
- _("This value must be either True or False."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@@ -453,36 +551,35 @@ class BooleanField(Field):
return super(BooleanField, self).formfield(**defaults)
class CharField(Field):
- description = ugettext_lazy("String (up to %(max_length)s)")
+ description = _("String (up to %(max_length)s)")
+
+ def __init__(self, *args, **kwargs):
+ super(CharField, self).__init__(*args, **kwargs)
+ self.validators.append(validators.MaxLengthValidator(self.max_length))
def get_internal_type(self):
return "CharField"
def to_python(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, basestring) or value is None:
return value
- if value is None:
- if self.null:
- return value
- else:
- raise exceptions.ValidationError(
- ugettext_lazy("This field cannot be null."))
return smart_unicode(value)
def formfield(self, **kwargs):
+ # Passing max_length to forms.CharField means that the value's length
+ # will be validated twice. This is considered acceptable since we want
+ # the value in the form field (to pass into widget for example).
defaults = {'max_length': self.max_length}
defaults.update(kwargs)
return super(CharField, self).formfield(**defaults)
# TODO: Maybe move this into contrib, because it's specialized.
class CommaSeparatedIntegerField(CharField):
- description = ugettext_lazy("Comma-separated integers")
+ default_validators = [validators.validate_comma_separated_integer_list]
+ description = _("Comma-separated integers")
def formfield(self, **kwargs):
defaults = {
- 'form_class': forms.RegexField,
- 'regex': '^[\d,]+$',
- 'max_length': self.max_length,
'error_messages': {
'invalid': _(u'Enter only digits separated by commas.'),
}
@@ -493,9 +590,13 @@ class CommaSeparatedIntegerField(CharField):
ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
class DateField(Field):
- description = ugettext_lazy("Date (without time)")
+ description = _("Date (without time)")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _('Enter a valid date in YYYY-MM-DD format.'),
+ 'invalid_date': _('Invalid date: %s'),
+ }
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
#HACKs : auto_now_add/auto_now should be done as a default or a pre_save.
@@ -516,8 +617,7 @@ class DateField(Field):
return value
if not ansi_date_re.search(value):
- raise exceptions.ValidationError(
- _('Enter a valid date in YYYY-MM-DD format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
# Now that we have the date string in YYYY-MM-DD format, check to make
# sure it's a valid date.
# We could use time.strptime here and catch errors, but datetime.date
@@ -526,7 +626,7 @@ class DateField(Field):
try:
return datetime.date(year, month, day)
except ValueError, e:
- msg = _('Invalid date: %s') % _(str(e))
+ msg = self.error_messages['invalid_date'] % _(str(e))
raise exceptions.ValidationError(msg)
def pre_save(self, model_instance, add):
@@ -575,7 +675,10 @@ class DateField(Field):
return super(DateField, self).formfield(**defaults)
class DateTimeField(DateField):
- description = ugettext_lazy("Date (with time)")
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'),
+ }
+ description = _("Date (with time)")
def get_internal_type(self):
return "DateTimeField"
@@ -596,8 +699,7 @@ class DateTimeField(DateField):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@@ -614,8 +716,7 @@ class DateTimeField(DateField):
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3],
**kwargs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_value(self, value):
return self.to_python(value)
@@ -642,7 +743,11 @@ class DateTimeField(DateField):
class DecimalField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Decimal number")
+ default_error_messages = {
+ 'invalid': _(u'This value must be a decimal number.'),
+ }
+ description = _("Decimal number")
+
def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, verbose_name, name, **kwargs)
@@ -656,8 +761,7 @@ class DecimalField(Field):
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
- raise exceptions.ValidationError(
- _("This value must be a decimal number."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def _format(self, value):
if isinstance(value, basestring) or value is None:
@@ -696,18 +800,15 @@ class DecimalField(Field):
return super(DecimalField, self).formfield(**defaults)
class EmailField(CharField):
- description = ugettext_lazy("E-mail address")
+ default_validators = [validators.validate_email]
+ description = _("E-mail address")
+
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 75)
CharField.__init__(self, *args, **kwargs)
- def formfield(self, **kwargs):
- defaults = {'form_class': forms.EmailField}
- defaults.update(kwargs)
- return super(EmailField, self).formfield(**defaults)
-
class FilePathField(Field):
- description = ugettext_lazy("File path")
+ description = _("File path")
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
@@ -729,7 +830,10 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Floating point number")
+ default_error_messages = {
+ 'invalid': _("This value must be a float."),
+ }
+ description = _("Floating point number")
def get_prep_value(self, value):
if value is None:
@@ -745,8 +849,7 @@ class FloatField(Field):
try:
return float(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be a float."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField}
@@ -755,7 +858,10 @@ class FloatField(Field):
class IntegerField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Integer")
+ default_error_messages = {
+ 'invalid': _("This value must be a float."),
+ }
+ description = _("Integer")
def get_prep_value(self, value):
if value is None:
@@ -771,8 +877,7 @@ class IntegerField(Field):
try:
return int(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be an integer."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField}
@@ -781,7 +886,7 @@ class IntegerField(Field):
class BigIntegerField(IntegerField):
empty_strings_allowed = False
- description = ugettext_lazy("Big (8 byte) integer")
+ description = _("Big (8 byte) integer")
MAX_BIGINT = 9223372036854775807
def get_internal_type(self):
return "BigIntegerField"
@@ -794,7 +899,7 @@ class BigIntegerField(IntegerField):
class IPAddressField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("IP address")
+ description = _("IP address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15
Field.__init__(self, *args, **kwargs)
@@ -809,7 +914,11 @@ class IPAddressField(Field):
class NullBooleanField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Boolean (Either True, False or None)")
+ default_error_messages = {
+ 'invalid': _("This value must be either None, True or False."),
+ }
+ description = _("Boolean (Either True, False or None)")
+
def __init__(self, *args, **kwargs):
kwargs['null'] = True
Field.__init__(self, *args, **kwargs)
@@ -822,8 +931,7 @@ class NullBooleanField(Field):
if value in ('None',): return None
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
- raise exceptions.ValidationError(
- _("This value must be either None, True or False."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@@ -849,7 +957,7 @@ class NullBooleanField(Field):
return super(NullBooleanField, self).formfield(**defaults)
class PositiveIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "PositiveIntegerField"
@@ -860,7 +968,7 @@ class PositiveIntegerField(IntegerField):
return super(PositiveIntegerField, self).formfield(**defaults)
class PositiveSmallIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "PositiveSmallIntegerField"
@@ -870,7 +978,7 @@ class PositiveSmallIntegerField(IntegerField):
return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField):
- description = ugettext_lazy("String (up to %(max_length)s)")
+ description = _("String (up to %(max_length)s)")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50)
# Set db_index=True unless it's been set manually.
@@ -887,13 +995,13 @@ class SlugField(CharField):
return super(SlugField, self).formfield(**defaults)
class SmallIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "SmallIntegerField"
class TextField(Field):
- description = ugettext_lazy("Text")
+ description = _("Text")
def get_internal_type(self):
return "TextField"
@@ -904,9 +1012,12 @@ class TextField(Field):
return super(TextField, self).formfield(**defaults)
class TimeField(Field):
- description = ugettext_lazy("Time")
+ description = _("Time")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'),
+ }
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
if auto_now or auto_now_add:
@@ -935,8 +1046,7 @@ class TimeField(Field):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@@ -949,8 +1059,7 @@ class TimeField(Field):
return datetime.time(*time.strptime(value, '%H:%M')[3:5],
**kwargs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -983,21 +1092,17 @@ class TimeField(Field):
return super(TimeField, self).formfield(**defaults)
class URLField(CharField):
- description = ugettext_lazy("URL")
+ description = _("URL")
def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
- self.verify_exists = verify_exists
CharField.__init__(self, verbose_name, name, **kwargs)
-
- def formfield(self, **kwargs):
- defaults = {'form_class': forms.URLField, 'verify_exists': self.verify_exists}
- defaults.update(kwargs)
- return super(URLField, self).formfield(**defaults)
+ self.validators.append(validators.URLValidator(verify_exists=verify_exists))
class XMLField(TextField):
- description = ugettext_lazy("XML text")
+ description = _("XML text")
def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
self.schema_path = schema_path
Field.__init__(self, verbose_name, name, **kwargs)
+
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 7cc9a03907..749bdcf39a 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -7,7 +7,7 @@ from django.db.models.related import RelatedObject
from django.db.models.query import QuerySet
from django.db.models.query_utils import QueryWrapper
from django.utils.encoding import smart_unicode
-from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _
+from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
from django.utils.functional import curry
from django.core import exceptions
from django import forms
@@ -473,7 +473,7 @@ def create_many_related_manager(superclass, rel=False):
if not rel.through._meta.auto_created:
opts = through._meta
raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
- new_obj = super(ManyRelatedManager, self).using(self.instance._state.db).create(**kwargs)
+ new_obj = super(ManyRelatedManager, self).create(**kwargs)
self.add(new_obj)
return new_obj
create.alters_data = True
@@ -708,7 +708,10 @@ class ManyToManyRel(object):
class ForeignKey(RelatedField, Field):
empty_strings_allowed = False
- description = ugettext_lazy("Foreign Key (type determined by related field)")
+ default_error_messages = {
+ 'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
+ }
+ description = _("Foreign Key (type determined by related field)")
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try:
to_name = to._meta.object_name.lower()
@@ -731,6 +734,18 @@ class ForeignKey(RelatedField, Field):
self.db_index = True
+ def validate(self, value, model_instance):
+ if self.rel.parent_link:
+ return
+ super(ForeignKey, self).validate(value, model_instance)
+ if not value:
+ return
+ try:
+ self.rel.to._default_manager.get(**{self.rel.field_name:value})
+ except self.rel.to.DoesNotExist, e:
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'] % {'model': self.rel.to._meta.verbose_name, 'pk': value})
+
def get_attname(self):
return '%s_id' % self.name
@@ -812,7 +827,7 @@ class OneToOneField(ForeignKey):
always returns the object pointed to (since there will only ever be one),
rather than returning a list.
"""
- description = ugettext_lazy("One-to-one relationship")
+ description = _("One-to-one relationship")
def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@@ -826,6 +841,12 @@ class OneToOneField(ForeignKey):
return None
return super(OneToOneField, self).formfield(**kwargs)
+ def save_form_data(self, instance, data):
+ if isinstance(data, self.rel.to):
+ setattr(instance, self.name, data)
+ else:
+ setattr(instance, self.attname, data)
+
def create_many_to_many_intermediary_model(field, klass):
from django.db import models
managed = True
@@ -866,7 +887,7 @@ def create_many_to_many_intermediary_model(field, klass):
})
class ManyToManyField(RelatedField, Field):
- description = ugettext_lazy("Many-to-many relationship")
+ description = _("Many-to-many relationship")
def __init__(self, to, **kwargs):
try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
@@ -886,7 +907,7 @@ class ManyToManyField(RelatedField, Field):
Field.__init__(self, **kwargs)
- msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
+ msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
self.help_text = string_concat(self.help_text, ' ', msg)
def get_choices_default(self):
diff --git a/django/forms/__init__.py b/django/forms/__init__.py
index 0d9c68f9e0..dc8b5212c4 100644
--- a/django/forms/__init__.py
+++ b/django/forms/__init__.py
@@ -10,7 +10,7 @@ TODO:
"This form field requires foo.js" and form.js_includes()
"""
-from util import ValidationError
+from django.core.exceptions import ValidationError
from widgets import *
from fields import *
from forms import *
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 1194196fd1..2e6eb821f3 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -14,15 +14,21 @@ try:
except ImportError:
from StringIO import StringIO
-import django.core.exceptions
+from django.core.exceptions import ValidationError
+from django.core import validators
import django.utils.copycompat as copy
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str
from django.utils.formats import get_format
from django.utils.functional import lazy
-from util import ErrorList, ValidationError
-from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
+# Provide this import for backwards compatibility.
+from django.core.validators import EMPTY_VALUES
+
+from util import ErrorList
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \
+ FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \
+ DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
__all__ = (
'Field', 'CharField', 'IntegerField',
@@ -36,9 +42,6 @@ __all__ = (
'TypedChoiceField'
)
-# These values, if given to to_python(), will trigger the self.required check.
-EMPTY_VALUES = (None, '')
-
def en_format(name):
"""
Helper function to stay backward compatible.
@@ -57,6 +60,7 @@ DEFAULT_DATETIME_INPUT_FORMATS = lazy(lambda: en_format('DATETIME_INPUT_FORMATS'
class Field(object):
widget = TextInput # Default widget to use when rendering this type of Field.
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
+ default_validators = [] # Default set of validators
default_error_messages = {
'required': _(u'This field is required.'),
'invalid': _(u'Enter a valid value.'),
@@ -66,7 +70,8 @@ class Field(object):
creation_counter = 0
def __init__(self, required=True, widget=None, label=None, initial=None,
- help_text=None, error_messages=None, show_hidden_initial=False):
+ help_text=None, error_messages=None, show_hidden_initial=False,
+ validators=[]):
# required -- Boolean that specifies whether the field is required.
# True by default.
# widget -- A Widget class, or instance of a Widget class, that should
@@ -82,6 +87,7 @@ class Field(object):
# help_text -- An optional string to use as "help text" for this Field.
# show_hidden_initial -- Boolean that specifies if it is needed to render a
# hidden widget with initial value after widget.
+ # validators -- List of addtional validators to use
if label is not None:
label = smart_unicode(label)
self.required, self.label, self.initial = required, label, initial
@@ -105,16 +111,39 @@ class Field(object):
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
- def set_class_error_messages(messages, klass):
- for base_class in klass.__bases__:
- set_class_error_messages(messages, base_class)
- messages.update(getattr(klass, 'default_error_messages', {}))
-
messages = {}
- set_class_error_messages(messages, self.__class__)
+ for c in reversed(self.__class__.__mro__):
+ messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
+ self.validators = self.default_validators + validators
+
+ def to_python(self, value):
+ return value
+
+ def validate(self, value):
+ if value in validators.EMPTY_VALUES and self.required:
+ raise ValidationError(self.error_messages['required'])
+
+ def run_validators(self, value):
+ if value in validators.EMPTY_VALUES:
+ return
+ errors = []
+ for v in self.validators:
+ try:
+ v(value)
+ except ValidationError, e:
+ if hasattr(e, 'code') and e.code in self.error_messages:
+ message = self.error_messages[e.code]
+ if e.params:
+ message = message % e.params
+ errors.append(message)
+ else:
+ errors.extend(e.messages)
+ if errors:
+ raise ValidationError(errors)
+
def clean(self, value):
"""
Validates the given value and returns its "cleaned" value as an
@@ -122,8 +151,9 @@ class Field(object):
Raises ValidationError for any errors.
"""
- if self.required and value in EMPTY_VALUES:
- raise ValidationError(self.error_messages['required'])
+ value = self.to_python(value)
+ self.validate(value)
+ self.run_validators(value)
return value
def widget_attrs(self, widget):
@@ -141,27 +171,19 @@ class Field(object):
return result
class CharField(Field):
- default_error_messages = {
- 'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'),
- 'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'),
- }
-
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
super(CharField, self).__init__(*args, **kwargs)
+ if min_length is not None:
+ self.validators.append(validators.MinLengthValidator(min_length))
+ if max_length is not None:
+ self.validators.append(validators.MaxLengthValidator(max_length))
- def clean(self, value):
- "Validates max_length and min_length. Returns a Unicode object."
- super(CharField, self).clean(value)
- if value in EMPTY_VALUES:
+ def to_python(self, value):
+ "Returns a Unicode object."
+ if value in validators.EMPTY_VALUES:
return u''
- value = smart_unicode(value)
- value_length = len(value)
- if self.max_length is not None and value_length > self.max_length:
- raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
- if self.min_length is not None and value_length < self.min_length:
- raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
- return value
+ return smart_unicode(value)
def widget_attrs(self, widget):
if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
@@ -171,87 +193,82 @@ class CharField(Field):
class IntegerField(Field):
default_error_messages = {
'invalid': _(u'Enter a whole number.'),
- 'max_value': _(u'Ensure this value is less than or equal to %s.'),
- 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+ 'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
+ 'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
}
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
super(IntegerField, self).__init__(*args, **kwargs)
- def clean(self, value):
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
+
+ def to_python(self, value):
"""
Validates that int() can be called on the input. Returns the result
of int(). Returns None for empty values.
"""
- super(IntegerField, self).clean(value)
- if value in EMPTY_VALUES:
+ value = super(IntegerField, self).to_python(value)
+ if value in validators.EMPTY_VALUES:
return None
+
try:
value = int(str(value))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'])
- if self.max_value is not None and value > self.max_value:
- raise ValidationError(self.error_messages['max_value'] % self.max_value)
- if self.min_value is not None and value < self.min_value:
- raise ValidationError(self.error_messages['min_value'] % self.min_value)
return value
-class FloatField(Field):
+class FloatField(IntegerField):
default_error_messages = {
'invalid': _(u'Enter a number.'),
- 'max_value': _(u'Ensure this value is less than or equal to %s.'),
- 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
}
- def __init__(self, max_value=None, min_value=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- Field.__init__(self, *args, **kwargs)
-
- def clean(self, value):
+ def to_python(self, value):
"""
- Validates that float() can be called on the input. Returns a float.
- Returns None for empty values.
+ Validates that float() can be called on the input. Returns the result
+ of float(). Returns None for empty values.
"""
- super(FloatField, self).clean(value)
- if not self.required and value in EMPTY_VALUES:
+ value = super(IntegerField, self).to_python(value)
+ if value in validators.EMPTY_VALUES:
return None
+
try:
# We always accept dot as decimal separator
if isinstance(value, str) or isinstance(value, unicode):
value = float(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'])
- if self.max_value is not None and value > self.max_value:
- raise ValidationError(self.error_messages['max_value'] % self.max_value)
- if self.min_value is not None and value < self.min_value:
- raise ValidationError(self.error_messages['min_value'] % self.min_value)
return value
class DecimalField(Field):
default_error_messages = {
'invalid': _(u'Enter a number.'),
- 'max_value': _(u'Ensure this value is less than or equal to %s.'),
- 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+ 'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
+ 'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
'max_digits': _('Ensure that there are no more than %s digits in total.'),
'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
}
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, *args, **kwargs)
- def clean(self, value):
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
+
+ def to_python(self, value):
"""
Validates that the input is a decimal number. Returns a Decimal
instance. Returns None for empty values. Ensures that there are no more
than max_digits in the number, and no more than decimal_places digits
after the decimal point.
"""
- super(DecimalField, self).clean(value)
- if not self.required and value in EMPTY_VALUES:
+ if value in validators.EMPTY_VALUES:
return None
value = smart_str(value).strip()
try:
@@ -260,7 +277,12 @@ class DecimalField(Field):
value = Decimal(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except DecimalException:
raise ValidationError(self.error_messages['invalid'])
+ return value
+ def validate(self, value):
+ super(DecimalField, self).validate(value)
+ if value in validators.EMPTY_VALUES:
+ return
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
@@ -273,10 +295,6 @@ class DecimalField(Field):
digits = decimals
whole_digits = digits - decimals
- if self.max_value is not None and value > self.max_value:
- raise ValidationError(self.error_messages['max_value'] % self.max_value)
- if self.min_value is not None and value < self.min_value:
- raise ValidationError(self.error_messages['min_value'] % self.min_value)
if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
if self.decimal_places is not None and decimals > self.decimal_places:
@@ -295,13 +313,12 @@ class DateField(Field):
super(DateField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
- def clean(self, value):
+ def to_python(self, value):
"""
Validates that the input can be converted to a date. Returns a Python
datetime.date object.
"""
- super(DateField, self).clean(value)
- if value in EMPTY_VALUES:
+ if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.datetime):
return value.date()
@@ -324,13 +341,12 @@ class TimeField(Field):
super(TimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
- def clean(self, value):
+ def to_python(self, value):
"""
Validates that the input can be converted to a time. Returns a Python
datetime.time object.
"""
- super(TimeField, self).clean(value)
- if value in EMPTY_VALUES:
+ if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.time):
return value
@@ -351,13 +367,12 @@ class DateTimeField(Field):
super(DateTimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
- def clean(self, value):
+ def to_python(self, value):
"""
Validates that the input can be converted to a datetime. Returns a
Python datetime.datetime object.
"""
- super(DateTimeField, self).clean(value)
- if value in EMPTY_VALUES:
+ if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.datetime):
return value
@@ -392,40 +407,13 @@ class RegexField(CharField):
if isinstance(regex, basestring):
regex = re.compile(regex)
self.regex = regex
+ self.validators.append(validators.RegexValidator(regex=regex))
- def clean(self, value):
- """
- Validates that the input matches the regular expression. Returns a
- Unicode object.
- """
- value = super(RegexField, self).clean(value)
- if value == u'':
- return value
- if not self.regex.search(value):
- raise ValidationError(self.error_messages['invalid'])
- return value
-
-email_re = re.compile(
- r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
- r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
- r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE) # domain
-
-class EmailField(RegexField):
+class EmailField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid e-mail address.'),
}
-
- def __init__(self, max_length=None, min_length=None, *args, **kwargs):
- RegexField.__init__(self, email_re, max_length, min_length, *args,
- **kwargs)
-
-try:
- from django.conf import settings
- URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
-except ImportError:
- # It's OK if Django settings aren't configured.
- URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
-
+ default_validators = [validators.validate_email]
class FileField(Field):
widget = FileInput
@@ -440,12 +428,9 @@ class FileField(Field):
self.max_length = kwargs.pop('max_length', None)
super(FileField, self).__init__(*args, **kwargs)
- def clean(self, data, initial=None):
- super(FileField, self).clean(initial or data)
- if not self.required and data in EMPTY_VALUES:
+ def to_python(self, data):
+ if data in validators.EMPTY_VALUES:
return None
- elif not data and initial:
- return initial
# UploadedFile objects should have name and size attributes.
try:
@@ -464,21 +449,24 @@ class FileField(Field):
return data
+ def clean(self, data, initial=None):
+ if not data and initial:
+ return initial
+ return super(FileField, self).clean(data)
+
class ImageField(FileField):
default_error_messages = {
'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
}
- def clean(self, data, initial=None):
+ def to_python(self, data):
"""
Checks that the file-upload field data contains a valid image (GIF, JPG,
PNG, possibly others -- whatever the Python Imaging Library supports).
"""
- f = super(ImageField, self).clean(data, initial)
+ f = super(ImageField, self).to_python(data)
if f is None:
return None
- elif not data and initial:
- return initial
from PIL import Image
# We need to get a file object for PIL. We might have a path or we might
@@ -517,59 +505,34 @@ class ImageField(FileField):
f.seek(0)
return f
-url_re = re.compile(
- r'^https?://' # http:// or https://
- r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
- r'localhost|' #localhost...
- r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
- r'(?::\d+)?' # optional port
- r'(?:/?|/\S+)$', re.IGNORECASE)
-
-class URLField(RegexField):
+class URLField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid URL.'),
'invalid_link': _(u'This URL appears to be a broken link.'),
}
def __init__(self, max_length=None, min_length=None, verify_exists=False,
- validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
- super(URLField, self).__init__(url_re, max_length, min_length, *args,
+ validator_user_agent=validators.URL_VALIDATOR_USER_AGENT, *args, **kwargs):
+ super(URLField, self).__init__(max_length, min_length, *args,
**kwargs)
- self.verify_exists = verify_exists
- self.user_agent = validator_user_agent
+ self.validators.append(validators.URLValidator(verify_exists=verify_exists, validator_user_agent=validator_user_agent))
- def clean(self, value):
- # If no URL scheme given, assume http://
- if value and '://' not in value:
- value = u'http://%s' % value
- # If no URL path given, assume /
- if value and not urlparse.urlsplit(value)[2]:
- value += '/'
- value = super(URLField, self).clean(value)
- if value == u'':
- return value
- if self.verify_exists:
- import urllib2
- headers = {
- "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
- "Accept-Language": "en-us,en;q=0.5",
- "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
- "Connection": "close",
- "User-Agent": self.user_agent,
- }
- try:
- req = urllib2.Request(value, None, headers)
- u = urllib2.urlopen(req)
- except ValueError:
- raise ValidationError(self.error_messages['invalid'])
- except: # urllib2.URLError, httplib.InvalidURL, etc.
- raise ValidationError(self.error_messages['invalid_link'])
- return value
+ def to_python(self, value):
+ if value:
+ if '://' not in value:
+ # If no URL scheme given, assume http://
+ value = u'http://%s' % value
+ url_fields = list(urlparse.urlsplit(value))
+ if not url_fields[2]:
+ # the path portion may need to be added before query params
+ url_fields[2] = '/'
+ value = urlparse.urlunsplit(url_fields)
+ return super(URLField, self).to_python(value)
class BooleanField(Field):
widget = CheckboxInput
- def clean(self, value):
+ def to_python(self, value):
"""Returns a Python boolean object."""
# Explicitly check for the string 'False', which is what a hidden field
# will submit for False. Also check for '0', since this is what
@@ -579,7 +542,7 @@ class BooleanField(Field):
value = False
else:
value = bool(value)
- super(BooleanField, self).clean(value)
+ value = super(BooleanField, self).to_python(value)
if not value and self.required:
raise ValidationError(self.error_messages['required'])
return value
@@ -591,7 +554,7 @@ class NullBooleanField(BooleanField):
"""
widget = NullBooleanSelect
- def clean(self, value):
+ def to_python(self, value):
"""
Explicitly checks for the string 'True' and 'False', which is what a
hidden field will submit for True and False, and for '1' and '0', which
@@ -605,6 +568,9 @@ class NullBooleanField(BooleanField):
else:
return None
+ def validate(self, value):
+ pass
+
class ChoiceField(Field):
widget = Select
default_error_messages = {
@@ -613,8 +579,8 @@ class ChoiceField(Field):
def __init__(self, choices=(), required=True, widget=None, label=None,
initial=None, help_text=None, *args, **kwargs):
- super(ChoiceField, self).__init__(required, widget, label, initial,
- help_text, *args, **kwargs)
+ super(ChoiceField, self).__init__(required=required, widget=widget, label=label,
+ initial=initial, help_text=help_text, *args, **kwargs)
self.choices = choices
def _get_choices(self):
@@ -628,19 +594,19 @@ class ChoiceField(Field):
choices = property(_get_choices, _set_choices)
- def clean(self, value):
+ def to_python(self, value):
+ "Returns a Unicode object."
+ if value in validators.EMPTY_VALUES:
+ return u''
+ return smart_unicode(value)
+
+ def validate(self, value):
"""
Validates that the input is in self.choices.
"""
- value = super(ChoiceField, self).clean(value)
- if value in EMPTY_VALUES:
- value = u''
- value = smart_unicode(value)
- if value == u'':
- return value
- if not self.valid_value(value):
+ super(ChoiceField, self).validate(value)
+ if value and not self.valid_value(value):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
- return value
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
@@ -661,27 +627,24 @@ class TypedChoiceField(ChoiceField):
self.empty_value = kwargs.pop('empty_value', '')
super(TypedChoiceField, self).__init__(*args, **kwargs)
- def clean(self, value):
+ def to_python(self, value):
"""
Validate that the value is in self.choices and can be coerced to the
right type.
"""
- value = super(TypedChoiceField, self).clean(value)
- if value == self.empty_value or value in EMPTY_VALUES:
+ value = super(TypedChoiceField, self).to_python(value)
+ super(TypedChoiceField, self).validate(value)
+ if value == self.empty_value or value in validators.EMPTY_VALUES:
return self.empty_value
-
- # Hack alert: This field is purpose-made to use with Field.to_python as
- # a coercion function so that ModelForms with choices work. However,
- # Django's Field.to_python raises
- # django.core.exceptions.ValidationError, which is a *different*
- # exception than django.forms.util.ValidationError. So we need to catch
- # both.
try:
value = self.coerce(value)
- except (ValueError, TypeError, django.core.exceptions.ValidationError):
+ except (ValueError, TypeError, ValidationError):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
+ def validate(self, value):
+ pass
+
class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput
widget = SelectMultiple
@@ -690,22 +653,23 @@ class MultipleChoiceField(ChoiceField):
'invalid_list': _(u'Enter a list of values.'),
}
- def clean(self, value):
+ def to_python(self, value):
+ if not value:
+ return []
+ elif not isinstance(value, (list, tuple)):
+ raise ValidationError(self.error_messages['invalid_list'])
+ return [smart_unicode(val) for val in value]
+
+ def validate(self, value):
"""
Validates that the input is a list or tuple.
"""
if self.required and not value:
raise ValidationError(self.error_messages['required'])
- elif not self.required and not value:
- return []
- if not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['invalid_list'])
- new_value = [smart_unicode(val) for val in value]
# Validate that each value in the value list is in self.choices.
- for val in new_value:
+ for val in value:
if not self.valid_value(val):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
- return new_value
class ComboField(Field):
"""
@@ -760,6 +724,9 @@ class MultiValueField(Field):
f.required = False
self.fields = fields
+ def validate(self, value):
+ pass
+
def clean(self, value):
"""
Validates every value in the given list. A value is validated against
@@ -772,7 +739,7 @@ class MultiValueField(Field):
clean_data = []
errors = ErrorList()
if not value or isinstance(value, (list, tuple)):
- if not value or not [v for v in value if v not in EMPTY_VALUES]:
+ if not value or not [v for v in value if v not in validators.EMPTY_VALUES]:
if self.required:
raise ValidationError(self.error_messages['required'])
else:
@@ -784,7 +751,7 @@ class MultiValueField(Field):
field_value = value[i]
except IndexError:
field_value = None
- if self.required and field_value in EMPTY_VALUES:
+ if self.required and field_value in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['required'])
try:
clean_data.append(field.clean(field_value))
@@ -795,7 +762,10 @@ class MultiValueField(Field):
errors.extend(e.messages)
if errors:
raise ValidationError(errors)
- return self.compress(clean_data)
+
+ out = self.compress(clean_data)
+ self.validate(out)
+ return out
def compress(self, data_list):
"""
@@ -864,30 +834,24 @@ class SplitDateTimeField(MultiValueField):
if data_list:
# Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False).
- if data_list[0] in EMPTY_VALUES:
+ if data_list[0] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_date'])
- if data_list[1] in EMPTY_VALUES:
+ if data_list[1] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_time'])
return datetime.datetime.combine(*data_list)
return None
-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}$')
-class IPAddressField(RegexField):
+class IPAddressField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid IPv4 address.'),
}
+ default_validators = [validators.validate_ipv4_address]
- def __init__(self, *args, **kwargs):
- super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs)
-
-slug_re = re.compile(r'^[-\w]+$')
-class SlugField(RegexField):
+class SlugField(CharField):
default_error_messages = {
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
u" underscores or hyphens."),
}
-
- def __init__(self, *args, **kwargs):
- super(SlugField, self).__init__(slug_re, *args, **kwargs)
+ default_validators = [validators.validate_slug]
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 7f6fa51287..d484300a0d 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -2,6 +2,7 @@
Form classes
"""
+from django.core.exceptions import ValidationError
from django.utils.copycompat import deepcopy
from django.utils.datastructures import SortedDict
from django.utils.html import conditional_escape
@@ -10,7 +11,7 @@ from django.utils.safestring import mark_safe
from fields import Field, FileField
from widgets import Media, media_property, TextInput, Textarea
-from util import flatatt, ErrorDict, ErrorList, ValidationError
+from util import flatatt, ErrorDict, ErrorList
__all__ = ('BaseForm', 'Form')
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index bb8e3107e1..d5101c762e 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -1,10 +1,11 @@
from forms import Form
+from django.core.exceptions import ValidationError
from django.utils.encoding import StrAndUnicode
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from fields import IntegerField, BooleanField
from widgets import Media, HiddenInput
-from util import ErrorList, ErrorDict, ValidationError
+from util import ErrorList
__all__ = ('BaseFormSet', 'all_valid')
diff --git a/django/forms/models.py b/django/forms/models.py
index 1c5f446c2b..ff20c936cb 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -9,10 +9,12 @@ from django.utils.datastructures import SortedDict
from django.utils.text import get_text_list, capfirst
from django.utils.translation import ugettext_lazy as _, ugettext
-from util import ValidationError, ErrorList
-from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS
-from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
-from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError
+from django.core.validators import EMPTY_VALUES
+from util import ErrorList
+from forms import BaseForm, get_declared_fields
+from fields import Field, ChoiceField
+from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput
from widgets import media_property
from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
@@ -27,20 +29,15 @@ __all__ = (
'ModelMultipleChoiceField',
)
-
-def save_instance(form, instance, fields=None, fail_message='saved',
- commit=True, exclude=None):
+def construct_instance(form, instance, fields=None, exclude=None):
"""
- Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
-
- If commit=True, then the changes to ``instance`` will be saved to the
- database. Returns ``instance``.
+ Constructs and returns a model instance from the bound ``form``'s
+ ``cleaned_data``, but does not save the returned instance to the
+ database.
"""
from django.db import models
opts = instance._meta
- if form.errors:
- raise ValueError("The %s could not be %s because the data didn't"
- " validate." % (opts.object_name, fail_message))
+
cleaned_data = form.cleaned_data
file_field_list = []
for f in opts.fields:
@@ -65,9 +62,28 @@ def save_instance(form, instance, fields=None, fail_message='saved',
for f in file_field_list:
f.save_form_data(instance, cleaned_data[f.name])
+ return instance
+
+def save_instance(form, instance, fields=None, fail_message='saved',
+ commit=True, exclude=None, construct=True):
+ """
+ Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
+
+ If commit=True, then the changes to ``instance`` will be saved to the
+ database. Returns ``instance``.
+
+ If construct=False, assume ``instance`` has already been constructed and
+ just needs to be saved.
+ """
+ if construct:
+ instance = construct_instance(form, instance, fields, exclude)
+ opts = instance._meta
+ if form.errors:
+ raise ValueError("The %s could not be %s because the data didn't"
+ " validate." % (opts.object_name, fail_message))
+
# Wrap up the saving of m2m data as a function.
def save_m2m():
- opts = instance._meta
cleaned_data = form.cleaned_data
for f in opts.many_to_many:
if fields and f.name not in fields:
@@ -120,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None):
the ``fields`` argument.
"""
# avoid a circular import
- from django.db.models.fields.related import ManyToManyField, OneToOneField
+ from django.db.models.fields.related import ManyToManyField
opts = instance._meta
data = {}
for f in opts.fields + opts.many_to_many:
@@ -218,8 +234,10 @@ class BaseModelForm(BaseForm):
# if we didn't get an instance, instantiate a new one
self.instance = opts.model()
object_data = {}
+ self.instance._adding = True
else:
self.instance = instance
+ self.instance._adding = False
object_data = model_to_dict(instance, opts.fields, opts.exclude)
# if initial was provided, it should override the values from instance
if initial is not None:
@@ -228,165 +246,31 @@ class BaseModelForm(BaseForm):
error_class, label_suffix, empty_permitted)
def clean(self):
- self.validate_unique()
- return self.cleaned_data
-
- def validate_unique(self):
- unique_checks, date_checks = self._get_unique_checks()
- form_errors = []
- bad_fields = set()
-
- field_errors, global_errors = self._perform_unique_checks(unique_checks)
- bad_fields.union(field_errors)
- form_errors.extend(global_errors)
-
- field_errors, global_errors = self._perform_date_checks(date_checks)
- bad_fields.union(field_errors)
- form_errors.extend(global_errors)
-
- for field_name in bad_fields:
- del self.cleaned_data[field_name]
- if form_errors:
- # Raise the unique together errors since they are considered
- # form-wide.
- raise ValidationError(form_errors)
-
- def _get_unique_checks(self):
- from django.db.models.fields import FieldDoesNotExist, Field as ModelField
-
- # Gather a list of checks to perform. We only perform unique checks
- # for fields present and not None in cleaned_data. Since this is a
- # ModelForm, some fields may have been excluded; we can't perform a unique
- # check on a form that is missing fields involved in that check. It also does
- # not make sense to check data that didn't validate, and since NULL does not
- # equal NULL in SQL we should not do any unique checking for NULL values.
- unique_checks = []
- # these are checks for the unique_for_<date/year/month>
- date_checks = []
- for check in self.instance._meta.unique_together[:]:
- fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None]
- if len(fields_on_form) == len(check):
- unique_checks.append(check)
-
- # Gather a list of checks for fields declared as unique and add them to
- # the list of checks. Again, skip empty fields and any that did not validate.
- for name in self.fields:
- try:
- f = self.instance._meta.get_field_by_name(name)[0]
- except FieldDoesNotExist:
- # This is an extra field that's not on the ModelForm, ignore it
- continue
- if not isinstance(f, ModelField):
- # This is an extra field that happens to have a name that matches,
- # for example, a related object accessor for this model. So
- # get_field_by_name found it, but it is not a Field so do not proceed
- # to use it as if it were.
- continue
- if self.cleaned_data.get(name) is None:
- continue
- if f.unique:
- unique_checks.append((name,))
- if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None:
- date_checks.append(('date', name, f.unique_for_date))
- if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None:
- date_checks.append(('year', name, f.unique_for_year))
- if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None:
- date_checks.append(('month', name, f.unique_for_month))
- return unique_checks, date_checks
-
-
- def _perform_unique_checks(self, unique_checks):
- bad_fields = set()
- form_errors = []
-
- for unique_check in unique_checks:
- # Try to look up an existing object with the same values as this
- # object's values for all the unique field.
-
- lookup_kwargs = {}
- for field_name in unique_check:
- lookup_value = self.cleaned_data[field_name]
- # ModelChoiceField will return an object instance rather than
- # a raw primary key value, so convert it to a pk value before
- # using it in a lookup.
- if isinstance(self.fields[field_name], ModelChoiceField):
- lookup_value = lookup_value.pk
- lookup_kwargs[str(field_name)] = lookup_value
-
- qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
-
- # Exclude the current object from the query if we are editing an
- # instance (as opposed to creating a new one)
- if self.instance.pk is not None:
- qs = qs.exclude(pk=self.instance.pk)
-
- if qs.exists():
- if len(unique_check) == 1:
- self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)])
- else:
- form_errors.append(self.unique_error_message(unique_check))
-
- # Mark these fields as needing to be removed from cleaned data
- # later.
- for field_name in unique_check:
- bad_fields.add(field_name)
- return bad_fields, form_errors
-
- def _perform_date_checks(self, date_checks):
- bad_fields = set()
- for lookup_type, field, unique_for in date_checks:
- lookup_kwargs = {}
- # there's a ticket to add a date lookup, we can remove this special
- # case if that makes it's way in
- if lookup_type == 'date':
- date = self.cleaned_data[unique_for]
- lookup_kwargs['%s__day' % unique_for] = date.day
- lookup_kwargs['%s__month' % unique_for] = date.month
- lookup_kwargs['%s__year' % unique_for] = date.year
- else:
- lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type)
- lookup_kwargs[field] = self.cleaned_data[field]
+ opts = self._meta
+ self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
+ try:
+ self.instance.full_validate(exclude=self._errors.keys())
+ except ValidationError, e:
+ for k, v in e.message_dict.items():
+ if k != NON_FIELD_ERRORS:
+ self._errors.setdefault(k, ErrorList()).extend(v)
- qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
- # Exclude the current object from the query if we are editing an
- # instance (as opposed to creating a new one)
- if self.instance.pk is not None:
- qs = qs.exclude(pk=self.instance.pk)
+ # Remove the data from the cleaned_data dict since it was invalid
+ if k in self.cleaned_data:
+ del self.cleaned_data[k]
- if qs.exists():
- self._errors[field] = ErrorList([
- self.date_error_message(lookup_type, field, unique_for)
- ])
- bad_fields.add(field)
- return bad_fields, []
+ if NON_FIELD_ERRORS in e.message_dict:
+ raise ValidationError(e.message_dict[NON_FIELD_ERRORS])
- def date_error_message(self, lookup_type, field, unique_for):
- return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
- 'field_name': unicode(self.fields[field].label),
- 'date_field': unicode(self.fields[unique_for].label),
- 'lookup': lookup_type,
- }
+ # If model validation threw errors for fields that aren't on the
+ # form, the the errors cannot be corrected by the user. Displaying
+ # those errors would be pointless, so raise another type of
+ # exception that *won't* be caught and displayed by the form.
+ if set(e.message_dict.keys()) - set(self.fields.keys() + [NON_FIELD_ERRORS]):
+ raise UnresolvableValidationError(e.message_dict)
- def unique_error_message(self, unique_check):
- model_name = capfirst(self.instance._meta.verbose_name)
- # A unique field
- if len(unique_check) == 1:
- field_name = unique_check[0]
- field_label = self.fields[field_name].label
- # Insert the error into the error dict, very sneaky
- return _(u"%(model_name)s with this %(field_label)s already exists.") % {
- 'model_name': unicode(model_name),
- 'field_label': unicode(field_label)
- }
- # unique_together
- else:
- field_labels = [self.fields[field_name].label for field_name in unique_check]
- field_labels = get_text_list(field_labels, _('and'))
- return _(u"%(model_name)s with this %(field_label)s already exists.") % {
- 'model_name': unicode(model_name),
- 'field_label': unicode(field_labels)
- }
+ return self.cleaned_data
def save(self, commit=True):
"""
@@ -401,7 +285,7 @@ class BaseModelForm(BaseForm):
else:
fail_message = 'changed'
return save_instance(self, self.instance, self._meta.fields,
- fail_message, commit, exclude=self._meta.exclude)
+ fail_message, commit, construct=False)
save.alters_data = True
@@ -530,7 +414,7 @@ class BaseModelFormSet(BaseFormSet):
break
else:
return
- unique_checks, date_checks = form._get_unique_checks()
+ unique_checks, date_checks = form.instance._get_unique_checks()
errors = []
# Do each of the unique checks (unique and unique_together)
for unique_check in unique_checks:
@@ -743,6 +627,9 @@ class BaseInlineFormSet(BaseModelFormSet):
# Remove the foreign key from the form's data
form.data[form.add_prefix(self.fk.name)] = None
+
+ # Set the fk value here so that the form can do it's validation.
+ setattr(form.instance, self.fk.get_attname(), self.instance.pk)
return form
#@classmethod
diff --git a/django/forms/util.py b/django/forms/util.py
index b9b88a61e6..1a1d823495 100644
--- a/django/forms/util.py
+++ b/django/forms/util.py
@@ -1,7 +1,11 @@
from django.utils.html import conditional_escape
-from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode
+from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe
+# Import ValidationError so that it can be imported from this
+# module to maintain backwards compatibility.
+from django.core.exceptions import ValidationError
+
def flatatt(attrs):
"""
Convert a dictionary of attributes to a single string.
@@ -48,21 +52,3 @@ class ErrorList(list, StrAndUnicode):
def __repr__(self):
return repr([force_unicode(e) for e in self])
-class ValidationError(Exception):
- def __init__(self, message):
- """
- ValidationError can be passed any object that can be printed (usually
- a string) or a list of objects.
- """
- if isinstance(message, list):
- self.messages = ErrorList([smart_unicode(msg) for msg in message])
- else:
- message = smart_unicode(message)
- self.messages = ErrorList([message])
-
- def __str__(self):
- # This is needed because, without a __str__(), printing an exception
- # instance would result in this:
- # AttributeError: ValidationError instance has no attribute 'args'
- # See http://www.python.org/doc/current/tut/node10.html#handling
- return repr(self.messages)