1
0
mirror of https://github.com/django/django.git synced 2025-03-29 02:30:48 +00:00

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

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12098 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Joseph Kocherhans 2010-01-05 03:56:19 +00:00
parent 4e89105d64
commit 471596fc1a
63 changed files with 1550 additions and 639 deletions

View File

@ -254,6 +254,7 @@ answer newbie questions, and generally made Django that much better:
Gasper Koren Gasper Koren
Martin Kosír <martin@martinkosir.net> Martin Kosír <martin@martinkosir.net>
Arthur Koziel <http://arthurkoziel.com> Arthur Koziel <http://arthurkoziel.com>
Honza Kral <honza.kral@gmail.com>
Meir Kriheli <http://mksoft.co.il/> Meir Kriheli <http://mksoft.co.il/>
Bruce Kroeze <http://coderseye.com/> Bruce Kroeze <http://coderseye.com/>
krzysiek.pawlik@silvermedia.pl krzysiek.pawlik@silvermedia.pl

View File

@ -578,12 +578,12 @@ class ModelAdmin(BaseModelAdmin):
""" """
messages.info(request, message) 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 Given a ModelForm return an unsaved instance. ``change`` is True if
the object is being changed, and False if it's being added. 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): def save_model(self, request, obj, form, change):
""" """
@ -757,8 +757,12 @@ class ModelAdmin(BaseModelAdmin):
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES) form = ModelForm(request.POST, request.FILES)
if form.is_valid(): 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 form_validated = True
new_object = self.save_form(request, form, change=False)
else: else:
form_validated = False form_validated = False
new_object = self.model() new_object = self.model()
@ -774,13 +778,15 @@ class ModelAdmin(BaseModelAdmin):
prefix=prefix, queryset=inline.queryset(request)) prefix=prefix, queryset=inline.queryset(request))
formsets.append(formset) formsets.append(formset)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=False)
form.save_m2m()
for formset in formsets: for formset in formsets:
self.save_formset(request, form, formset, change=False) self.save_formset(request, form, formset, change=False)
self.log_addition(request, new_object) self.log_addition(request, new_object)
return self.response_add(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: else:
# Prepare the dict of initial data from the request. # Prepare the dict of initial data from the request.
# We have to special-case M2Ms as a list of comma-separated PKs. # We have to special-case M2Ms as a list of comma-separated PKs.

View File

@ -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 import authenticate
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
@ -21,6 +21,12 @@ class UserCreationForm(forms.ModelForm):
model = User model = User
fields = ("username",) 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): def clean_username(self):
username = self.cleaned_data["username"] username = self.cleaned_data["username"]
try: try:
@ -34,15 +40,9 @@ class UserCreationForm(forms.ModelForm):
password2 = self.cleaned_data["password2"] password2 = self.cleaned_data["password2"]
if password1 != password2: if password1 != password2:
raise forms.ValidationError(_("The two password fields didn't match.")) raise forms.ValidationError(_("The two password fields didn't match."))
self.instance.set_password(password1)
return password2 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): class UserChangeForm(forms.ModelForm):
username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$', 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)."), help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),

View File

@ -297,7 +297,11 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
# Avoid a circular import. # Avoid a circular import.
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
opts = self.model._meta 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(( self.rel_name = '-'.join((
opts.app_label, opts.object_name.lower(), opts.app_label, opts.object_name.lower(),
self.ct_field.name, self.ct_fk_field.name, self.ct_field.name, self.ct_fk_field.name,
@ -324,15 +328,19 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
)) ))
get_default_prefix = classmethod(get_default_prefix) get_default_prefix = classmethod(get_default_prefix)
def save_new(self, form, commit=True): def _construct_form(self, i, **kwargs):
# Avoid a circular import. # Avoid a circular import.
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
kwargs = { form = super(BaseGenericInlineFormSet, self)._construct_form(i, **kwargs)
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk, if self.save_as_new:
self.ct_fk_field.get_attname(): self.instance.pk, # 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
new_obj = self.model(**kwargs) form.data[form.add_prefix(self.ct_field.name)] = None
return save_instance(form, new_obj, commit=commit)
# 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, def generic_inlineformset_factory(model, form=ModelForm,
formset=BaseGenericInlineFormSet, formset=BaseGenericInlineFormSet,

View File

@ -4,7 +4,8 @@ AR-specific Form helpers.
""" """
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -2,9 +2,10 @@
Australian-specific Form helpers Australian-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.forms.util import smart_unicode from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -3,8 +3,9 @@
BR-specific Form helpers BR-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -2,9 +2,10 @@
Canada-specific Form helpers Canada-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.forms.util import smart_unicode from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -2,8 +2,9 @@
Swiss-specific Form helpers Swiss-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -2,8 +2,9 @@
Chile specific form helpers. Chile specific form helpers.
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
Czech-specific form helpers Czech-specific form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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 _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -2,8 +2,9 @@
DE-specific Form helpers DE-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
import re import re

View File

@ -3,8 +3,9 @@
Spanish-specific Form helpers Spanish-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
import re import re

View File

@ -3,8 +3,9 @@ FI-specific Form helpers
""" """
import re import re
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
class FIZipCodeField(RegexField): class FIZipCodeField(RegexField):

View File

@ -2,8 +2,9 @@
FR-specific Form helpers FR-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -5,8 +5,9 @@ ID-specific Form helpers
import re import re
import time import time
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
India-specific Form helpers. India-specific Form helpers.
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import gettext from django.utils.translation import gettext
import re import re

View File

@ -2,8 +2,9 @@
Iceland specific form helpers. Iceland specific form helpers.
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.forms.widgets import Select
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
IT-specific Form helpers IT-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from django.contrib.localflavor.it.util import ssn_check_digit, vat_number_check_digit from django.contrib.localflavor.it.util import ssn_check_digit, vat_number_check_digit

View File

@ -3,8 +3,10 @@ Kuwait-specific Form helpers
""" """
import re import re
from datetime import date from datetime import date
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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 _ 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})') 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})')

View File

@ -4,8 +4,9 @@ NL-specific Form helpers
import re import re
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode

View File

@ -3,8 +3,9 @@ Norwegian-specific Form helpers
""" """
import re, datetime import re, datetime
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.translation import ugettext_lazy as _
class NOZipCodeField(RegexField): class NOZipCodeField(RegexField):

View File

@ -3,8 +3,9 @@
PE-specific Form helpers. PE-specific Form helpers.
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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 _ from django.utils.translation import ugettext_lazy as _
class PERegionSelect(Select): class PERegionSelect(Select):

View File

@ -2,8 +2,9 @@
PT-specific Form helpers PT-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -5,8 +5,8 @@ Romanian specific form helpers.
import re import re
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError, Field, RegexField, Select from django.forms import ValidationError, Field, RegexField, Select
from django.forms.fields import EMPTY_VALUES
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class ROCIFField(RegexField): class ROCIFField(RegexField):

View File

@ -5,7 +5,7 @@ Swedish specific Form helpers
import re import re
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ 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, from django.contrib.localflavor.se.utils import (id_number_checksum,
validate_id_birthday, format_personal_id_number, valid_organisation, validate_id_birthday, format_personal_id_number, valid_organisation,
format_organisation_number) format_organisation_number)

View File

@ -2,8 +2,9 @@
USA-specific Form helpers USA-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import re import re

View File

@ -4,7 +4,8 @@ UY-specific form helpers.
""" """
import re 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.forms import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.localflavor.uy.util import get_validation_digit from django.contrib.localflavor.uy.util import get_validation_digit

View File

@ -2,8 +2,9 @@
South Africa-specific Form helpers South Africa-specific Form helpers
""" """
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError 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.checksums import luhn
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
import re import re

View File

@ -32,6 +32,42 @@ class FieldError(Exception):
"""Some kind of problem with a model field.""" """Some kind of problem with a model field."""
pass pass
class ValidationError(Exception): NON_FIELD_ERRORS = '__all__'
class BaseValidationError(Exception):
"""An error while validating data.""" """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 pass
class UnresolvableValidationError(BaseValidationError):
"""Validation error that cannot be resolved by the user."""
pass

137
django/core/validators.py Normal file
View File

@ -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'

View File

@ -3,7 +3,8 @@ import sys
import os import os
from itertools import izip from itertools import izip
import django.db.models.manager # Imported to register signal handler. 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 import AutoField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q 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 import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS
from django.db.models import signals from django.db.models import signals
from django.db.models.loading import register_models, get_model from django.db.models.loading import register_models, get_model
from django.utils.translation import ugettext_lazy as _
import django.utils.copycompat as copy import django.utils.copycompat as copy
from django.utils.functional import curry from django.utils.functional import curry
from django.utils.encoding import smart_str, force_unicode, smart_unicode 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 from django.conf import settings
class ModelBase(type): class ModelBase(type):
@ -639,6 +642,180 @@ class Model(object):
def prepare_database_save(self, unused): def prepare_database_save(self, unused):
return self.pk 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) # # HELPER FUNCTIONS (CURRIED MODEL METHODS) #

View File

@ -13,12 +13,12 @@ from django.db.models.query_utils import QueryWrapper
from django.dispatch import dispatcher from django.dispatch import dispatcher
from django.conf import settings from django.conf import settings
from django import forms from django import forms
from django.core import exceptions from django.core import exceptions, validators
from django.utils.datastructures import DictWrapper from django.utils.datastructures import DictWrapper
from django.utils.functional import curry from django.utils.functional import curry
from django.utils.itercompat import tee from django.utils.itercompat import tee
from django.utils.text import capfirst 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.encoding import smart_unicode, force_unicode, smart_str
from django.utils import datetime_safe from django.utils import datetime_safe
@ -60,6 +60,12 @@ class Field(object):
# creates, creation_counter is used for all user-specified fields. # creates, creation_counter is used for all user-specified fields.
creation_counter = 0 creation_counter = 0
auto_creation_counter = -1 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 # Generic field type description, usually overriden by subclasses
def _description(self): def _description(self):
@ -73,7 +79,8 @@ class Field(object):
db_index=False, rel=None, default=NOT_PROVIDED, editable=True, db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
serialize=True, unique_for_date=None, unique_for_month=None, serialize=True, unique_for_date=None, unique_for_month=None,
unique_for_year=None, choices=None, help_text='', db_column=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.name = name
self.verbose_name = verbose_name self.verbose_name = verbose_name
self.primary_key = primary_key self.primary_key = primary_key
@ -106,6 +113,42 @@ class Field(object):
self.creation_counter = Field.creation_counter self.creation_counter = Field.creation_counter
Field.creation_counter += 1 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): def __cmp__(self, other):
# This is needed because bisect does not take a comparison function. # This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter) return cmp(self.creation_counter, other.creation_counter)
@ -127,6 +170,54 @@ class Field(object):
""" """
return value 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): def db_type(self, connection):
""" """
Returns the database column data type for this field, for the provided Returns the database column data type for this field, for the provided
@ -377,9 +468,12 @@ class Field(object):
return getattr(obj, self.attname) return getattr(obj, self.attname)
class AutoField(Field): class AutoField(Field):
description = ugettext_lazy("Integer") description = _("Integer")
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = {
'invalid': _(u'This value must be an integer.'),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__ assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
kwargs['blank'] = True kwargs['blank'] = True
@ -391,8 +485,10 @@ class AutoField(Field):
try: try:
return int(value) return int(value)
except (TypeError, ValueError): except (TypeError, ValueError):
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be an integer."))
def validate(self, value, model_instance):
pass
def get_prep_value(self, value): def get_prep_value(self, value):
if value is None: if value is None:
@ -410,7 +506,10 @@ class AutoField(Field):
class BooleanField(Field): class BooleanField(Field):
empty_strings_allowed = False 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): def __init__(self, *args, **kwargs):
kwargs['blank'] = True kwargs['blank'] = True
if 'default' not in kwargs and not kwargs.get('null'): 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 (True, False): return value
if value in ('t', 'True', '1'): return True if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False if value in ('f', 'False', '0'): return False
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be either True or False."))
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the # 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) return super(BooleanField, self).formfield(**defaults)
class CharField(Field): 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): def get_internal_type(self):
return "CharField" return "CharField"
def to_python(self, value): def to_python(self, value):
if isinstance(value, basestring): if isinstance(value, basestring) or value is None:
return value 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) return smart_unicode(value)
def formfield(self, **kwargs): 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 = {'max_length': self.max_length}
defaults.update(kwargs) defaults.update(kwargs)
return super(CharField, self).formfield(**defaults) return super(CharField, self).formfield(**defaults)
# TODO: Maybe move this into contrib, because it's specialized. # TODO: Maybe move this into contrib, because it's specialized.
class CommaSeparatedIntegerField(CharField): 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): def formfield(self, **kwargs):
defaults = { defaults = {
'form_class': forms.RegexField,
'regex': '^[\d,]+$',
'max_length': self.max_length,
'error_messages': { 'error_messages': {
'invalid': _(u'Enter only digits separated by commas.'), '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}$') ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
class DateField(Field): class DateField(Field):
description = ugettext_lazy("Date (without time)") description = _("Date (without time)")
empty_strings_allowed = False 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): 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 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. #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 return value
if not ansi_date_re.search(value): if not ansi_date_re.search(value):
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_('Enter a valid date in YYYY-MM-DD format.'))
# Now that we have the date string in YYYY-MM-DD format, check to make # Now that we have the date string in YYYY-MM-DD format, check to make
# sure it's a valid date. # sure it's a valid date.
# We could use time.strptime here and catch errors, but datetime.date # We could use time.strptime here and catch errors, but datetime.date
@ -526,7 +626,7 @@ class DateField(Field):
try: try:
return datetime.date(year, month, day) return datetime.date(year, month, day)
except ValueError, e: except ValueError, e:
msg = _('Invalid date: %s') % _(str(e)) msg = self.error_messages['invalid_date'] % _(str(e))
raise exceptions.ValidationError(msg) raise exceptions.ValidationError(msg)
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
@ -575,7 +675,10 @@ class DateField(Field):
return super(DateField, self).formfield(**defaults) return super(DateField, self).formfield(**defaults)
class DateTimeField(DateField): 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): def get_internal_type(self):
return "DateTimeField" return "DateTimeField"
@ -596,8 +699,7 @@ class DateTimeField(DateField):
value, usecs = value.split('.') value, usecs = value.split('.')
usecs = int(usecs) usecs = int(usecs)
except ValueError: except ValueError:
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
else: else:
usecs = 0 usecs = 0
kwargs = {'microsecond': usecs} kwargs = {'microsecond': usecs}
@ -614,8 +716,7 @@ class DateTimeField(DateField):
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3], return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3],
**kwargs) **kwargs)
except ValueError: except ValueError:
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
def get_prep_value(self, value): def get_prep_value(self, value):
return self.to_python(value) return self.to_python(value)
@ -642,7 +743,11 @@ class DateTimeField(DateField):
class DecimalField(Field): class DecimalField(Field):
empty_strings_allowed = False 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): 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 self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, verbose_name, name, **kwargs) Field.__init__(self, verbose_name, name, **kwargs)
@ -656,8 +761,7 @@ class DecimalField(Field):
try: try:
return decimal.Decimal(value) return decimal.Decimal(value)
except decimal.InvalidOperation: except decimal.InvalidOperation:
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be a decimal number."))
def _format(self, value): def _format(self, value):
if isinstance(value, basestring) or value is None: if isinstance(value, basestring) or value is None:
@ -696,18 +800,15 @@ class DecimalField(Field):
return super(DecimalField, self).formfield(**defaults) return super(DecimalField, self).formfield(**defaults)
class EmailField(CharField): class EmailField(CharField):
description = ugettext_lazy("E-mail address") default_validators = [validators.validate_email]
description = _("E-mail address")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 75) kwargs['max_length'] = kwargs.get('max_length', 75)
CharField.__init__(self, *args, **kwargs) 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): 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): def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive self.path, self.match, self.recursive = path, match, recursive
@ -729,7 +830,10 @@ class FilePathField(Field):
class FloatField(Field): class FloatField(Field):
empty_strings_allowed = False 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): def get_prep_value(self, value):
if value is None: if value is None:
@ -745,8 +849,7 @@ class FloatField(Field):
try: try:
return float(value) return float(value)
except (TypeError, ValueError): except (TypeError, ValueError):
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be a float."))
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField} defaults = {'form_class': forms.FloatField}
@ -755,7 +858,10 @@ class FloatField(Field):
class IntegerField(Field): class IntegerField(Field):
empty_strings_allowed = False 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): def get_prep_value(self, value):
if value is None: if value is None:
@ -771,8 +877,7 @@ class IntegerField(Field):
try: try:
return int(value) return int(value)
except (TypeError, ValueError): except (TypeError, ValueError):
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be an integer."))
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField} defaults = {'form_class': forms.IntegerField}
@ -781,7 +886,7 @@ class IntegerField(Field):
class BigIntegerField(IntegerField): class BigIntegerField(IntegerField):
empty_strings_allowed = False empty_strings_allowed = False
description = ugettext_lazy("Big (8 byte) integer") description = _("Big (8 byte) integer")
MAX_BIGINT = 9223372036854775807 MAX_BIGINT = 9223372036854775807
def get_internal_type(self): def get_internal_type(self):
return "BigIntegerField" return "BigIntegerField"
@ -794,7 +899,7 @@ class BigIntegerField(IntegerField):
class IPAddressField(Field): class IPAddressField(Field):
empty_strings_allowed = False empty_strings_allowed = False
description = ugettext_lazy("IP address") description = _("IP address")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15 kwargs['max_length'] = 15
Field.__init__(self, *args, **kwargs) Field.__init__(self, *args, **kwargs)
@ -809,7 +914,11 @@ class IPAddressField(Field):
class NullBooleanField(Field): class NullBooleanField(Field):
empty_strings_allowed = False 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): def __init__(self, *args, **kwargs):
kwargs['null'] = True kwargs['null'] = True
Field.__init__(self, *args, **kwargs) Field.__init__(self, *args, **kwargs)
@ -822,8 +931,7 @@ class NullBooleanField(Field):
if value in ('None',): return None if value in ('None',): return None
if value in ('t', 'True', '1'): return True if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False if value in ('f', 'False', '0'): return False
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_("This value must be either None, True or False."))
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the # 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) return super(NullBooleanField, self).formfield(**defaults)
class PositiveIntegerField(IntegerField): class PositiveIntegerField(IntegerField):
description = ugettext_lazy("Integer") description = _("Integer")
def get_internal_type(self): def get_internal_type(self):
return "PositiveIntegerField" return "PositiveIntegerField"
@ -860,7 +968,7 @@ class PositiveIntegerField(IntegerField):
return super(PositiveIntegerField, self).formfield(**defaults) return super(PositiveIntegerField, self).formfield(**defaults)
class PositiveSmallIntegerField(IntegerField): class PositiveSmallIntegerField(IntegerField):
description = ugettext_lazy("Integer") description = _("Integer")
def get_internal_type(self): def get_internal_type(self):
return "PositiveSmallIntegerField" return "PositiveSmallIntegerField"
@ -870,7 +978,7 @@ class PositiveSmallIntegerField(IntegerField):
return super(PositiveSmallIntegerField, self).formfield(**defaults) return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField): class SlugField(CharField):
description = ugettext_lazy("String (up to %(max_length)s)") description = _("String (up to %(max_length)s)")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50) kwargs['max_length'] = kwargs.get('max_length', 50)
# Set db_index=True unless it's been set manually. # Set db_index=True unless it's been set manually.
@ -887,13 +995,13 @@ class SlugField(CharField):
return super(SlugField, self).formfield(**defaults) return super(SlugField, self).formfield(**defaults)
class SmallIntegerField(IntegerField): class SmallIntegerField(IntegerField):
description = ugettext_lazy("Integer") description = _("Integer")
def get_internal_type(self): def get_internal_type(self):
return "SmallIntegerField" return "SmallIntegerField"
class TextField(Field): class TextField(Field):
description = ugettext_lazy("Text") description = _("Text")
def get_internal_type(self): def get_internal_type(self):
return "TextField" return "TextField"
@ -904,9 +1012,12 @@ class TextField(Field):
return super(TextField, self).formfield(**defaults) return super(TextField, self).formfield(**defaults)
class TimeField(Field): class TimeField(Field):
description = ugettext_lazy("Time") description = _("Time")
empty_strings_allowed = False 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): 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 self.auto_now, self.auto_now_add = auto_now, auto_now_add
if auto_now or auto_now_add: if auto_now or auto_now_add:
@ -935,8 +1046,7 @@ class TimeField(Field):
value, usecs = value.split('.') value, usecs = value.split('.')
usecs = int(usecs) usecs = int(usecs)
except ValueError: except ValueError:
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
else: else:
usecs = 0 usecs = 0
kwargs = {'microsecond': usecs} kwargs = {'microsecond': usecs}
@ -949,8 +1059,7 @@ class TimeField(Field):
return datetime.time(*time.strptime(value, '%H:%M')[3:5], return datetime.time(*time.strptime(value, '%H:%M')[3:5],
**kwargs) **kwargs)
except ValueError: except ValueError:
raise exceptions.ValidationError( raise exceptions.ValidationError(self.error_messages['invalid'])
_('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and 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) return super(TimeField, self).formfield(**defaults)
class URLField(CharField): class URLField(CharField):
description = ugettext_lazy("URL") description = _("URL")
def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200) kwargs['max_length'] = kwargs.get('max_length', 200)
self.verify_exists = verify_exists
CharField.__init__(self, verbose_name, name, **kwargs) CharField.__init__(self, verbose_name, name, **kwargs)
self.validators.append(validators.URLValidator(verify_exists=verify_exists))
def formfield(self, **kwargs):
defaults = {'form_class': forms.URLField, 'verify_exists': self.verify_exists}
defaults.update(kwargs)
return super(URLField, self).formfield(**defaults)
class XMLField(TextField): class XMLField(TextField):
description = ugettext_lazy("XML text") description = _("XML text")
def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
self.schema_path = schema_path self.schema_path = schema_path
Field.__init__(self, verbose_name, name, **kwargs) Field.__init__(self, verbose_name, name, **kwargs)

View File

@ -7,7 +7,7 @@ from django.db.models.related import RelatedObject
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models.query_utils import QueryWrapper from django.db.models.query_utils import QueryWrapper
from django.utils.encoding import smart_unicode 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.utils.functional import curry
from django.core import exceptions from django.core import exceptions
from django import forms from django import forms
@ -473,7 +473,7 @@ def create_many_related_manager(superclass, rel=False):
if not rel.through._meta.auto_created: if not rel.through._meta.auto_created:
opts = through._meta 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) 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) self.add(new_obj)
return new_obj return new_obj
create.alters_data = True create.alters_data = True
@ -708,7 +708,10 @@ class ManyToManyRel(object):
class ForeignKey(RelatedField, Field): class ForeignKey(RelatedField, Field):
empty_strings_allowed = False 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): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try: try:
to_name = to._meta.object_name.lower() to_name = to._meta.object_name.lower()
@ -731,6 +734,18 @@ class ForeignKey(RelatedField, Field):
self.db_index = True 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): def get_attname(self):
return '%s_id' % self.name 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), always returns the object pointed to (since there will only ever be one),
rather than returning a list. 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): def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True kwargs['unique'] = True
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@ -826,6 +841,12 @@ class OneToOneField(ForeignKey):
return None return None
return super(OneToOneField, self).formfield(**kwargs) 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): def create_many_to_many_intermediary_model(field, klass):
from django.db import models from django.db import models
managed = True managed = True
@ -866,7 +887,7 @@ def create_many_to_many_intermediary_model(field, klass):
}) })
class ManyToManyField(RelatedField, Field): class ManyToManyField(RelatedField, Field):
description = ugettext_lazy("Many-to-many relationship") description = _("Many-to-many relationship")
def __init__(self, to, **kwargs): def __init__(self, to, **kwargs):
try: try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) 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) 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) self.help_text = string_concat(self.help_text, ' ', msg)
def get_choices_default(self): def get_choices_default(self):

View File

@ -10,7 +10,7 @@ TODO:
"This form field requires foo.js" and form.js_includes() "This form field requires foo.js" and form.js_includes()
""" """
from util import ValidationError from django.core.exceptions import ValidationError
from widgets import * from widgets import *
from fields import * from fields import *
from forms import * from forms import *

View File

@ -14,15 +14,21 @@ try:
except ImportError: except ImportError:
from StringIO import StringIO 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 import django.utils.copycompat as copy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str from django.utils.encoding import smart_unicode, smart_str
from django.utils.formats import get_format from django.utils.formats import get_format
from django.utils.functional import lazy from django.utils.functional import lazy
from util import ErrorList, ValidationError # Provide this import for backwards compatibility.
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget 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__ = ( __all__ = (
'Field', 'CharField', 'IntegerField', 'Field', 'CharField', 'IntegerField',
@ -36,9 +42,6 @@ __all__ = (
'TypedChoiceField' 'TypedChoiceField'
) )
# These values, if given to to_python(), will trigger the self.required check.
EMPTY_VALUES = (None, '')
def en_format(name): def en_format(name):
""" """
Helper function to stay backward compatible. Helper function to stay backward compatible.
@ -57,6 +60,7 @@ DEFAULT_DATETIME_INPUT_FORMATS = lazy(lambda: en_format('DATETIME_INPUT_FORMATS'
class Field(object): class Field(object):
widget = TextInput # Default widget to use when rendering this type of Field. widget = TextInput # Default widget to use when rendering this type of Field.
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
default_validators = [] # Default set of validators
default_error_messages = { default_error_messages = {
'required': _(u'This field is required.'), 'required': _(u'This field is required.'),
'invalid': _(u'Enter a valid value.'), 'invalid': _(u'Enter a valid value.'),
@ -66,7 +70,8 @@ class Field(object):
creation_counter = 0 creation_counter = 0
def __init__(self, required=True, widget=None, label=None, initial=None, 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. # required -- Boolean that specifies whether the field is required.
# True by default. # True by default.
# widget -- A Widget class, or instance of a Widget class, that should # 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. # 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 # show_hidden_initial -- Boolean that specifies if it is needed to render a
# hidden widget with initial value after widget. # hidden widget with initial value after widget.
# validators -- List of addtional validators to use
if label is not None: if label is not None:
label = smart_unicode(label) label = smart_unicode(label)
self.required, self.label, self.initial = required, label, initial self.required, self.label, self.initial = required, label, initial
@ -105,16 +111,39 @@ class Field(object):
self.creation_counter = Field.creation_counter self.creation_counter = Field.creation_counter
Field.creation_counter += 1 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 = {} 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 {}) messages.update(error_messages or {})
self.error_messages = messages 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): def clean(self, value):
""" """
Validates the given value and returns its "cleaned" value as an Validates the given value and returns its "cleaned" value as an
@ -122,8 +151,9 @@ class Field(object):
Raises ValidationError for any errors. Raises ValidationError for any errors.
""" """
if self.required and value in EMPTY_VALUES: value = self.to_python(value)
raise ValidationError(self.error_messages['required']) self.validate(value)
self.run_validators(value)
return value return value
def widget_attrs(self, widget): def widget_attrs(self, widget):
@ -141,27 +171,19 @@ class Field(object):
return result return result
class CharField(Field): 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): def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length self.max_length, self.min_length = max_length, min_length
super(CharField, self).__init__(*args, **kwargs) 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): def to_python(self, value):
"Validates max_length and min_length. Returns a Unicode object." "Returns a Unicode object."
super(CharField, self).clean(value) if value in validators.EMPTY_VALUES:
if value in EMPTY_VALUES:
return u'' return u''
value = smart_unicode(value) return 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
def widget_attrs(self, widget): def widget_attrs(self, widget):
if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
@ -171,87 +193,82 @@ class CharField(Field):
class IntegerField(Field): class IntegerField(Field):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a whole number.'), 'invalid': _(u'Enter a whole number.'),
'max_value': _(u'Ensure this value is less 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 %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): 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) 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 Validates that int() can be called on the input. Returns the result
of int(). Returns None for empty values. of int(). Returns None for empty values.
""" """
super(IntegerField, self).clean(value) value = super(IntegerField, self).to_python(value)
if value in EMPTY_VALUES: if value in validators.EMPTY_VALUES:
return None return None
try: try:
value = int(str(value)) value = int(str(value))
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid']) 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 return value
class FloatField(Field): class FloatField(IntegerField):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a number.'), '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): def to_python(self, value):
self.max_value, self.min_value = max_value, min_value
Field.__init__(self, *args, **kwargs)
def clean(self, value):
""" """
Validates that float() can be called on the input. Returns a float. Validates that float() can be called on the input. Returns the result
Returns None for empty values. of float(). Returns None for empty values.
""" """
super(FloatField, self).clean(value) value = super(IntegerField, self).to_python(value)
if not self.required and value in EMPTY_VALUES: if value in validators.EMPTY_VALUES:
return None return None
try: try:
# We always accept dot as decimal separator # We always accept dot as decimal separator
if isinstance(value, str) or isinstance(value, unicode): if isinstance(value, str) or isinstance(value, unicode):
value = float(value.replace(get_format('DECIMAL_SEPARATOR'), '.')) value = float(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid']) 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 return value
class DecimalField(Field): class DecimalField(Field):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a number.'), 'invalid': _(u'Enter a number.'),
'max_value': _(u'Ensure this value is less 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 %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_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_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.') '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): 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 self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, *args, **kwargs) 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 Validates that the input is a decimal number. Returns a Decimal
instance. Returns None for empty values. Ensures that there are no more 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 than max_digits in the number, and no more than decimal_places digits
after the decimal point. after the decimal point.
""" """
super(DecimalField, self).clean(value) if value in validators.EMPTY_VALUES:
if not self.required and value in EMPTY_VALUES:
return None return None
value = smart_str(value).strip() value = smart_str(value).strip()
try: try:
@ -260,7 +277,12 @@ class DecimalField(Field):
value = Decimal(value.replace(get_format('DECIMAL_SEPARATOR'), '.')) value = Decimal(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except DecimalException: except DecimalException:
raise ValidationError(self.error_messages['invalid']) 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() sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent) decimals = abs(exponent)
# digittuple doesn't include any leading zeros. # digittuple doesn't include any leading zeros.
@ -273,10 +295,6 @@ class DecimalField(Field):
digits = decimals digits = decimals
whole_digits = 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: if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_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: 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) super(DateField, self).__init__(*args, **kwargs)
self.input_formats = input_formats 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 Validates that the input can be converted to a date. Returns a Python
datetime.date object. datetime.date object.
""" """
super(DateField, self).clean(value) if value in validators.EMPTY_VALUES:
if value in EMPTY_VALUES:
return None return None
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return value.date() return value.date()
@ -324,13 +341,12 @@ class TimeField(Field):
super(TimeField, self).__init__(*args, **kwargs) super(TimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats 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 Validates that the input can be converted to a time. Returns a Python
datetime.time object. datetime.time object.
""" """
super(TimeField, self).clean(value) if value in validators.EMPTY_VALUES:
if value in EMPTY_VALUES:
return None return None
if isinstance(value, datetime.time): if isinstance(value, datetime.time):
return value return value
@ -351,13 +367,12 @@ class DateTimeField(Field):
super(DateTimeField, self).__init__(*args, **kwargs) super(DateTimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats 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 Validates that the input can be converted to a datetime. Returns a
Python datetime.datetime object. Python datetime.datetime object.
""" """
super(DateTimeField, self).clean(value) if value in validators.EMPTY_VALUES:
if value in EMPTY_VALUES:
return None return None
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return value return value
@ -392,40 +407,13 @@ class RegexField(CharField):
if isinstance(regex, basestring): if isinstance(regex, basestring):
regex = re.compile(regex) regex = re.compile(regex)
self.regex = regex self.regex = regex
self.validators.append(validators.RegexValidator(regex=regex))
def clean(self, value): class EmailField(CharField):
"""
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):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a valid e-mail address.'), 'invalid': _(u'Enter a valid e-mail address.'),
} }
default_validators = [validators.validate_email]
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/)'
class FileField(Field): class FileField(Field):
widget = FileInput widget = FileInput
@ -440,12 +428,9 @@ class FileField(Field):
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
super(FileField, self).__init__(*args, **kwargs) super(FileField, self).__init__(*args, **kwargs)
def clean(self, data, initial=None): def to_python(self, data):
super(FileField, self).clean(initial or data) if data in validators.EMPTY_VALUES:
if not self.required and data in EMPTY_VALUES:
return None return None
elif not data and initial:
return initial
# UploadedFile objects should have name and size attributes. # UploadedFile objects should have name and size attributes.
try: try:
@ -464,21 +449,24 @@ class FileField(Field):
return data return data
def clean(self, data, initial=None):
if not data and initial:
return initial
return super(FileField, self).clean(data)
class ImageField(FileField): class ImageField(FileField):
default_error_messages = { default_error_messages = {
'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), '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, Checks that the file-upload field data contains a valid image (GIF, JPG,
PNG, possibly others -- whatever the Python Imaging Library supports). 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: if f is None:
return None return None
elif not data and initial:
return initial
from PIL import Image from PIL import Image
# We need to get a file object for PIL. We might have a path or we might # 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) f.seek(0)
return f return f
url_re = re.compile( class URLField(CharField):
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):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a valid URL.'), 'invalid': _(u'Enter a valid URL.'),
'invalid_link': _(u'This URL appears to be a broken link.'), 'invalid_link': _(u'This URL appears to be a broken link.'),
} }
def __init__(self, max_length=None, min_length=None, verify_exists=False, def __init__(self, max_length=None, min_length=None, verify_exists=False,
validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): validator_user_agent=validators.URL_VALIDATOR_USER_AGENT, *args, **kwargs):
super(URLField, self).__init__(url_re, max_length, min_length, *args, super(URLField, self).__init__(max_length, min_length, *args,
**kwargs) **kwargs)
self.verify_exists = verify_exists self.validators.append(validators.URLValidator(verify_exists=verify_exists, validator_user_agent=validator_user_agent))
self.user_agent = validator_user_agent
def clean(self, value): def to_python(self, value):
# If no URL scheme given, assume http:// if value:
if value and '://' not in value: if '://' not in value:
value = u'http://%s' % value # If no URL scheme given, assume http://
# If no URL path given, assume / value = u'http://%s' % value
if value and not urlparse.urlsplit(value)[2]: url_fields = list(urlparse.urlsplit(value))
value += '/' if not url_fields[2]:
value = super(URLField, self).clean(value) # the path portion may need to be added before query params
if value == u'': url_fields[2] = '/'
return value value = urlparse.urlunsplit(url_fields)
if self.verify_exists: return super(URLField, self).to_python(value)
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
class BooleanField(Field): class BooleanField(Field):
widget = CheckboxInput widget = CheckboxInput
def clean(self, value): def to_python(self, value):
"""Returns a Python boolean object.""" """Returns a Python boolean object."""
# Explicitly check for the string 'False', which is what a hidden field # Explicitly check for the string 'False', which is what a hidden field
# will submit for False. Also check for '0', since this is what # will submit for False. Also check for '0', since this is what
@ -579,7 +542,7 @@ class BooleanField(Field):
value = False value = False
else: else:
value = bool(value) value = bool(value)
super(BooleanField, self).clean(value) value = super(BooleanField, self).to_python(value)
if not value and self.required: if not value and self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
return value return value
@ -591,7 +554,7 @@ class NullBooleanField(BooleanField):
""" """
widget = NullBooleanSelect widget = NullBooleanSelect
def clean(self, value): def to_python(self, value):
""" """
Explicitly checks for the string 'True' and 'False', which is what a 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 hidden field will submit for True and False, and for '1' and '0', which
@ -605,6 +568,9 @@ class NullBooleanField(BooleanField):
else: else:
return None return None
def validate(self, value):
pass
class ChoiceField(Field): class ChoiceField(Field):
widget = Select widget = Select
default_error_messages = { default_error_messages = {
@ -613,8 +579,8 @@ class ChoiceField(Field):
def __init__(self, choices=(), required=True, widget=None, label=None, def __init__(self, choices=(), required=True, widget=None, label=None,
initial=None, help_text=None, *args, **kwargs): initial=None, help_text=None, *args, **kwargs):
super(ChoiceField, self).__init__(required, widget, label, initial, super(ChoiceField, self).__init__(required=required, widget=widget, label=label,
help_text, *args, **kwargs) initial=initial, help_text=help_text, *args, **kwargs)
self.choices = choices self.choices = choices
def _get_choices(self): def _get_choices(self):
@ -628,19 +594,19 @@ class ChoiceField(Field):
choices = property(_get_choices, _set_choices) 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. Validates that the input is in self.choices.
""" """
value = super(ChoiceField, self).clean(value) super(ChoiceField, self).validate(value)
if value in EMPTY_VALUES: if value and not self.valid_value(value):
value = u''
value = smart_unicode(value)
if value == u'':
return value
if not self.valid_value(value):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
def valid_value(self, value): def valid_value(self, value):
"Check to see if the provided value is a valid choice" "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', '') self.empty_value = kwargs.pop('empty_value', '')
super(TypedChoiceField, self).__init__(*args, **kwargs) 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 Validate that the value is in self.choices and can be coerced to the
right type. right type.
""" """
value = super(TypedChoiceField, self).clean(value) value = super(TypedChoiceField, self).to_python(value)
if value == self.empty_value or value in EMPTY_VALUES: super(TypedChoiceField, self).validate(value)
if value == self.empty_value or value in validators.EMPTY_VALUES:
return self.empty_value 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: try:
value = self.coerce(value) value = self.coerce(value)
except (ValueError, TypeError, django.core.exceptions.ValidationError): except (ValueError, TypeError, ValidationError):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value return value
def validate(self, value):
pass
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput hidden_widget = MultipleHiddenInput
widget = SelectMultiple widget = SelectMultiple
@ -690,22 +653,23 @@ class MultipleChoiceField(ChoiceField):
'invalid_list': _(u'Enter a list of values.'), '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. Validates that the input is a list or tuple.
""" """
if self.required and not value: if self.required and not value:
raise ValidationError(self.error_messages['required']) 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. # 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): if not self.valid_value(val):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
return new_value
class ComboField(Field): class ComboField(Field):
""" """
@ -760,6 +724,9 @@ class MultiValueField(Field):
f.required = False f.required = False
self.fields = fields self.fields = fields
def validate(self, value):
pass
def clean(self, value): def clean(self, value):
""" """
Validates every value in the given list. A value is validated against Validates every value in the given list. A value is validated against
@ -772,7 +739,7 @@ class MultiValueField(Field):
clean_data = [] clean_data = []
errors = ErrorList() errors = ErrorList()
if not value or isinstance(value, (list, tuple)): 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: if self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
else: else:
@ -784,7 +751,7 @@ class MultiValueField(Field):
field_value = value[i] field_value = value[i]
except IndexError: except IndexError:
field_value = None 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']) raise ValidationError(self.error_messages['required'])
try: try:
clean_data.append(field.clean(field_value)) clean_data.append(field.clean(field_value))
@ -795,7 +762,10 @@ class MultiValueField(Field):
errors.extend(e.messages) errors.extend(e.messages)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
return self.compress(clean_data)
out = self.compress(clean_data)
self.validate(out)
return out
def compress(self, data_list): def compress(self, data_list):
""" """
@ -864,30 +834,24 @@ class SplitDateTimeField(MultiValueField):
if data_list: if data_list:
# Raise a validation error if time or date is empty # Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False). # (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']) 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']) raise ValidationError(self.error_messages['invalid_time'])
return datetime.datetime.combine(*data_list) return datetime.datetime.combine(*data_list)
return None 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 = { default_error_messages = {
'invalid': _(u'Enter a valid IPv4 address.'), '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(CharField):
class SlugField(RegexField):
default_error_messages = { default_error_messages = {
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers," 'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
u" underscores or hyphens."), u" underscores or hyphens."),
} }
default_validators = [validators.validate_slug]
def __init__(self, *args, **kwargs):
super(SlugField, self).__init__(slug_re, *args, **kwargs)

View File

@ -2,6 +2,7 @@
Form classes Form classes
""" """
from django.core.exceptions import ValidationError
from django.utils.copycompat import deepcopy from django.utils.copycompat import deepcopy
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
@ -10,7 +11,7 @@ from django.utils.safestring import mark_safe
from fields import Field, FileField from fields import Field, FileField
from widgets import Media, media_property, TextInput, Textarea from widgets import Media, media_property, TextInput, Textarea
from util import flatatt, ErrorDict, ErrorList, ValidationError from util import flatatt, ErrorDict, ErrorList
__all__ = ('BaseForm', 'Form') __all__ = ('BaseForm', 'Form')

View File

@ -1,10 +1,11 @@
from forms import Form from forms import Form
from django.core.exceptions import ValidationError
from django.utils.encoding import StrAndUnicode from django.utils.encoding import StrAndUnicode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from fields import IntegerField, BooleanField from fields import IntegerField, BooleanField
from widgets import Media, HiddenInput from widgets import Media, HiddenInput
from util import ErrorList, ErrorDict, ValidationError from util import ErrorList
__all__ = ('BaseFormSet', 'all_valid') __all__ = ('BaseFormSet', 'all_valid')

View File

@ -9,10 +9,12 @@ from django.utils.datastructures import SortedDict
from django.utils.text import get_text_list, capfirst from django.utils.text import get_text_list, capfirst
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
from util import ValidationError, ErrorList from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError
from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS from django.core.validators import EMPTY_VALUES
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES from util import ErrorList
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField
from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput
from widgets import media_property from widgets import media_property
from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
@ -27,20 +29,15 @@ __all__ = (
'ModelMultipleChoiceField', 'ModelMultipleChoiceField',
) )
def construct_instance(form, instance, fields=None, exclude=None):
def save_instance(form, instance, fields=None, fail_message='saved',
commit=True, exclude=None):
""" """
Saves bound Form ``form``'s cleaned_data into model instance ``instance``. Constructs and returns a model instance from the bound ``form``'s
``cleaned_data``, but does not save the returned instance to the
If commit=True, then the changes to ``instance`` will be saved to the database.
database. Returns ``instance``.
""" """
from django.db import models from django.db import models
opts = instance._meta 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 cleaned_data = form.cleaned_data
file_field_list = [] file_field_list = []
for f in opts.fields: 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: for f in file_field_list:
f.save_form_data(instance, cleaned_data[f.name]) 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. # Wrap up the saving of m2m data as a function.
def save_m2m(): def save_m2m():
opts = instance._meta
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
for f in opts.many_to_many: for f in opts.many_to_many:
if fields and f.name not in fields: if fields and f.name not in fields:
@ -120,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None):
the ``fields`` argument. the ``fields`` argument.
""" """
# avoid a circular import # avoid a circular import
from django.db.models.fields.related import ManyToManyField, OneToOneField from django.db.models.fields.related import ManyToManyField
opts = instance._meta opts = instance._meta
data = {} data = {}
for f in opts.fields + opts.many_to_many: 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 # if we didn't get an instance, instantiate a new one
self.instance = opts.model() self.instance = opts.model()
object_data = {} object_data = {}
self.instance._adding = True
else: else:
self.instance = instance self.instance = instance
self.instance._adding = False
object_data = model_to_dict(instance, opts.fields, opts.exclude) object_data = model_to_dict(instance, opts.fields, opts.exclude)
# if initial was provided, it should override the values from instance # if initial was provided, it should override the values from instance
if initial is not None: if initial is not None:
@ -228,166 +246,32 @@ class BaseModelForm(BaseForm):
error_class, label_suffix, empty_permitted) error_class, label_suffix, empty_permitted)
def clean(self): def clean(self):
self.validate_unique() 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)
# Remove the data from the cleaned_data dict since it was invalid
if k in self.cleaned_data:
del self.cleaned_data[k]
if NON_FIELD_ERRORS in e.message_dict:
raise ValidationError(e.message_dict[NON_FIELD_ERRORS])
# 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)
return self.cleaned_data 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]
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():
self._errors[field] = ErrorList([
self.date_error_message(lookup_type, field, unique_for)
])
bad_fields.add(field)
return bad_fields, []
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,
}
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)
}
def save(self, commit=True): def save(self, commit=True):
""" """
Saves this ``form``'s cleaned_data into model instance Saves this ``form``'s cleaned_data into model instance
@ -401,7 +285,7 @@ class BaseModelForm(BaseForm):
else: else:
fail_message = 'changed' fail_message = 'changed'
return save_instance(self, self.instance, self._meta.fields, 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 save.alters_data = True
@ -530,7 +414,7 @@ class BaseModelFormSet(BaseFormSet):
break break
else: else:
return return
unique_checks, date_checks = form._get_unique_checks() unique_checks, date_checks = form.instance._get_unique_checks()
errors = [] errors = []
# Do each of the unique checks (unique and unique_together) # Do each of the unique checks (unique and unique_together)
for unique_check in unique_checks: for unique_check in unique_checks:
@ -743,6 +627,9 @@ class BaseInlineFormSet(BaseModelFormSet):
# Remove the foreign key from the form's data # Remove the foreign key from the form's data
form.data[form.add_prefix(self.fk.name)] = None 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 return form
#@classmethod #@classmethod

View File

@ -1,7 +1,11 @@
from django.utils.html import conditional_escape 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 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): def flatatt(attrs):
""" """
Convert a dictionary of attributes to a single string. Convert a dictionary of attributes to a single string.
@ -48,21 +52,3 @@ class ErrorList(list, StrAndUnicode):
def __repr__(self): def __repr__(self):
return repr([force_unicode(e) for e in 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)

View File

@ -257,6 +257,17 @@ And here is a custom error message::
In the `built-in Field classes`_ section below, each ``Field`` defines the In the `built-in Field classes`_ section below, each ``Field`` defines the
error message keys it uses. error message keys it uses.
``validators``
~~~~~~~~~~~~~~
.. versionadded:: 1.2
.. attribute:: Field.validators
The ``validators`` argument lets you provide a list of validation functions
for this field.
See the :ref:`validators documentation <validators>` for more information.
Built-in ``Field`` classes Built-in ``Field`` classes
-------------------------- --------------------------

View File

@ -3,6 +3,8 @@
Form and field validation Form and field validation
========================= =========================
.. versionchanged:: 1.2
Form validation happens when the data is cleaned. If you want to customize Form validation happens when the data is cleaned. If you want to customize
this process, there are various places you can change, each one serving a this process, there are various places you can change, each one serving a
different purpose. Three types of cleaning methods are run during form different purpose. Three types of cleaning methods are run during form
@ -20,13 +22,38 @@ If you detect multiple errors during a cleaning method and wish to signal all
of them to the form submitter, it is possible to pass a list of errors to the of them to the form submitter, it is possible to pass a list of errors to the
``ValidationError`` constructor. ``ValidationError`` constructor.
The three types of cleaning methods are: Most validation can be done using `validators`_ - simple helpers that can be
reused easily. Validators are simple functions (or callables) that take a single
argument and raise ``ValidationError`` on invalid input. Validators are run
after the field's ``to_python`` and ``validate`` methods have been called.
* The ``clean()`` method on a Field subclass. This is responsible Validation of a Form is split into several steps, which can be customized or
for cleaning the data in a way that is generic for that type of field. overridden:
For example, a FloatField will turn the data into a Python ``float`` or
raise a ``ValidationError``. This method returns the clean data, which * The ``to_python()`` method on a Field is the first step in every
is then inserted into the ``cleaned_data`` dictionary of the form. validation. It coerces the value to correct datatype and raises
``ValidationError`` if that is not possible. This method accepts the raw
value from the widget and returns the converted value. For example, a
FloatField will turn the data into a Python ``float`` or raise a
``ValidationError``.
* The ``validate()`` method on a Field handles field-specific validation
that is not suitable for a validator, It takes a value that has been
coerced to correct datatype and raises ``ValidationError`` on any error.
This method does not return anything and shouldn't alter the value. You
should override it to handle validation logic that you can't or don't
want to put in a validator.
* The ``run_validators()`` method on a Field runs all of the field's
validators and aggregates all the errors into a single
``ValidationError``. You shouldn't need to override this method.
* The ``clean()`` method on a Field subclass. This is responsible for
running ``to_python``, ``validate`` and ``run_validators`` in the correct
order and propagating their errors. If, at any time, any of the methods
raise ``ValidationError``, the validation stops and that error is raised.
This method returns the clean data, which is then inserted into the
``cleaned_data`` dictionary of the form.
* The ``clean_<fieldname>()`` method in a form subclass -- where * The ``clean_<fieldname>()`` method in a form subclass -- where
``<fieldname>`` is replaced with the name of the form field attribute. ``<fieldname>`` is replaced with the name of the form field attribute.
@ -141,35 +168,68 @@ Since it can sometimes be easier to put things into place by seeing each
feature in use, here are a series of small examples that use each of the feature in use, here are a series of small examples that use each of the
previous features. previous features.
.. _validators:
Using validators
~~~~~~~~~~~~~~~~
.. versionadded:: 1.2
Django's form (and model) fields support use of simple utility functions and
classes known as validators. These can passed to a field's constructor, via
the field's ``validators`` argument, or defined on the Field class itself with
the ``default_validators`` attribute.
Simple validators can be used to validate values inside the field, let's have
a look at Django's ``EmailField``::
class EmailField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid e-mail address.'),
}
default_validators = [validators.validate_email]
As you can see, ``EmailField`` is just a ``CharField`` with customized error
message and a validator that validates e-mail addresses. This can also be done
on field definition so::
email = forms.EmailField()
is equivalent to::
email = forms.CharField(validators=[validators.validate_email],
error_messages={'invalid': _(u'Enter a valid e-mail address.')})
Form field default cleaning Form field default cleaning
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let's firstly create a custom form field that validates its input is a string Let's firstly create a custom form field that validates its input is a string
containing comma-separated e-mail addresses, with at least one address. We'll containing comma-separated e-mail addresses. The full class looks like this::
keep it simple and assume e-mail validation is contained in a function called
``is_valid_email()``. The full class looks like this::
from django import forms from django import forms
from django.core.validators import validate_email
class MultiEmailField(forms.Field): class MultiEmailField(forms.Field):
def clean(self, value): def to_python(self, value):
""" "Normalize data to a list of strings."
Check that the field contains one or more comma-separated emails
and normalizes the data to a list of the email strings. # Return an empty list if no input was given.
"""
if not value: if not value:
raise forms.ValidationError('Enter at least one e-mail address.') return []
emails = value.split(',') return value.split(',')
for email in emails:
if not is_valid_email(email):
raise forms.ValidationError('%s is not a valid e-mail address.' % email)
# Always return the cleaned data. def validate(self, value):
return emails "Check if value consists only of valid emails."
Every form that uses this field will have this ``clean()`` method run before # Use the parent's handling of required fields, etc.
anything else can be done with the field's data. This is cleaning that is super(MultiEmailField, self).validate(value)
specific to this type of field, regardless of how it is subsequently used.
for email in value:
validate_email(email)
Every form that uses this field will have these methods run before anything
else can be done with the field's data. This is cleaning that is specific to
this type of field, regardless of how it is subsequently used.
Let's create a simple ``ContactForm`` to demonstrate how you'd use this Let's create a simple ``ContactForm`` to demonstrate how you'd use this
field:: field::
@ -183,7 +243,8 @@ field::
Simply use ``MultiEmailField`` like any other form field. When the Simply use ``MultiEmailField`` like any other form field. When the
``is_valid()`` method is called on the form, the ``MultiEmailField.clean()`` ``is_valid()`` method is called on the form, the ``MultiEmailField.clean()``
method will be run as part of the cleaning process. method will be run as part of the cleaning process and it will, in turn, call
the custom ``to_python()`` and ``validate()`` methods.
Cleaning a specific field attribute Cleaning a specific field attribute
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -196,6 +196,17 @@ callable it will be called every time a new object is created.
If ``False``, the field will not be editable in the admin or via forms If ``False``, the field will not be editable in the admin or via forms
automatically generated from the model class. Default is ``True``. automatically generated from the model class. Default is ``True``.
``error_messages``
------------------
.. versionadded:: 1.2
.. attribute:: Field.error_messages
The ``error_messages`` argument lets you override the default messages that the
field will raise. Pass in a dictionary with keys matching the error messages you
want to override.
``help_text`` ``help_text``
------------- -------------
@ -284,6 +295,17 @@ underscores to spaces. See :ref:`Verbose field names <verbose-field-names>`.
.. _model-field-types: .. _model-field-types:
``validators``
-------------------
.. versionadded:: 1.2
.. attribute:: Field.validators
A list of validators to run for this field.See the :ref:`validators
documentation <validators>` for more information.
Field types Field types
=========== ===========

View File

@ -27,6 +27,31 @@ The keyword arguments are simply the names of the fields you've defined on your
model. Note that instantiating a model in no way touches your database; for model. Note that instantiating a model in no way touches your database; for
that, you need to ``save()``. that, you need to ``save()``.
Validating objects
==================
.. versionadded:: 1.2
To validate your model, just call its ``full_validate()`` method:
.. method:: Model.full_validate([exclude=[]])
The optional ``exclude`` argument can contain a list of field names that should
be omitted when validating. This method raises ``ValidationError`` containing a
message dict with errors from all fields.
To add your own validation logic, override the supplied ``validate()`` method:
.. method:: Model.validate()
The ``validate()`` method on ``Model`` by default checks for uniqueness of
fields and group of fields that are declared to be unique so, remember to call
``self.validate_unique()`` or the superclasses ``validate`` method if you want
this validation to run.
Any ``ValidationError`` raised in this method will be propagated in the
``message_dict`` under ``NON_FIELD_ERRORS``.
Saving objects Saving objects
============== ==============

View File

@ -68,7 +68,7 @@ class ImprovedArticleWithParentLink(models.Model):
article = models.OneToOneField(Article, parent_link=True) article = models.OneToOneField(Article, parent_link=True)
class BetterWriter(Writer): class BetterWriter(Writer):
pass score = models.IntegerField()
class WriterProfile(models.Model): class WriterProfile(models.Model):
writer = models.OneToOneField(Writer, primary_key=True) writer = models.OneToOneField(Writer, primary_key=True)
@ -555,6 +555,8 @@ inserted as 'initial' data in each Field.
<option value="3">Third test</option> <option value="3">Third test</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> </select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
>>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'}, instance=art) >>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'}, instance=art)
>>> f.errors
{}
>>> f.is_valid() >>> f.is_valid()
True True
>>> test_art = f.save() >>> test_art = f.save()
@ -967,10 +969,20 @@ ValidationError: [u'Select a valid choice. 4 is not one of the available choices
>>> ImprovedArticleWithParentLinkForm.base_fields.keys() >>> ImprovedArticleWithParentLinkForm.base_fields.keys()
[] []
>>> bw = BetterWriter(name=u'Joe Better') >>> bw = BetterWriter(name=u'Joe Better', score=10)
>>> bw.save() >>> bw.save()
>>> sorted(model_to_dict(bw).keys()) >>> sorted(model_to_dict(bw).keys())
['id', 'name', 'writer_ptr'] ['id', 'name', 'score', 'writer_ptr']
>>> class BetterWriterForm(ModelForm):
... class Meta:
... model = BetterWriter
>>> form = BetterWriterForm({'name': 'Some Name', 'score': 12})
>>> form.is_valid()
True
>>> bw2 = form.save()
>>> bw2.delete()
>>> class WriterProfileForm(ModelForm): >>> class WriterProfileForm(ModelForm):
... class Meta: ... class Meta:
@ -1102,16 +1114,6 @@ True
>>> instance.delete() >>> instance.delete()
# Test the non-required FileField
>>> f = TextFileForm(data={'description': u'Assistance'})
>>> f.fields['file'].required = False
>>> f.is_valid()
True
>>> instance = f.save()
>>> instance.file
<FieldFile: None>
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance) >>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
>>> f.is_valid() >>> f.is_valid()
True True
@ -1358,27 +1360,35 @@ __test__['API_TESTS'] += """
... class Meta: ... class Meta:
... model = CommaSeparatedInteger ... model = CommaSeparatedInteger
>>> f = CommaSeparatedIntegerForm().fields['field'] >>> f = CommaSeparatedIntegerForm({'field': '1,2,3'})
>>> f.clean('1,2,3') >>> f.is_valid()
u'1,2,3' True
>>> f.clean('1a,2') >>> f.cleaned_data
Traceback (most recent call last): {'field': u'1,2,3'}
... >>> f = CommaSeparatedIntegerForm({'field': '1a,2'})
ValidationError: [u'Enter only digits separated by commas.'] >>> f.errors
>>> f.clean(',,,,') {'field': [u'Enter only digits separated by commas.']}
u',,,,' >>> f = CommaSeparatedIntegerForm({'field': ',,,,'})
>>> f.clean('1.2') >>> f.is_valid()
Traceback (most recent call last): True
... >>> f.cleaned_data
ValidationError: [u'Enter only digits separated by commas.'] {'field': u',,,,'}
>>> f.clean('1,a,2') >>> f = CommaSeparatedIntegerForm({'field': '1.2'})
Traceback (most recent call last): >>> f.errors
... {'field': [u'Enter only digits separated by commas.']}
ValidationError: [u'Enter only digits separated by commas.'] >>> f = CommaSeparatedIntegerForm({'field': '1,a,2'})
>>> f.clean('1,,2') >>> f.errors
u'1,,2' {'field': [u'Enter only digits separated by commas.']}
>>> f.clean('1') >>> f = CommaSeparatedIntegerForm({'field': '1,,2'})
u'1' >>> f.is_valid()
True
>>> f.cleaned_data
{'field': u'1,,2'}
>>> f = CommaSeparatedIntegerForm({'field': '1'})
>>> f.is_valid()
True
>>> f.cleaned_data
{'field': u'1'}
# unique/unique_together validation # unique/unique_together validation
@ -1415,13 +1425,16 @@ False
>>> form._errors >>> form._errors
{'__all__': [u'Price with this Price and Quantity already exists.']} {'__all__': [u'Price with this Price and Quantity already exists.']}
# This form is never valid because quantity is blank=False.
>>> class PriceForm(ModelForm): >>> class PriceForm(ModelForm):
... class Meta: ... class Meta:
... model = Price ... model = Price
... exclude = ('quantity',) ... exclude = ('quantity',)
>>> form = PriceForm({'price': '6.00'}) >>> form = PriceForm({'price': '6.00'})
>>> form.is_valid() >>> form.is_valid()
True Traceback (most recent call last):
...
UnresolvableValidationError: {'quantity': [u'This field cannot be null.']}
# Unique & unique together with null values # Unique & unique together with null values
>>> class BookForm(ModelForm): >>> class BookForm(ModelForm):

View File

@ -543,10 +543,6 @@ This is used in the admin for save_as functionality.
... 'book_set-2-title': '', ... 'book_set-2-title': '',
... } ... }
>>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True)
>>> formset.is_valid()
True
>>> new_author = Author.objects.create(name='Charles Baudelaire') >>> new_author = Author.objects.create(name='Charles Baudelaire')
>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True) >>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True)
>>> [book for book in formset.save() if book.author.pk == new_author.pk] >>> [book for book in formset.save() if book.author.pk == new_author.pk]
@ -1035,19 +1031,6 @@ False
>>> formset._non_form_errors >>> formset._non_form_errors
[u'Please correct the duplicate data for price and quantity, which must be unique.'] [u'Please correct the duplicate data for price and quantity, which must be unique.']
# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled.
# this will fail with a KeyError if broken.
>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
>>> data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-0-price': '24',
... 'form-1-price': '24',
... }
>>> formset = FormSet(data)
>>> formset.is_valid()
True
>>> FormSet = inlineformset_factory(Author, Book, extra=0) >>> FormSet = inlineformset_factory(Author, Book, extra=0)
>>> author = Author.objects.order_by('id')[0] >>> author = Author.objects.order_by('id')[0]
>>> book_ids = author.book_set.values_list('id', flat=True) >>> book_ids = author.book_set.values_list('id', flat=True)

View File

@ -0,0 +1,21 @@
import unittest
from django.core.exceptions import ValidationError
class ValidationTestCase(unittest.TestCase):
def assertFailsValidation(self, clean, failed_fields):
self.assertRaises(ValidationError, clean)
try:
clean()
except ValidationError, e:
self.assertEquals(sorted(failed_fields), sorted(e.message_dict.keys()))
def assertFieldFailsValidationWithMessage(self, clean, field_name, message):
self.assertRaises(ValidationError, clean)
try:
clean()
except ValidationError, e:
self.assertTrue(field_name in e.message_dict)
self.assertEquals(message, e.message_dict[field_name])

View File

@ -0,0 +1,53 @@
from datetime import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.test import TestCase
def validate_answer_to_universe(value):
if value != 42:
raise ValidationError('This is not the answer to life, universe and everything!', code='not42')
class ModelToValidate(models.Model):
name = models.CharField(max_length=100)
created = models.DateTimeField(default=datetime.now)
number = models.IntegerField()
parent = models.ForeignKey('self', blank=True, null=True)
email = models.EmailField(blank=True)
url = models.URLField(blank=True)
f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe])
def validate(self):
super(ModelToValidate, self).validate()
if self.number == 11:
raise ValidationError('Invalid number supplied!')
class UniqueFieldsModel(models.Model):
unique_charfield = models.CharField(max_length=100, unique=True)
unique_integerfield = models.IntegerField(unique=True)
non_unique_field = models.IntegerField()
class CustomPKModel(models.Model):
my_pk_field = models.CharField(max_length=100, primary_key=True)
class UniqueTogetherModel(models.Model):
cfield = models.CharField(max_length=100)
ifield = models.IntegerField()
efield = models.EmailField()
class Meta:
unique_together = (('ifield', 'cfield',),('ifield', 'efield'), )
class UniqueForDateModel(models.Model):
start_date = models.DateField()
end_date = models.DateTimeField()
count = models.IntegerField(unique_for_date="start_date", unique_for_year="end_date")
order = models.IntegerField(unique_for_month="end_date")
name = models.CharField(max_length=100)
class CustomMessagesModel(models.Model):
other = models.IntegerField(blank=True, null=True)
number = models.IntegerField(
error_messages={'null': 'NULL', 'not42': 'AAARGH', 'not_equal': '%s != me'},
validators=[validate_answer_to_universe]
)

View File

@ -0,0 +1,13 @@
from modeltests.validation import ValidationTestCase
from models import CustomMessagesModel
class CustomMessagesTest(ValidationTestCase):
def test_custom_simple_validator_message(self):
cmm = CustomMessagesModel(number=12)
self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['AAARGH'])
def test_custom_null_message(self):
cmm = CustomMessagesModel()
self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['NULL'])

View File

@ -0,0 +1,58 @@
import unittest
from django.conf import settings
from django.db import connection
from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate
class GetUniqueCheckTests(unittest.TestCase):
def test_unique_fields_get_collected(self):
m = UniqueFieldsModel()
self.assertEqual(
([('id',), ('unique_charfield',), ('unique_integerfield',)], []),
m._get_unique_checks()
)
def test_unique_together_gets_picked_up(self):
m = UniqueTogetherModel()
self.assertEqual(
([('ifield', 'cfield',),('ifield', 'efield'), ('id',), ], []),
m._get_unique_checks()
)
def test_primary_key_is_considered_unique(self):
m = CustomPKModel()
self.assertEqual(([('my_pk_field',)], []), m._get_unique_checks())
def test_unique_for_date_gets_picked_up(self):
m = UniqueForDateModel()
self.assertEqual((
[('id',)],
[('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
), m._get_unique_checks()
)
class PerformUniqueChecksTest(unittest.TestCase):
def setUp(self):
# Set debug to True to gain access to connection.queries.
self._old_debug, settings.DEBUG = settings.DEBUG, True
super(PerformUniqueChecksTest, self).setUp()
def tearDown(self):
# Restore old debug value.
settings.DEBUG = self._old_debug
super(PerformUniqueChecksTest, self).tearDown()
def test_primary_key_unique_check_performed_when_adding(self):
"""Regression test for #12132"""
l = len(connection.queries)
mtv = ModelToValidate(number=10, name='Some Name')
setattr(mtv, '_adding', True)
mtv.full_validate()
self.assertEqual(l+1, len(connection.queries))
def test_primary_key_unique_check_not_performed_when_not_adding(self):
"""Regression test for #12132"""
l = len(connection.queries)
mtv = ModelToValidate(number=10, name='Some Name')
mtv.full_validate()
self.assertEqual(l, len(connection.queries))

View File

@ -0,0 +1,58 @@
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.db import models
from modeltests.validation import ValidationTestCase
from models import *
from validators import TestModelsWithValidators
from test_unique import GetUniqueCheckTests, PerformUniqueChecksTest
from test_custom_messages import CustomMessagesTest
class BaseModelValidationTests(ValidationTestCase):
def test_missing_required_field_raises_error(self):
mtv = ModelToValidate(f_with_custom_validator=42)
self.assertFailsValidation(mtv.full_validate, ['name', 'number'])
def test_with_correct_value_model_validates(self):
mtv = ModelToValidate(number=10, name='Some Name')
self.assertEqual(None, mtv.full_validate())
def test_custom_validate_method_is_called(self):
mtv = ModelToValidate(number=11)
self.assertFailsValidation(mtv.full_validate, [NON_FIELD_ERRORS, 'name'])
def test_wrong_FK_value_raises_error(self):
mtv=ModelToValidate(number=10, name='Some Name', parent_id=3)
self.assertFailsValidation(mtv.full_validate, ['parent'])
def test_correct_FK_value_validates(self):
parent = ModelToValidate.objects.create(number=10, name='Some Name')
mtv=ModelToValidate(number=10, name='Some Name', parent_id=parent.pk)
self.assertEqual(None, mtv.full_validate())
def test_wrong_email_value_raises_error(self):
mtv = ModelToValidate(number=10, name='Some Name', email='not-an-email')
self.assertFailsValidation(mtv.full_validate, ['email'])
def test_correct_email_value_passes(self):
mtv = ModelToValidate(number=10, name='Some Name', email='valid@email.com')
self.assertEqual(None, mtv.full_validate())
def test_wrong_url_value_raises_error(self):
mtv = ModelToValidate(number=10, name='Some Name', url='not a url')
self.assertFieldFailsValidationWithMessage(mtv.full_validate, 'url', [u'Enter a valid value.'])
def test_correct_url_but_nonexisting_gives_404(self):
mtv = ModelToValidate(number=10, name='Some Name', url='http://google.com/we-love-microsoft.html')
self.assertFieldFailsValidationWithMessage(mtv.full_validate, 'url', [u'This URL appears to be a broken link.'])
def test_correct_url_value_passes(self):
mtv = ModelToValidate(number=10, name='Some Name', url='http://www.djangoproject.com/')
self.assertEqual(None, mtv.full_validate()) # This will fail if there's no Internet connection
def test_text_greater_that_charfields_max_length_eaises_erros(self):
mtv = ModelToValidate(number=10, name='Some Name'*100)
self.assertFailsValidation(mtv.full_validate, ['name',])

View File

@ -0,0 +1,18 @@
from unittest import TestCase
from modeltests.validation import ValidationTestCase
from models import *
class TestModelsWithValidators(ValidationTestCase):
def test_custom_validator_passes_for_correct_value(self):
mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=42)
self.assertEqual(None, mtv.full_validate())
def test_custom_validator_raises_error_for_incorrect_value(self):
mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=12)
self.assertFailsValidation(mtv.full_validate, ['f_with_custom_validator'])
self.assertFieldFailsValidationWithMessage(
mtv.full_validate,
'f_with_custom_validator',
[u'This is not the answer to life, universe and everything!']
)

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
import re
import types
from unittest import TestCase
from datetime import datetime, timedelta
from django.core.exceptions import ValidationError
from django.core.validators import *
NOW = datetime.now()
TEST_DATA = (
# (validator, value, expected),
(validate_integer, '42', None),
(validate_integer, '-42', None),
(validate_integer, -42, None),
(validate_integer, -42.5, None),
(validate_integer, None, ValidationError),
(validate_integer, 'a', ValidationError),
(validate_email, 'email@here.com', None),
(validate_email, 'weirder-email@here.and.there.com', None),
(validate_email, None, ValidationError),
(validate_email, '', ValidationError),
(validate_email, 'abc', ValidationError),
(validate_email, 'a @x.cz', ValidationError),
(validate_email, 'something@@somewhere.com', ValidationError),
(validate_slug, 'slug-ok', None),
(validate_slug, 'longer-slug-still-ok', None),
(validate_slug, '--------', None),
(validate_slug, 'nohyphensoranything', None),
(validate_slug, '', ValidationError),
(validate_slug, ' text ', ValidationError),
(validate_slug, ' ', ValidationError),
(validate_slug, 'some@mail.com', ValidationError),
(validate_slug, '你好', ValidationError),
(validate_slug, '\n', ValidationError),
(validate_ipv4_address, '1.1.1.1', None),
(validate_ipv4_address, '255.0.0.0', None),
(validate_ipv4_address, '0.0.0.0', None),
(validate_ipv4_address, '256.1.1.1', ValidationError),
(validate_ipv4_address, '25.1.1.', ValidationError),
(validate_ipv4_address, '25,1,1,1', ValidationError),
(validate_ipv4_address, '25.1 .1.1', ValidationError),
(validate_comma_separated_integer_list, '1', None),
(validate_comma_separated_integer_list, '1,2,3', None),
(validate_comma_separated_integer_list, '1,2,3,', None),
(validate_comma_separated_integer_list, '', ValidationError),
(validate_comma_separated_integer_list, 'a,b,c', ValidationError),
(validate_comma_separated_integer_list, '1, 2, 3', ValidationError),
(MaxValueValidator(10), 10, None),
(MaxValueValidator(10), -10, None),
(MaxValueValidator(10), 0, None),
(MaxValueValidator(NOW), NOW, None),
(MaxValueValidator(NOW), NOW - timedelta(days=1), None),
(MaxValueValidator(0), 1, ValidationError),
(MaxValueValidator(NOW), NOW + timedelta(days=1), ValidationError),
(MinValueValidator(-10), -10, None),
(MinValueValidator(-10), 10, None),
(MinValueValidator(-10), 0, None),
(MinValueValidator(NOW), NOW, None),
(MinValueValidator(NOW), NOW + timedelta(days=1), None),
(MinValueValidator(0), -1, ValidationError),
(MinValueValidator(NOW), NOW - timedelta(days=1), ValidationError),
(MaxLengthValidator(10), '', None),
(MaxLengthValidator(10), 10*'x', None),
(MaxLengthValidator(10), 15*'x', ValidationError),
(MinLengthValidator(10), 15*'x', None),
(MinLengthValidator(10), 10*'x', None),
(MinLengthValidator(10), '', ValidationError),
(URLValidator(), 'http://www.djangoproject.com/', None),
(URLValidator(), 'http://localhost/', None),
(URLValidator(), 'http://example.com/', None),
(URLValidator(), 'http://www.example.com/', None),
(URLValidator(), 'http://www.example.com:8000/test', None),
(URLValidator(), 'http://valid-with-hyphens.com/', None),
(URLValidator(), 'http://subdomain.domain.com/', None),
(URLValidator(), 'http://200.8.9.10/', None),
(URLValidator(), 'http://200.8.9.10:8000/test', None),
(URLValidator(), 'http://valid-----hyphens.com/', None),
(URLValidator(), 'http://example.com?something=value', None),
(URLValidator(), 'http://example.com/index.php?something=value&another=value2', None),
(URLValidator(), 'foo', ValidationError),
(URLValidator(), 'http://', ValidationError),
(URLValidator(), 'http://example', ValidationError),
(URLValidator(), 'http://example.', ValidationError),
(URLValidator(), 'http://.com', ValidationError),
(URLValidator(), 'http://invalid-.com', ValidationError),
(URLValidator(), 'http://-invalid.com', ValidationError),
(URLValidator(), 'http://inv-.alid-.com', ValidationError),
(URLValidator(), 'http://inv-.-alid.com', ValidationError),
(BaseValidator(True), True, None),
(BaseValidator(True), False, ValidationError),
(RegexValidator('.*'), '', None),
(RegexValidator(re.compile('.*')), '', None),
(RegexValidator('.*'), 'xxxxx', None),
(RegexValidator('x'), 'y', ValidationError),
(RegexValidator(re.compile('x')), 'y', ValidationError),
)
def create_simple_test_method(validator, expected, value, num):
if isinstance(expected, type) and issubclass(expected, Exception):
test_mask = 'test_%s_raises_error_%d'
def test_func(self):
self.assertRaises(expected, validator, value)
else:
test_mask = 'test_%s_%d'
def test_func(self):
self.assertEqual(expected, validator(value))
if isinstance(validator, types.FunctionType):
val_name = validator.__name__
else:
val_name = validator.__class__.__name__
test_name = test_mask % (val_name, num)
return test_name, test_func
# Dynamically assemble a test class with the contents of TEST_DATA
class TestSimpleValidators(TestCase):
pass
test_counter = 0
for validator, value, expected in TEST_DATA:
name, method = create_simple_test_method(validator, expected, value, test_counter)
setattr(TestSimpleValidators, name, method)
test_counter += 1

View File

@ -6,8 +6,8 @@ tests = r"""
# CharField ################################################################### # CharField ###################################################################
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' >>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' >>> e['max_length'] = 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s'
>>> f = CharField(min_length=5, max_length=10, error_messages=e) >>> f = CharField(min_length=5, max_length=10, error_messages=e)
>>> f.clean('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
@ -26,8 +26,8 @@ ValidationError: [u'LENGTH 11, MAX LENGTH 10']
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID' >>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s' >>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %s' >>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> f = IntegerField(min_value=5, max_value=10, error_messages=e) >>> f = IntegerField(min_value=5, max_value=10, error_messages=e)
>>> f.clean('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
@ -50,8 +50,8 @@ ValidationError: [u'MAX VALUE IS 10']
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID' >>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s' >>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %s' >>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> f = FloatField(min_value=5, max_value=10, error_messages=e) >>> f = FloatField(min_value=5, max_value=10, error_messages=e)
>>> f.clean('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
@ -74,8 +74,8 @@ ValidationError: [u'MAX VALUE IS 10']
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID' >>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s' >>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %s' >>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> e['max_digits'] = 'MAX DIGITS IS %s' >>> e['max_digits'] = 'MAX DIGITS IS %s'
>>> e['max_decimal_places'] = 'MAX DP IS %s' >>> e['max_decimal_places'] = 'MAX DP IS %s'
>>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s' >>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s'
@ -156,8 +156,8 @@ ValidationError: [u'INVALID']
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID' >>> e['invalid'] = 'INVALID'
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' >>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' >>> e['max_length'] = 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s'
>>> f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e) >>> f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e)
>>> f.clean('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
@ -180,8 +180,8 @@ ValidationError: [u'LENGTH 11, MAX LENGTH 10']
>>> e = {'required': 'REQUIRED'} >>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID' >>> e['invalid'] = 'INVALID'
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' >>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' >>> e['max_length'] = 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s'
>>> f = EmailField(min_length=8, max_length=10, error_messages=e) >>> f = EmailField(min_length=8, max_length=10, error_messages=e)
>>> f.clean('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):

View File

@ -386,7 +386,7 @@ class FieldsTests(TestCase):
def test_regexfield_31(self): def test_regexfield_31(self):
f = RegexField('^\d+$', min_length=5, max_length=10) f = RegexField('^\d+$', min_length=5, max_length=10)
self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, '123') self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, '123')
self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, 'abc') self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).', u'Enter a valid value.']", f.clean, 'abc')
self.assertEqual(u'12345', f.clean('12345')) self.assertEqual(u'12345', f.clean('12345'))
self.assertEqual(u'1234567890', f.clean('1234567890')) self.assertEqual(u'1234567890', f.clean('1234567890'))
self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '12345678901') self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '12345678901')
@ -548,6 +548,10 @@ class FieldsTests(TestCase):
self.assertEqual(u'http://example.com/', f.clean('http://example.com')) self.assertEqual(u'http://example.com/', f.clean('http://example.com'))
self.assertEqual(u'http://example.com/test', f.clean('http://example.com/test')) self.assertEqual(u'http://example.com/test', f.clean('http://example.com/test'))
def test_urlfield_ticket11826(self):
f = URLField()
self.assertEqual(u'http://example.com/?some_param=some_value', f.clean('http://example.com?some_param=some_value'))
# BooleanField ################################################################ # BooleanField ################################################################
def test_booleanfield_44(self): def test_booleanfield_44(self):

View File

@ -28,7 +28,7 @@ u'C1064AAB'
>>> f.clean('C1064AABB') >>> f.clean('C1064AABB')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 8 characters (it has 9).'] ValidationError: [u'Ensure this value has at most 8 characters (it has 9).', u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('C1064AA') >>> f.clean('C1064AA')
Traceback (most recent call last): Traceback (most recent call last):
... ...
@ -44,7 +44,7 @@ ValidationError: [u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('500') >>> f.clean('500')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 4 characters (it has 3).'] ValidationError: [u'Ensure this value has at least 4 characters (it has 3).', u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('5PPP') >>> f.clean('5PPP')
Traceback (most recent call last): Traceback (most recent call last):
... ...
@ -78,7 +78,7 @@ u'C1064AAB'
>>> f.clean('C1064AABB') >>> f.clean('C1064AABB')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 8 characters (it has 9).'] ValidationError: [u'Ensure this value has at most 8 characters (it has 9).', u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('C1064AA') >>> f.clean('C1064AA')
Traceback (most recent call last): Traceback (most recent call last):
... ...
@ -94,7 +94,7 @@ ValidationError: [u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('500') >>> f.clean('500')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 4 characters (it has 3).'] ValidationError: [u'Ensure this value has at least 4 characters (it has 3).', u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('5PPP') >>> f.clean('5PPP')
Traceback (most recent call last): Traceback (most recent call last):
... ...

View File

@ -15,11 +15,11 @@ u'230880-3449'
>>> f.clean('230880343') >>> f.clean('230880343')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 10 characters (it has 9).'] ValidationError: [u'Ensure this value has at least 10 characters (it has 9).', u'Enter a valid Icelandic identification number. The format is XXXXXX-XXXX.']
>>> f.clean('230880343234') >>> f.clean('230880343234')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 11 characters (it has 12).'] ValidationError: [u'Ensure this value has at most 11 characters (it has 12).', u'Enter a valid Icelandic identification number. The format is XXXXXX-XXXX.']
>>> f.clean('abcdefghijk') >>> f.clean('abcdefghijk')
Traceback (most recent call last): Traceback (most recent call last):
... ...
@ -61,18 +61,18 @@ ValidationError: [u'Enter a valid value.']
>>> f.clean('123456') >>> f.clean('123456')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 7 characters (it has 6).'] ValidationError: [u'Ensure this value has at least 7 characters (it has 6).', u'Enter a valid value.']
>>> f.clean('123456555') >>> f.clean('123456555')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 8 characters (it has 9).'] ValidationError: [u'Ensure this value has at most 8 characters (it has 9).', u'Enter a valid value.']
>>> f.clean('abcdefg') >>> f.clean('abcdefg')
Traceback (most recent call last): Traceback (most recent call last):
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
>>> f.clean(' 1234567 ') >>> f.clean(' 1234567 ')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 8 characters (it has 9).'] ValidationError: [u'Ensure this value has at most 8 characters (it has 9).', u'Enter a valid value.']
>>> f.clean(' 12367 ') >>> f.clean(' 12367 ')
Traceback (most recent call last): Traceback (most recent call last):
... ...

View File

@ -38,6 +38,7 @@ from formsets import tests as formset_tests
from media import media_tests from media import media_tests
from fields import FieldsTests from fields import FieldsTests
from validators import TestFieldWithValidators
__test__ = { __test__ = {
'extra_tests': extra_tests, 'extra_tests': extra_tests,

View File

@ -5,6 +5,7 @@ Tests for forms/util.py module.
tests = r""" tests = r"""
>>> from django.forms.util import * >>> from django.forms.util import *
>>> from django.core.exceptions import ValidationError
>>> from django.utils.translation import ugettext_lazy >>> from django.utils.translation import ugettext_lazy
########### ###########
@ -24,36 +25,36 @@ u''
################### ###################
# Can take a string. # Can take a string.
>>> print ValidationError("There was an error.").messages >>> print ErrorList(ValidationError("There was an error.").messages)
<ul class="errorlist"><li>There was an error.</li></ul> <ul class="errorlist"><li>There was an error.</li></ul>
# Can take a unicode string. # Can take a unicode string.
>>> print ValidationError(u"Not \u03C0.").messages >>> print ErrorList(ValidationError(u"Not \u03C0.").messages)
<ul class="errorlist"><li>Not π.</li></ul> <ul class="errorlist"><li>Not π.</li></ul>
# Can take a lazy string. # Can take a lazy string.
>>> print ValidationError(ugettext_lazy("Error.")).messages >>> print ErrorList(ValidationError(ugettext_lazy("Error.")).messages)
<ul class="errorlist"><li>Error.</li></ul> <ul class="errorlist"><li>Error.</li></ul>
# Can take a list. # Can take a list.
>>> print ValidationError(["Error one.", "Error two."]).messages >>> print ErrorList(ValidationError(["Error one.", "Error two."]).messages)
<ul class="errorlist"><li>Error one.</li><li>Error two.</li></ul> <ul class="errorlist"><li>Error one.</li><li>Error two.</li></ul>
# Can take a mixture in a list. # Can take a mixture in a list.
>>> print ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages >>> print ErrorList(ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages)
<ul class="errorlist"><li>First error.</li><li>Not π.</li><li>Error.</li></ul> <ul class="errorlist"><li>First error.</li><li>Not π.</li><li>Error.</li></ul>
>>> class VeryBadError: >>> class VeryBadError:
... def __unicode__(self): return u"A very bad error." ... def __unicode__(self): return u"A very bad error."
# Can take a non-string. # Can take a non-string.
>>> print ValidationError(VeryBadError()).messages >>> print ErrorList(ValidationError(VeryBadError()).messages)
<ul class="errorlist"><li>A very bad error.</li></ul> <ul class="errorlist"><li>A very bad error.</li></ul>
# Escapes non-safe input but not input marked safe. # Escapes non-safe input but not input marked safe.
>>> example = 'Example of link: <a href="http://www.example.com/">example</a>' >>> example = 'Example of link: <a href="http://www.example.com/">example</a>'
>>> print ValidationError(example).messages >>> print ErrorList([example])
<ul class="errorlist"><li>Example of link: &lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;</li></ul> <ul class="errorlist"><li>Example of link: &lt;a href=&quot;http://www.example.com/&quot;&gt;example&lt;/a&gt;</li></ul>
>>> print ValidationError(mark_safe(example)).messages >>> print ErrorList([mark_safe(example)])
<ul class="errorlist"><li>Example of link: <a href="http://www.example.com/">example</a></li></ul> <ul class="errorlist"><li>Example of link: <a href="http://www.example.com/">example</a></li></ul>
""" """

View File

@ -0,0 +1,17 @@
from unittest import TestCase
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
class TestFieldWithValidators(TestCase):
def test_all_errors_get_reported(self):
field = forms.CharField(
validators=[validators.validate_integer, validators.validate_email]
)
self.assertRaises(ValidationError, field.clean, 'not int nor mail')
try:
field.clean('not int nor mail')
except ValidationError, e:
self.assertEqual(2, len(e.messages))

View File

@ -81,7 +81,7 @@ class DeletionTests(TestCase):
regression for #10750 regression for #10750
""" """
# exclude some required field from the forms # exclude some required field from the forms
ChildFormSet = inlineformset_factory(School, Child, exclude=['father', 'mother']) ChildFormSet = inlineformset_factory(School, Child)
school = School.objects.create(name=u'test') school = School.objects.create(name=u'test')
mother = Parent.objects.create(name=u'mother') mother = Parent.objects.create(name=u'mother')
father = Parent.objects.create(name=u'father') father = Parent.objects.create(name=u'father')
@ -89,13 +89,13 @@ class DeletionTests(TestCase):
'child_set-TOTAL_FORMS': u'1', 'child_set-TOTAL_FORMS': u'1',
'child_set-INITIAL_FORMS': u'0', 'child_set-INITIAL_FORMS': u'0',
'child_set-0-name': u'child', 'child_set-0-name': u'child',
'child_set-0-mother': unicode(mother.pk),
'child_set-0-father': unicode(father.pk),
} }
formset = ChildFormSet(data, instance=school) formset = ChildFormSet(data, instance=school)
self.assertEqual(formset.is_valid(), True) self.assertEqual(formset.is_valid(), True)
objects = formset.save(commit=False) objects = formset.save(commit=False)
for obj in objects: self.assertEqual(school.child_set.count(), 0)
obj.mother = mother objects[0].save()
obj.father = father
obj.save()
self.assertEqual(school.child_set.count(), 1) self.assertEqual(school.child_set.count(), 1)

View File

@ -147,6 +147,58 @@ class SlugFieldTests(django.test.TestCase):
bs = BigS.objects.get(pk=bs.pk) bs = BigS.objects.get(pk=bs.pk)
self.assertEqual(bs.s, 'slug'*50) self.assertEqual(bs.s, 'slug'*50)
class ValidationTest(django.test.TestCase):
def test_charfield_raises_error_on_empty_string(self):
f = models.CharField()
self.assertRaises(ValidationError, f.clean, "", None)
def test_charfield_cleans_empty_string_when_blank_true(self):
f = models.CharField(blank=True)
self.assertEqual('', f.clean('', None))
def test_integerfield_cleans_valid_string(self):
f = models.IntegerField()
self.assertEqual(2, f.clean('2', None))
def test_integerfield_raises_error_on_invalid_intput(self):
f = models.IntegerField()
self.assertRaises(ValidationError, f.clean, "a", None)
def test_charfield_with_choices_cleans_valid_choice(self):
f = models.CharField(max_length=1, choices=[('a','A'), ('b','B')])
self.assertEqual('a', f.clean('a', None))
def test_charfield_with_choices_raises_error_on_invalid_choice(self):
f = models.CharField(choices=[('a','A'), ('b','B')])
self.assertRaises(ValidationError, f.clean, "not a", None)
def test_nullable_integerfield_raises_error_with_blank_false(self):
f = models.IntegerField(null=True, blank=False)
self.assertRaises(ValidationError, f.clean, None, None)
def test_nullable_integerfield_cleans_none_on_null_and_blank_true(self):
f = models.IntegerField(null=True, blank=True)
self.assertEqual(None, f.clean(None, None))
def test_integerfield_raises_error_on_empty_input(self):
f = models.IntegerField(null=False)
self.assertRaises(ValidationError, f.clean, None, None)
self.assertRaises(ValidationError, f.clean, '', None)
def test_charfield_raises_error_on_empty_input(self):
f = models.CharField(null=False)
self.assertRaises(ValidationError, f.clean, None, None)
def test_datefield_cleans_date(self):
f = models.DateField()
self.assertEqual(datetime.date(2008, 10, 10), f.clean('2008-10-10', None))
def test_boolean_field_doesnt_accept_empty_input(self):
f = models.BooleanField()
self.assertRaises(ValidationError, f.clean, None, None)
class BigIntegerFieldTests(django.test.TestCase): class BigIntegerFieldTests(django.test.TestCase):
def test_limits(self): def test_limits(self):
# Ensure that values that are right at the limits can be saved # Ensure that values that are right at the limits can be saved

View File

@ -24,7 +24,7 @@ def custom_create(request):
model = Article model = Article
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.cleaned_data['slug'] = 'some-other-slug' self.instance.slug = 'some-other-slug'
return super(SlugChangingArticleForm, self).save(*args, **kwargs) return super(SlugChangingArticleForm, self).save(*args, **kwargs)
return create_object(request, return create_object(request,