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
Martin Kosír <martin@martinkosir.net>
Arthur Koziel <http://arthurkoziel.com>
Honza Kral <honza.kral@gmail.com>
Meir Kriheli <http://mksoft.co.il/>
Bruce Kroeze <http://coderseye.com/>
krzysiek.pawlik@silvermedia.pl

View File

@ -578,12 +578,12 @@ class ModelAdmin(BaseModelAdmin):
"""
messages.info(request, message)
def save_form(self, request, form, change):
def save_form(self, request, form, change, commit=False):
"""
Given a ModelForm return an unsaved instance. ``change`` is True if
the object is being changed, and False if it's being added.
"""
return form.save(commit=False)
return form.save(commit=commit)
def save_model(self, request, obj, form, change):
"""
@ -757,8 +757,12 @@ class ModelAdmin(BaseModelAdmin):
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
if form.is_valid():
# Save the object, even if inline formsets haven't been
# validated yet. We need to pass the valid model to the
# formsets for validation. If the formsets do not validate, we
# will delete the object.
new_object = self.save_form(request, form, change=False, commit=True)
form_validated = True
new_object = self.save_form(request, form, change=False)
else:
form_validated = False
new_object = self.model()
@ -774,13 +778,15 @@ class ModelAdmin(BaseModelAdmin):
prefix=prefix, queryset=inline.queryset(request))
formsets.append(formset)
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=False)
form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=False)
self.log_addition(request, new_object)
return self.response_add(request, new_object)
elif form_validated:
# The form was valid, but formsets were not, so delete the
# object we saved above.
new_object.delete()
else:
# Prepare the dict of initial data from the request.
# We have to special-case M2Ms as a list of comma-separated PKs.

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.tokens import default_token_generator
from django.contrib.sites.models import Site
@ -21,6 +21,12 @@ class UserCreationForm(forms.ModelForm):
model = User
fields = ("username",)
def clean(self):
# Fill the password field so model validation won't complain about it
# being blank. We'll set it with the real value below.
self.instance.password = UNUSABLE_PASSWORD
super(UserCreationForm, self).clean()
def clean_username(self):
username = self.cleaned_data["username"]
try:
@ -34,15 +40,9 @@ class UserCreationForm(forms.ModelForm):
password2 = self.cleaned_data["password2"]
if password1 != password2:
raise forms.ValidationError(_("The two password fields didn't match."))
self.instance.set_password(password1)
return password2
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),

View File

@ -297,7 +297,11 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
# Avoid a circular import.
from django.contrib.contenttypes.models import ContentType
opts = self.model._meta
if instance is None:
self.instance = self.model()
else:
self.instance = instance
self.save_as_new = save_as_new
self.rel_name = '-'.join((
opts.app_label, opts.object_name.lower(),
self.ct_field.name, self.ct_fk_field.name,
@ -324,15 +328,19 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
))
get_default_prefix = classmethod(get_default_prefix)
def save_new(self, form, commit=True):
def _construct_form(self, i, **kwargs):
# Avoid a circular import.
from django.contrib.contenttypes.models import ContentType
kwargs = {
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
self.ct_fk_field.get_attname(): self.instance.pk,
}
new_obj = self.model(**kwargs)
return save_instance(form, new_obj, commit=commit)
form = super(BaseGenericInlineFormSet, self)._construct_form(i, **kwargs)
if self.save_as_new:
# Remove the key from the form's data, we are only creating new instances.
form.data[form.add_prefix(self.ct_fk_field.name)] = None
form.data[form.add_prefix(self.ct_field.name)] = None
# Set the GenericForeignKey value here so that the form can do its validation.
setattr(form.instance, self.ct_fk_field.attname, self.instance.pk)
setattr(form.instance, self.ct_field.attname, ContentType.objects.get_for_model(self.instance).pk)
return form
def generic_inlineformset_factory(model, form=ModelForm,
formset=BaseGenericInlineFormSet,

View File

@ -4,7 +4,8 @@ AR-specific Form helpers.
"""
from django.forms import ValidationError
from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
from django.core.validators import EMPTY_VALUES
from django.forms.fields import RegexField, CharField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _

View File

@ -2,9 +2,10 @@
Australian-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.util import smart_unicode
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -3,8 +3,9 @@
BR-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, CharField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, CharField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -2,9 +2,10 @@
Canada-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.util import smart_unicode
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -2,8 +2,9 @@
Swiss-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -2,8 +2,9 @@
Chile specific form helpers.
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import RegexField, Select, EMPTY_VALUES
from django.forms.fields import RegexField, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
Czech-specific form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Select, RegexField, Field, EMPTY_VALUES
from django.forms.fields import Select, RegexField, Field
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -2,8 +2,9 @@
DE-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -3,8 +3,9 @@
Spanish-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import RegexField, Select, EMPTY_VALUES
from django.forms.fields import RegexField, Select
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -3,8 +3,9 @@ FI-specific Form helpers
"""
import re
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
class FIZipCodeField(RegexField):

View File

@ -2,8 +2,9 @@
FR-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -5,8 +5,9 @@ ID-specific Form helpers
import re
import time
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, Select, EMPTY_VALUES
from django.forms.fields import Field, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
India-specific Form helpers.
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import gettext
import re

View File

@ -2,8 +2,9 @@
Iceland specific form helpers.
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import RegexField, EMPTY_VALUES
from django.forms.fields import RegexField
from django.forms.widgets import Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode

View File

@ -2,8 +2,9 @@
IT-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from django.contrib.localflavor.it.util import ssn_check_digit, vat_number_check_digit

View File

@ -3,8 +3,10 @@ Kuwait-specific Form helpers
"""
import re
from datetime import date
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, EMPTY_VALUES
from django.forms.fields import Field, RegexField
from django.utils.translation import gettext as _
id_re = re.compile(r'^(?P<initial>\d{1})(?P<yy>\d\d)(?P<mm>\d\d)(?P<dd>\d\d)(?P<mid>\d{4})(?P<checksum>\d{1})')

View File

@ -4,8 +4,9 @@ NL-specific Form helpers
import re
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, Select, EMPTY_VALUES
from django.forms.fields import Field, Select
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode

View File

@ -3,8 +3,9 @@ Norwegian-specific Form helpers
"""
import re, datetime
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.translation import ugettext_lazy as _
class NOZipCodeField(RegexField):

View File

@ -3,8 +3,9 @@
PE-specific Form helpers.
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import RegexField, CharField, Select, EMPTY_VALUES
from django.forms.fields import RegexField, CharField, Select
from django.utils.translation import ugettext_lazy as _
class PERegionSelect(Select):

View File

@ -2,8 +2,9 @@
PT-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
from django.forms.fields import Field, RegexField, Select
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

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

View File

@ -5,7 +5,7 @@ Swedish specific Form helpers
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.forms.fields import EMPTY_VALUES
from django.core.validators import EMPTY_VALUES
from django.contrib.localflavor.se.utils import (id_number_checksum,
validate_id_birthday, format_personal_id_number, valid_organisation,
format_organisation_number)

View File

@ -2,8 +2,9 @@
USA-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES, CharField
from django.forms.fields import Field, RegexField, Select, CharField
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re

View File

@ -4,7 +4,8 @@ UY-specific form helpers.
"""
import re
from django.forms.fields import Select, RegexField, EMPTY_VALUES
from django.core.validators import EMPTY_VALUES
from django.forms.fields import Select, RegexField
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.contrib.localflavor.uy.util import get_validation_digit

View File

@ -2,8 +2,9 @@
South Africa-specific Form helpers
"""
from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, RegexField, EMPTY_VALUES
from django.forms.fields import Field, RegexField
from django.utils.checksums import luhn
from django.utils.translation import gettext as _
import re

View File

@ -32,6 +32,42 @@ class FieldError(Exception):
"""Some kind of problem with a model field."""
pass
class ValidationError(Exception):
NON_FIELD_ERRORS = '__all__'
class BaseValidationError(Exception):
"""An error while validating data."""
def __init__(self, message, code=None, params=None):
import operator
from django.utils.encoding import force_unicode
"""
ValidationError can be passed any object that can be printed (usually
a string), a list of objects or a dictionary.
"""
if isinstance(message, dict):
self.message_dict = message
# Reduce each list of messages into a single list.
message = reduce(operator.add, message.values())
if isinstance(message, list):
self.messages = [force_unicode(msg) for msg in message]
else:
self.code = code
self.params = params
message = force_unicode(message)
self.messages = [message]
def __str__(self):
# This is needed because, without a __str__(), printing an exception
# instance would result in this:
# AttributeError: ValidationError instance has no attribute 'args'
# See http://www.python.org/doc/current/tut/node10.html#handling
if hasattr(self, 'message_dict'):
return repr(self.message_dict)
return repr(self.messages)
class ValidationError(BaseValidationError):
pass
class UnresolvableValidationError(BaseValidationError):
"""Validation error that cannot be resolved by the user."""
pass

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
from itertools import izip
import django.db.models.manager # Imported to register signal handler.
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
from django.core import validators
from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q
@ -12,9 +13,11 @@ from django.db.models.options import Options
from django.db import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS
from django.db.models import signals
from django.db.models.loading import register_models, get_model
from django.utils.translation import ugettext_lazy as _
import django.utils.copycompat as copy
from django.utils.functional import curry
from django.utils.encoding import smart_str, force_unicode, smart_unicode
from django.utils.text import get_text_list, capfirst
from django.conf import settings
class ModelBase(type):
@ -639,6 +642,180 @@ class Model(object):
def prepare_database_save(self, unused):
return self.pk
def validate(self):
"""
Hook for doing any extra model-wide validation after clean() has been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field defined by NON_FIELD_ERRORS.
"""
self.validate_unique()
def validate_unique(self):
unique_checks, date_checks = self._get_unique_checks()
errors = self._perform_unique_checks(unique_checks)
date_errors = self._perform_date_checks(date_checks)
for k, v in date_errors.items():
errors.setdefault(k, []).extend(v)
if errors:
raise ValidationError(errors)
def _get_unique_checks(self):
from django.db.models.fields import FieldDoesNotExist, Field as ModelField
unique_checks = list(self._meta.unique_together)
# these are checks for the unique_for_<date/year/month>
date_checks = []
# Gather a list of checks for fields declared as unique and add them to
# the list of checks. Again, skip empty fields and any that did not validate.
for f in self._meta.fields:
name = f.name
if f.unique:
unique_checks.append((name,))
if f.unique_for_date:
date_checks.append(('date', name, f.unique_for_date))
if f.unique_for_year:
date_checks.append(('year', name, f.unique_for_year))
if f.unique_for_month:
date_checks.append(('month', name, f.unique_for_month))
return unique_checks, date_checks
def _perform_unique_checks(self, unique_checks):
errors = {}
for unique_check in unique_checks:
# Try to look up an existing object with the same values as this
# object's values for all the unique field.
lookup_kwargs = {}
for field_name in unique_check:
f = self._meta.get_field(field_name)
lookup_value = getattr(self, f.attname)
if f.null and lookup_value is None:
# no value, skip the lookup
continue
if f.primary_key and not getattr(self, '_adding', False):
# no need to check for unique primary key when editting
continue
lookup_kwargs[str(field_name)] = lookup_value
# some fields were skipped, no reason to do the check
if len(unique_check) != len(lookup_kwargs.keys()):
continue
qs = self.__class__._default_manager.filter(**lookup_kwargs)
# Exclude the current object from the query if we are editing an
# instance (as opposed to creating a new one)
if not getattr(self, '_adding', False) and self.pk is not None:
qs = qs.exclude(pk=self.pk)
# This cute trick with extra/values is the most efficient way to
# tell if a particular query returns any results.
if qs.extra(select={'a': 1}).values('a').order_by():
if len(unique_check) == 1:
key = unique_check[0]
else:
key = NON_FIELD_ERRORS
errors.setdefault(key, []).append(self.unique_error_message(unique_check))
return errors
def _perform_date_checks(self, date_checks):
errors = {}
for lookup_type, field, unique_for in date_checks:
lookup_kwargs = {}
# there's a ticket to add a date lookup, we can remove this special
# case if that makes it's way in
date = getattr(self, unique_for)
if lookup_type == 'date':
lookup_kwargs['%s__day' % unique_for] = date.day
lookup_kwargs['%s__month' % unique_for] = date.month
lookup_kwargs['%s__year' % unique_for] = date.year
else:
lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(date, lookup_type)
lookup_kwargs[field] = getattr(self, field)
qs = self.__class__._default_manager.filter(**lookup_kwargs)
# Exclude the current object from the query if we are editing an
# instance (as opposed to creating a new one)
if not getattr(self, '_adding', False) and self.pk is not None:
qs = qs.exclude(pk=self.pk)
# This cute trick with extra/values is the most efficient way to
# tell if a particular query returns any results.
if qs.extra(select={'a': 1}).values('a').order_by():
errors.setdefault(field, []).append(
self.date_error_message(lookup_type, field, unique_for)
)
return errors
def date_error_message(self, lookup_type, field, unique_for):
opts = self._meta
return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
'field_name': unicode(capfirst(opts.get_field(field).verbose_name)),
'date_field': unicode(capfirst(opts.get_field(unique_for).verbose_name)),
'lookup': lookup_type,
}
def unique_error_message(self, unique_check):
opts = self._meta
model_name = capfirst(opts.verbose_name)
# A unique field
if len(unique_check) == 1:
field_name = unique_check[0]
field_label = capfirst(opts.get_field(field_name).verbose_name)
# Insert the error into the error dict, very sneaky
return _(u"%(model_name)s with this %(field_label)s already exists.") % {
'model_name': unicode(model_name),
'field_label': unicode(field_label)
}
# unique_together
else:
field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check)
field_labels = get_text_list(field_labels, _('and'))
return _(u"%(model_name)s with this %(field_label)s already exists.") % {
'model_name': unicode(model_name),
'field_label': unicode(field_labels)
}
def full_validate(self, exclude=[]):
"""
Cleans all fields and raises ValidationError containing message_dict
of all validation errors if any occur.
"""
errors = {}
for f in self._meta.fields:
if f.name in exclude:
continue
try:
setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
except ValidationError, e:
errors[f.name] = e.messages
# Form.clean() is run even if other validation fails, so do the
# same with Model.validate() for consistency.
try:
self.validate()
except ValidationError, e:
if hasattr(e, 'message_dict'):
if errors:
for k, v in e.message_dict.items():
errors.set_default(k, []).extend(v)
else:
errors = e.message_dict
else:
errors[NON_FIELD_ERRORS] = e.messages
if errors:
raise ValidationError(errors)
############################################
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #

View File

@ -13,12 +13,12 @@ from django.db.models.query_utils import QueryWrapper
from django.dispatch import dispatcher
from django.conf import settings
from django import forms
from django.core import exceptions
from django.core import exceptions, validators
from django.utils.datastructures import DictWrapper
from django.utils.functional import curry
from django.utils.itercompat import tee
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy, ugettext as _
from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils import datetime_safe
@ -60,6 +60,12 @@ class Field(object):
# creates, creation_counter is used for all user-specified fields.
creation_counter = 0
auto_creation_counter = -1
default_validators = [] # Default set of validators
default_error_messages = {
'invalid_choice': _(u'Value %r is not a valid choice.'),
'null': _(u'This field cannot be null.'),
'blank': _(u'This field cannot be blank.'),
}
# Generic field type description, usually overriden by subclasses
def _description(self):
@ -73,7 +79,8 @@ class Field(object):
db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
serialize=True, unique_for_date=None, unique_for_month=None,
unique_for_year=None, choices=None, help_text='', db_column=None,
db_tablespace=None, auto_created=False):
db_tablespace=None, auto_created=False, validators=[],
error_messages=None):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@ -106,6 +113,42 @@ class Field(object):
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
self.validators = self.default_validators + validators
messages = {}
for c in reversed(self.__class__.__mro__):
messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
def __getstate__(self):
"""
Pickling support.
"""
from django.utils.functional import Promise
obj_dict = self.__dict__.copy()
items = []
translated_keys = []
for k, v in self.error_messages.items():
if isinstance(v, Promise):
args = getattr(v, '_proxy____args', None)
if args:
translated_keys.append(k)
v = args[0]
items.append((k,v))
obj_dict['_translated_keys'] = translated_keys
obj_dict['error_messages'] = dict(items)
return obj_dict
def __setstate__(self, obj_dict):
"""
Unpickling support.
"""
translated_keys = obj_dict.pop('_translated_keys')
self.__dict__.update(obj_dict)
for k in translated_keys:
self.error_messages[k] = _(self.error_messages[k])
def __cmp__(self, other):
# This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter)
@ -127,6 +170,54 @@ class Field(object):
"""
return value
def run_validators(self, value):
if value in validators.EMPTY_VALUES:
return
errors = []
for v in self.validators:
try:
v(value)
except exceptions.ValidationError, e:
if hasattr(e, 'code') and e.code in self.error_messages:
message = self.error_messages[e.code]
if e.params:
message = message % e.params
errors.append(message)
else:
errors.extend(e.messages)
if errors:
raise exceptions.ValidationError(errors)
def validate(self, value, model_instance):
"""
Validates value and throws ValidationError. Subclasses should override
this to provide validation logic.
"""
if not self.editable:
# Skip validation for non-editable fields.
return
if self._choices and value:
if not value in dict(self.choices):
raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
if value is None and not self.null:
raise exceptions.ValidationError(self.error_messages['null'])
if not self.blank and value in validators.EMPTY_VALUES:
raise exceptions.ValidationError(self.error_messages['blank'])
def clean(self, value, model_instance):
"""
Convert the value's type and run validation. Validation errors from to_python
and validate are propagated. The correct value is returned if no error is
raised.
"""
value = self.to_python(value)
self.validate(value, model_instance)
self.run_validators(value)
return value
def db_type(self, connection):
"""
Returns the database column data type for this field, for the provided
@ -377,9 +468,12 @@ class Field(object):
return getattr(obj, self.attname)
class AutoField(Field):
description = ugettext_lazy("Integer")
description = _("Integer")
empty_strings_allowed = False
default_error_messages = {
'invalid': _(u'This value must be an integer.'),
}
def __init__(self, *args, **kwargs):
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
kwargs['blank'] = True
@ -391,8 +485,10 @@ class AutoField(Field):
try:
return int(value)
except (TypeError, ValueError):
raise exceptions.ValidationError(
_("This value must be an integer."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def validate(self, value, model_instance):
pass
def get_prep_value(self, value):
if value is None:
@ -410,7 +506,10 @@ class AutoField(Field):
class BooleanField(Field):
empty_strings_allowed = False
description = ugettext_lazy("Boolean (Either True or False)")
default_error_messages = {
'invalid': _(u'This value must be either True or False.'),
}
description = _("Boolean (Either True or False)")
def __init__(self, *args, **kwargs):
kwargs['blank'] = True
if 'default' not in kwargs and not kwargs.get('null'):
@ -424,8 +523,7 @@ class BooleanField(Field):
if value in (True, False): return value
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
raise exceptions.ValidationError(
_("This value must be either True or False."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@ -453,36 +551,35 @@ class BooleanField(Field):
return super(BooleanField, self).formfield(**defaults)
class CharField(Field):
description = ugettext_lazy("String (up to %(max_length)s)")
description = _("String (up to %(max_length)s)")
def __init__(self, *args, **kwargs):
super(CharField, self).__init__(*args, **kwargs)
self.validators.append(validators.MaxLengthValidator(self.max_length))
def get_internal_type(self):
return "CharField"
def to_python(self, value):
if isinstance(value, basestring):
if isinstance(value, basestring) or value is None:
return value
if value is None:
if self.null:
return value
else:
raise exceptions.ValidationError(
ugettext_lazy("This field cannot be null."))
return smart_unicode(value)
def formfield(self, **kwargs):
# Passing max_length to forms.CharField means that the value's length
# will be validated twice. This is considered acceptable since we want
# the value in the form field (to pass into widget for example).
defaults = {'max_length': self.max_length}
defaults.update(kwargs)
return super(CharField, self).formfield(**defaults)
# TODO: Maybe move this into contrib, because it's specialized.
class CommaSeparatedIntegerField(CharField):
description = ugettext_lazy("Comma-separated integers")
default_validators = [validators.validate_comma_separated_integer_list]
description = _("Comma-separated integers")
def formfield(self, **kwargs):
defaults = {
'form_class': forms.RegexField,
'regex': '^[\d,]+$',
'max_length': self.max_length,
'error_messages': {
'invalid': _(u'Enter only digits separated by commas.'),
}
@ -493,9 +590,13 @@ class CommaSeparatedIntegerField(CharField):
ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
class DateField(Field):
description = ugettext_lazy("Date (without time)")
description = _("Date (without time)")
empty_strings_allowed = False
default_error_messages = {
'invalid': _('Enter a valid date in YYYY-MM-DD format.'),
'invalid_date': _('Invalid date: %s'),
}
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
#HACKs : auto_now_add/auto_now should be done as a default or a pre_save.
@ -516,8 +617,7 @@ class DateField(Field):
return value
if not ansi_date_re.search(value):
raise exceptions.ValidationError(
_('Enter a valid date in YYYY-MM-DD format.'))
raise exceptions.ValidationError(self.error_messages['invalid'])
# Now that we have the date string in YYYY-MM-DD format, check to make
# sure it's a valid date.
# We could use time.strptime here and catch errors, but datetime.date
@ -526,7 +626,7 @@ class DateField(Field):
try:
return datetime.date(year, month, day)
except ValueError, e:
msg = _('Invalid date: %s') % _(str(e))
msg = self.error_messages['invalid_date'] % _(str(e))
raise exceptions.ValidationError(msg)
def pre_save(self, model_instance, add):
@ -575,7 +675,10 @@ class DateField(Field):
return super(DateField, self).formfield(**defaults)
class DateTimeField(DateField):
description = ugettext_lazy("Date (with time)")
default_error_messages = {
'invalid': _(u'Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'),
}
description = _("Date (with time)")
def get_internal_type(self):
return "DateTimeField"
@ -596,8 +699,7 @@ class DateTimeField(DateField):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
raise exceptions.ValidationError(
_('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@ -614,8 +716,7 @@ class DateTimeField(DateField):
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3],
**kwargs)
except ValueError:
raise exceptions.ValidationError(
_('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_value(self, value):
return self.to_python(value)
@ -642,7 +743,11 @@ class DateTimeField(DateField):
class DecimalField(Field):
empty_strings_allowed = False
description = ugettext_lazy("Decimal number")
default_error_messages = {
'invalid': _(u'This value must be a decimal number.'),
}
description = _("Decimal number")
def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, verbose_name, name, **kwargs)
@ -656,8 +761,7 @@ class DecimalField(Field):
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
raise exceptions.ValidationError(
_("This value must be a decimal number."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def _format(self, value):
if isinstance(value, basestring) or value is None:
@ -696,18 +800,15 @@ class DecimalField(Field):
return super(DecimalField, self).formfield(**defaults)
class EmailField(CharField):
description = ugettext_lazy("E-mail address")
default_validators = [validators.validate_email]
description = _("E-mail address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 75)
CharField.__init__(self, *args, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': forms.EmailField}
defaults.update(kwargs)
return super(EmailField, self).formfield(**defaults)
class FilePathField(Field):
description = ugettext_lazy("File path")
description = _("File path")
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
@ -729,7 +830,10 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
description = ugettext_lazy("Floating point number")
default_error_messages = {
'invalid': _("This value must be a float."),
}
description = _("Floating point number")
def get_prep_value(self, value):
if value is None:
@ -745,8 +849,7 @@ class FloatField(Field):
try:
return float(value)
except (TypeError, ValueError):
raise exceptions.ValidationError(
_("This value must be a float."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField}
@ -755,7 +858,10 @@ class FloatField(Field):
class IntegerField(Field):
empty_strings_allowed = False
description = ugettext_lazy("Integer")
default_error_messages = {
'invalid': _("This value must be a float."),
}
description = _("Integer")
def get_prep_value(self, value):
if value is None:
@ -771,8 +877,7 @@ class IntegerField(Field):
try:
return int(value)
except (TypeError, ValueError):
raise exceptions.ValidationError(
_("This value must be an integer."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField}
@ -781,7 +886,7 @@ class IntegerField(Field):
class BigIntegerField(IntegerField):
empty_strings_allowed = False
description = ugettext_lazy("Big (8 byte) integer")
description = _("Big (8 byte) integer")
MAX_BIGINT = 9223372036854775807
def get_internal_type(self):
return "BigIntegerField"
@ -794,7 +899,7 @@ class BigIntegerField(IntegerField):
class IPAddressField(Field):
empty_strings_allowed = False
description = ugettext_lazy("IP address")
description = _("IP address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15
Field.__init__(self, *args, **kwargs)
@ -809,7 +914,11 @@ class IPAddressField(Field):
class NullBooleanField(Field):
empty_strings_allowed = False
description = ugettext_lazy("Boolean (Either True, False or None)")
default_error_messages = {
'invalid': _("This value must be either None, True or False."),
}
description = _("Boolean (Either True, False or None)")
def __init__(self, *args, **kwargs):
kwargs['null'] = True
Field.__init__(self, *args, **kwargs)
@ -822,8 +931,7 @@ class NullBooleanField(Field):
if value in ('None',): return None
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
raise exceptions.ValidationError(
_("This value must be either None, True or False."))
raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@ -849,7 +957,7 @@ class NullBooleanField(Field):
return super(NullBooleanField, self).formfield(**defaults)
class PositiveIntegerField(IntegerField):
description = ugettext_lazy("Integer")
description = _("Integer")
def get_internal_type(self):
return "PositiveIntegerField"
@ -860,7 +968,7 @@ class PositiveIntegerField(IntegerField):
return super(PositiveIntegerField, self).formfield(**defaults)
class PositiveSmallIntegerField(IntegerField):
description = ugettext_lazy("Integer")
description = _("Integer")
def get_internal_type(self):
return "PositiveSmallIntegerField"
@ -870,7 +978,7 @@ class PositiveSmallIntegerField(IntegerField):
return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField):
description = ugettext_lazy("String (up to %(max_length)s)")
description = _("String (up to %(max_length)s)")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50)
# Set db_index=True unless it's been set manually.
@ -887,13 +995,13 @@ class SlugField(CharField):
return super(SlugField, self).formfield(**defaults)
class SmallIntegerField(IntegerField):
description = ugettext_lazy("Integer")
description = _("Integer")
def get_internal_type(self):
return "SmallIntegerField"
class TextField(Field):
description = ugettext_lazy("Text")
description = _("Text")
def get_internal_type(self):
return "TextField"
@ -904,9 +1012,12 @@ class TextField(Field):
return super(TextField, self).formfield(**defaults)
class TimeField(Field):
description = ugettext_lazy("Time")
description = _("Time")
empty_strings_allowed = False
default_error_messages = {
'invalid': _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'),
}
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
if auto_now or auto_now_add:
@ -935,8 +1046,7 @@ class TimeField(Field):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
raise exceptions.ValidationError(
_('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@ -949,8 +1059,7 @@ class TimeField(Field):
return datetime.time(*time.strptime(value, '%H:%M')[3:5],
**kwargs)
except ValueError:
raise exceptions.ValidationError(
_('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
raise exceptions.ValidationError(self.error_messages['invalid'])
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@ -983,21 +1092,17 @@ class TimeField(Field):
return super(TimeField, self).formfield(**defaults)
class URLField(CharField):
description = ugettext_lazy("URL")
description = _("URL")
def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
self.verify_exists = verify_exists
CharField.__init__(self, verbose_name, name, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': forms.URLField, 'verify_exists': self.verify_exists}
defaults.update(kwargs)
return super(URLField, self).formfield(**defaults)
self.validators.append(validators.URLValidator(verify_exists=verify_exists))
class XMLField(TextField):
description = ugettext_lazy("XML text")
description = _("XML text")
def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
self.schema_path = schema_path
Field.__init__(self, verbose_name, name, **kwargs)

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_utils import QueryWrapper
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _
from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
from django.utils.functional import curry
from django.core import exceptions
from django import forms
@ -473,7 +473,7 @@ def create_many_related_manager(superclass, rel=False):
if not rel.through._meta.auto_created:
opts = through._meta
raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
new_obj = super(ManyRelatedManager, self).using(self.instance._state.db).create(**kwargs)
new_obj = super(ManyRelatedManager, self).create(**kwargs)
self.add(new_obj)
return new_obj
create.alters_data = True
@ -708,7 +708,10 @@ class ManyToManyRel(object):
class ForeignKey(RelatedField, Field):
empty_strings_allowed = False
description = ugettext_lazy("Foreign Key (type determined by related field)")
default_error_messages = {
'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
}
description = _("Foreign Key (type determined by related field)")
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try:
to_name = to._meta.object_name.lower()
@ -731,6 +734,18 @@ class ForeignKey(RelatedField, Field):
self.db_index = True
def validate(self, value, model_instance):
if self.rel.parent_link:
return
super(ForeignKey, self).validate(value, model_instance)
if not value:
return
try:
self.rel.to._default_manager.get(**{self.rel.field_name:value})
except self.rel.to.DoesNotExist, e:
raise exceptions.ValidationError(
self.error_messages['invalid'] % {'model': self.rel.to._meta.verbose_name, 'pk': value})
def get_attname(self):
return '%s_id' % self.name
@ -812,7 +827,7 @@ class OneToOneField(ForeignKey):
always returns the object pointed to (since there will only ever be one),
rather than returning a list.
"""
description = ugettext_lazy("One-to-one relationship")
description = _("One-to-one relationship")
def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@ -826,6 +841,12 @@ class OneToOneField(ForeignKey):
return None
return super(OneToOneField, self).formfield(**kwargs)
def save_form_data(self, instance, data):
if isinstance(data, self.rel.to):
setattr(instance, self.name, data)
else:
setattr(instance, self.attname, data)
def create_many_to_many_intermediary_model(field, klass):
from django.db import models
managed = True
@ -866,7 +887,7 @@ def create_many_to_many_intermediary_model(field, klass):
})
class ManyToManyField(RelatedField, Field):
description = ugettext_lazy("Many-to-many relationship")
description = _("Many-to-many relationship")
def __init__(self, to, **kwargs):
try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
@ -886,7 +907,7 @@ class ManyToManyField(RelatedField, Field):
Field.__init__(self, **kwargs)
msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
self.help_text = string_concat(self.help_text, ' ', msg)
def get_choices_default(self):

View File

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

View File

@ -14,15 +14,21 @@ try:
except ImportError:
from StringIO import StringIO
import django.core.exceptions
from django.core.exceptions import ValidationError
from django.core import validators
import django.utils.copycompat as copy
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str
from django.utils.formats import get_format
from django.utils.functional import lazy
from util import ErrorList, ValidationError
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
# Provide this import for backwards compatibility.
from django.core.validators import EMPTY_VALUES
from util import ErrorList
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \
FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \
DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
__all__ = (
'Field', 'CharField', 'IntegerField',
@ -36,9 +42,6 @@ __all__ = (
'TypedChoiceField'
)
# These values, if given to to_python(), will trigger the self.required check.
EMPTY_VALUES = (None, '')
def en_format(name):
"""
Helper function to stay backward compatible.
@ -57,6 +60,7 @@ DEFAULT_DATETIME_INPUT_FORMATS = lazy(lambda: en_format('DATETIME_INPUT_FORMATS'
class Field(object):
widget = TextInput # Default widget to use when rendering this type of Field.
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
default_validators = [] # Default set of validators
default_error_messages = {
'required': _(u'This field is required.'),
'invalid': _(u'Enter a valid value.'),
@ -66,7 +70,8 @@ class Field(object):
creation_counter = 0
def __init__(self, required=True, widget=None, label=None, initial=None,
help_text=None, error_messages=None, show_hidden_initial=False):
help_text=None, error_messages=None, show_hidden_initial=False,
validators=[]):
# required -- Boolean that specifies whether the field is required.
# True by default.
# widget -- A Widget class, or instance of a Widget class, that should
@ -82,6 +87,7 @@ class Field(object):
# help_text -- An optional string to use as "help text" for this Field.
# show_hidden_initial -- Boolean that specifies if it is needed to render a
# hidden widget with initial value after widget.
# validators -- List of addtional validators to use
if label is not None:
label = smart_unicode(label)
self.required, self.label, self.initial = required, label, initial
@ -105,16 +111,39 @@ class Field(object):
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
def set_class_error_messages(messages, klass):
for base_class in klass.__bases__:
set_class_error_messages(messages, base_class)
messages.update(getattr(klass, 'default_error_messages', {}))
messages = {}
set_class_error_messages(messages, self.__class__)
for c in reversed(self.__class__.__mro__):
messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
self.validators = self.default_validators + validators
def to_python(self, value):
return value
def validate(self, value):
if value in validators.EMPTY_VALUES and self.required:
raise ValidationError(self.error_messages['required'])
def run_validators(self, value):
if value in validators.EMPTY_VALUES:
return
errors = []
for v in self.validators:
try:
v(value)
except ValidationError, e:
if hasattr(e, 'code') and e.code in self.error_messages:
message = self.error_messages[e.code]
if e.params:
message = message % e.params
errors.append(message)
else:
errors.extend(e.messages)
if errors:
raise ValidationError(errors)
def clean(self, value):
"""
Validates the given value and returns its "cleaned" value as an
@ -122,8 +151,9 @@ class Field(object):
Raises ValidationError for any errors.
"""
if self.required and value in EMPTY_VALUES:
raise ValidationError(self.error_messages['required'])
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
def widget_attrs(self, widget):
@ -141,27 +171,19 @@ class Field(object):
return result
class CharField(Field):
default_error_messages = {
'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'),
'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'),
}
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
super(CharField, self).__init__(*args, **kwargs)
if min_length is not None:
self.validators.append(validators.MinLengthValidator(min_length))
if max_length is not None:
self.validators.append(validators.MaxLengthValidator(max_length))
def clean(self, value):
"Validates max_length and min_length. Returns a Unicode object."
super(CharField, self).clean(value)
if value in EMPTY_VALUES:
def to_python(self, value):
"Returns a Unicode object."
if value in validators.EMPTY_VALUES:
return u''
value = smart_unicode(value)
value_length = len(value)
if self.max_length is not None and value_length > self.max_length:
raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
if self.min_length is not None and value_length < self.min_length:
raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
return value
return smart_unicode(value)
def widget_attrs(self, widget):
if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
@ -171,87 +193,82 @@ class CharField(Field):
class IntegerField(Field):
default_error_messages = {
'invalid': _(u'Enter a whole number.'),
'max_value': _(u'Ensure this value is less than or equal to %s.'),
'min_value': _(u'Ensure this value is greater than or equal to %s.'),
'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
}
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
super(IntegerField, self).__init__(*args, **kwargs)
def clean(self, value):
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
def to_python(self, value):
"""
Validates that int() can be called on the input. Returns the result
of int(). Returns None for empty values.
"""
super(IntegerField, self).clean(value)
if value in EMPTY_VALUES:
value = super(IntegerField, self).to_python(value)
if value in validators.EMPTY_VALUES:
return None
try:
value = int(str(value))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'])
if self.max_value is not None and value > self.max_value:
raise ValidationError(self.error_messages['max_value'] % self.max_value)
if self.min_value is not None and value < self.min_value:
raise ValidationError(self.error_messages['min_value'] % self.min_value)
return value
class FloatField(Field):
class FloatField(IntegerField):
default_error_messages = {
'invalid': _(u'Enter a number.'),
'max_value': _(u'Ensure this value is less than or equal to %s.'),
'min_value': _(u'Ensure this value is greater than or equal to %s.'),
}
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
Field.__init__(self, *args, **kwargs)
def clean(self, value):
def to_python(self, value):
"""
Validates that float() can be called on the input. Returns a float.
Returns None for empty values.
Validates that float() can be called on the input. Returns the result
of float(). Returns None for empty values.
"""
super(FloatField, self).clean(value)
if not self.required and value in EMPTY_VALUES:
value = super(IntegerField, self).to_python(value)
if value in validators.EMPTY_VALUES:
return None
try:
# We always accept dot as decimal separator
if isinstance(value, str) or isinstance(value, unicode):
value = float(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'])
if self.max_value is not None and value > self.max_value:
raise ValidationError(self.error_messages['max_value'] % self.max_value)
if self.min_value is not None and value < self.min_value:
raise ValidationError(self.error_messages['min_value'] % self.min_value)
return value
class DecimalField(Field):
default_error_messages = {
'invalid': _(u'Enter a number.'),
'max_value': _(u'Ensure this value is less than or equal to %s.'),
'min_value': _(u'Ensure this value is greater than or equal to %s.'),
'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
'max_digits': _('Ensure that there are no more than %s digits in total.'),
'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
}
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, *args, **kwargs)
def clean(self, value):
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
def to_python(self, value):
"""
Validates that the input is a decimal number. Returns a Decimal
instance. Returns None for empty values. Ensures that there are no more
than max_digits in the number, and no more than decimal_places digits
after the decimal point.
"""
super(DecimalField, self).clean(value)
if not self.required and value in EMPTY_VALUES:
if value in validators.EMPTY_VALUES:
return None
value = smart_str(value).strip()
try:
@ -260,7 +277,12 @@ class DecimalField(Field):
value = Decimal(value.replace(get_format('DECIMAL_SEPARATOR'), '.'))
except DecimalException:
raise ValidationError(self.error_messages['invalid'])
return value
def validate(self, value):
super(DecimalField, self).validate(value)
if value in validators.EMPTY_VALUES:
return
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
@ -273,10 +295,6 @@ class DecimalField(Field):
digits = decimals
whole_digits = digits - decimals
if self.max_value is not None and value > self.max_value:
raise ValidationError(self.error_messages['max_value'] % self.max_value)
if self.min_value is not None and value < self.min_value:
raise ValidationError(self.error_messages['min_value'] % self.min_value)
if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
if self.decimal_places is not None and decimals > self.decimal_places:
@ -295,13 +313,12 @@ class DateField(Field):
super(DateField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
def clean(self, value):
def to_python(self, value):
"""
Validates that the input can be converted to a date. Returns a Python
datetime.date object.
"""
super(DateField, self).clean(value)
if value in EMPTY_VALUES:
if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.datetime):
return value.date()
@ -324,13 +341,12 @@ class TimeField(Field):
super(TimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
def clean(self, value):
def to_python(self, value):
"""
Validates that the input can be converted to a time. Returns a Python
datetime.time object.
"""
super(TimeField, self).clean(value)
if value in EMPTY_VALUES:
if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.time):
return value
@ -351,13 +367,12 @@ class DateTimeField(Field):
super(DateTimeField, self).__init__(*args, **kwargs)
self.input_formats = input_formats
def clean(self, value):
def to_python(self, value):
"""
Validates that the input can be converted to a datetime. Returns a
Python datetime.datetime object.
"""
super(DateTimeField, self).clean(value)
if value in EMPTY_VALUES:
if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.datetime):
return value
@ -392,40 +407,13 @@ class RegexField(CharField):
if isinstance(regex, basestring):
regex = re.compile(regex)
self.regex = regex
self.validators.append(validators.RegexValidator(regex=regex))
def clean(self, value):
"""
Validates that the input matches the regular expression. Returns a
Unicode object.
"""
value = super(RegexField, self).clean(value)
if value == u'':
return value
if not self.regex.search(value):
raise ValidationError(self.error_messages['invalid'])
return value
email_re = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE) # domain
class EmailField(RegexField):
class EmailField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid e-mail address.'),
}
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
RegexField.__init__(self, email_re, max_length, min_length, *args,
**kwargs)
try:
from django.conf import settings
URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
except ImportError:
# It's OK if Django settings aren't configured.
URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
default_validators = [validators.validate_email]
class FileField(Field):
widget = FileInput
@ -440,12 +428,9 @@ class FileField(Field):
self.max_length = kwargs.pop('max_length', None)
super(FileField, self).__init__(*args, **kwargs)
def clean(self, data, initial=None):
super(FileField, self).clean(initial or data)
if not self.required and data in EMPTY_VALUES:
def to_python(self, data):
if data in validators.EMPTY_VALUES:
return None
elif not data and initial:
return initial
# UploadedFile objects should have name and size attributes.
try:
@ -464,21 +449,24 @@ class FileField(Field):
return data
def clean(self, data, initial=None):
if not data and initial:
return initial
return super(FileField, self).clean(data)
class ImageField(FileField):
default_error_messages = {
'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
}
def clean(self, data, initial=None):
def to_python(self, data):
"""
Checks that the file-upload field data contains a valid image (GIF, JPG,
PNG, possibly others -- whatever the Python Imaging Library supports).
"""
f = super(ImageField, self).clean(data, initial)
f = super(ImageField, self).to_python(data)
if f is None:
return None
elif not data and initial:
return initial
from PIL import Image
# We need to get a file object for PIL. We might have a path or we might
@ -517,59 +505,34 @@ class ImageField(FileField):
f.seek(0)
return f
url_re = re.compile(
r'^https?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
r'localhost|' #localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|/\S+)$', re.IGNORECASE)
class URLField(RegexField):
class URLField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid URL.'),
'invalid_link': _(u'This URL appears to be a broken link.'),
}
def __init__(self, max_length=None, min_length=None, verify_exists=False,
validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
super(URLField, self).__init__(url_re, max_length, min_length, *args,
validator_user_agent=validators.URL_VALIDATOR_USER_AGENT, *args, **kwargs):
super(URLField, self).__init__(max_length, min_length, *args,
**kwargs)
self.verify_exists = verify_exists
self.user_agent = validator_user_agent
self.validators.append(validators.URLValidator(verify_exists=verify_exists, validator_user_agent=validator_user_agent))
def clean(self, value):
def to_python(self, value):
if value:
if '://' not in value:
# If no URL scheme given, assume http://
if value and '://' not in value:
value = u'http://%s' % value
# If no URL path given, assume /
if value and not urlparse.urlsplit(value)[2]:
value += '/'
value = super(URLField, self).clean(value)
if value == u'':
return value
if self.verify_exists:
import urllib2
headers = {
"Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
"Accept-Language": "en-us,en;q=0.5",
"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Connection": "close",
"User-Agent": self.user_agent,
}
try:
req = urllib2.Request(value, None, headers)
u = urllib2.urlopen(req)
except ValueError:
raise ValidationError(self.error_messages['invalid'])
except: # urllib2.URLError, httplib.InvalidURL, etc.
raise ValidationError(self.error_messages['invalid_link'])
return value
url_fields = list(urlparse.urlsplit(value))
if not url_fields[2]:
# the path portion may need to be added before query params
url_fields[2] = '/'
value = urlparse.urlunsplit(url_fields)
return super(URLField, self).to_python(value)
class BooleanField(Field):
widget = CheckboxInput
def clean(self, value):
def to_python(self, value):
"""Returns a Python boolean object."""
# Explicitly check for the string 'False', which is what a hidden field
# will submit for False. Also check for '0', since this is what
@ -579,7 +542,7 @@ class BooleanField(Field):
value = False
else:
value = bool(value)
super(BooleanField, self).clean(value)
value = super(BooleanField, self).to_python(value)
if not value and self.required:
raise ValidationError(self.error_messages['required'])
return value
@ -591,7 +554,7 @@ class NullBooleanField(BooleanField):
"""
widget = NullBooleanSelect
def clean(self, value):
def to_python(self, value):
"""
Explicitly checks for the string 'True' and 'False', which is what a
hidden field will submit for True and False, and for '1' and '0', which
@ -605,6 +568,9 @@ class NullBooleanField(BooleanField):
else:
return None
def validate(self, value):
pass
class ChoiceField(Field):
widget = Select
default_error_messages = {
@ -613,8 +579,8 @@ class ChoiceField(Field):
def __init__(self, choices=(), required=True, widget=None, label=None,
initial=None, help_text=None, *args, **kwargs):
super(ChoiceField, self).__init__(required, widget, label, initial,
help_text, *args, **kwargs)
super(ChoiceField, self).__init__(required=required, widget=widget, label=label,
initial=initial, help_text=help_text, *args, **kwargs)
self.choices = choices
def _get_choices(self):
@ -628,19 +594,19 @@ class ChoiceField(Field):
choices = property(_get_choices, _set_choices)
def clean(self, value):
def to_python(self, value):
"Returns a Unicode object."
if value in validators.EMPTY_VALUES:
return u''
return smart_unicode(value)
def validate(self, value):
"""
Validates that the input is in self.choices.
"""
value = super(ChoiceField, self).clean(value)
if value in EMPTY_VALUES:
value = u''
value = smart_unicode(value)
if value == u'':
return value
if not self.valid_value(value):
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
@ -661,27 +627,24 @@ class TypedChoiceField(ChoiceField):
self.empty_value = kwargs.pop('empty_value', '')
super(TypedChoiceField, self).__init__(*args, **kwargs)
def clean(self, value):
def to_python(self, value):
"""
Validate that the value is in self.choices and can be coerced to the
right type.
"""
value = super(TypedChoiceField, self).clean(value)
if value == self.empty_value or value in EMPTY_VALUES:
value = super(TypedChoiceField, self).to_python(value)
super(TypedChoiceField, self).validate(value)
if value == self.empty_value or value in validators.EMPTY_VALUES:
return self.empty_value
# Hack alert: This field is purpose-made to use with Field.to_python as
# a coercion function so that ModelForms with choices work. However,
# Django's Field.to_python raises
# django.core.exceptions.ValidationError, which is a *different*
# exception than django.forms.util.ValidationError. So we need to catch
# both.
try:
value = self.coerce(value)
except (ValueError, TypeError, django.core.exceptions.ValidationError):
except (ValueError, TypeError, ValidationError):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
def validate(self, value):
pass
class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput
widget = SelectMultiple
@ -690,22 +653,23 @@ class MultipleChoiceField(ChoiceField):
'invalid_list': _(u'Enter a list of values.'),
}
def clean(self, value):
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'])
return [smart_unicode(val) for val in value]
def validate(self, value):
"""
Validates that the input is a list or tuple.
"""
if self.required and not value:
raise ValidationError(self.error_messages['required'])
elif not self.required and not value:
return []
if not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'])
new_value = [smart_unicode(val) for val in value]
# Validate that each value in the value list is in self.choices.
for val in new_value:
for val in value:
if not self.valid_value(val):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
return new_value
class ComboField(Field):
"""
@ -760,6 +724,9 @@ class MultiValueField(Field):
f.required = False
self.fields = fields
def validate(self, value):
pass
def clean(self, value):
"""
Validates every value in the given list. A value is validated against
@ -772,7 +739,7 @@ class MultiValueField(Field):
clean_data = []
errors = ErrorList()
if not value or isinstance(value, (list, tuple)):
if not value or not [v for v in value if v not in EMPTY_VALUES]:
if not value or not [v for v in value if v not in validators.EMPTY_VALUES]:
if self.required:
raise ValidationError(self.error_messages['required'])
else:
@ -784,7 +751,7 @@ class MultiValueField(Field):
field_value = value[i]
except IndexError:
field_value = None
if self.required and field_value in EMPTY_VALUES:
if self.required and field_value in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['required'])
try:
clean_data.append(field.clean(field_value))
@ -795,7 +762,10 @@ class MultiValueField(Field):
errors.extend(e.messages)
if errors:
raise ValidationError(errors)
return self.compress(clean_data)
out = self.compress(clean_data)
self.validate(out)
return out
def compress(self, data_list):
"""
@ -864,30 +834,24 @@ class SplitDateTimeField(MultiValueField):
if data_list:
# Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False).
if data_list[0] in EMPTY_VALUES:
if data_list[0] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_date'])
if data_list[1] in EMPTY_VALUES:
if data_list[1] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_time'])
return datetime.datetime.combine(*data_list)
return None
ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
class IPAddressField(RegexField):
class IPAddressField(CharField):
default_error_messages = {
'invalid': _(u'Enter a valid IPv4 address.'),
}
default_validators = [validators.validate_ipv4_address]
def __init__(self, *args, **kwargs):
super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs)
slug_re = re.compile(r'^[-\w]+$')
class SlugField(RegexField):
class SlugField(CharField):
default_error_messages = {
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
u" underscores or hyphens."),
}
def __init__(self, *args, **kwargs):
super(SlugField, self).__init__(slug_re, *args, **kwargs)
default_validators = [validators.validate_slug]

View File

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

View File

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

View File

@ -9,10 +9,12 @@ from django.utils.datastructures import SortedDict
from django.utils.text import get_text_list, capfirst
from django.utils.translation import ugettext_lazy as _, ugettext
from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError
from django.core.validators import EMPTY_VALUES
from util import ErrorList
from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField
from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput
from widgets import media_property
from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
@ -27,20 +29,15 @@ __all__ = (
'ModelMultipleChoiceField',
)
def save_instance(form, instance, fields=None, fail_message='saved',
commit=True, exclude=None):
def construct_instance(form, instance, fields=None, exclude=None):
"""
Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
If commit=True, then the changes to ``instance`` will be saved to the
database. Returns ``instance``.
Constructs and returns a model instance from the bound ``form``'s
``cleaned_data``, but does not save the returned instance to the
database.
"""
from django.db import models
opts = instance._meta
if form.errors:
raise ValueError("The %s could not be %s because the data didn't"
" validate." % (opts.object_name, fail_message))
cleaned_data = form.cleaned_data
file_field_list = []
for f in opts.fields:
@ -65,9 +62,28 @@ def save_instance(form, instance, fields=None, fail_message='saved',
for f in file_field_list:
f.save_form_data(instance, cleaned_data[f.name])
return instance
def save_instance(form, instance, fields=None, fail_message='saved',
commit=True, exclude=None, construct=True):
"""
Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
If commit=True, then the changes to ``instance`` will be saved to the
database. Returns ``instance``.
If construct=False, assume ``instance`` has already been constructed and
just needs to be saved.
"""
if construct:
instance = construct_instance(form, instance, fields, exclude)
opts = instance._meta
if form.errors:
raise ValueError("The %s could not be %s because the data didn't"
" validate." % (opts.object_name, fail_message))
# Wrap up the saving of m2m data as a function.
def save_m2m():
opts = instance._meta
cleaned_data = form.cleaned_data
for f in opts.many_to_many:
if fields and f.name not in fields:
@ -120,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None):
the ``fields`` argument.
"""
# avoid a circular import
from django.db.models.fields.related import ManyToManyField, OneToOneField
from django.db.models.fields.related import ManyToManyField
opts = instance._meta
data = {}
for f in opts.fields + opts.many_to_many:
@ -218,8 +234,10 @@ class BaseModelForm(BaseForm):
# if we didn't get an instance, instantiate a new one
self.instance = opts.model()
object_data = {}
self.instance._adding = True
else:
self.instance = instance
self.instance._adding = False
object_data = model_to_dict(instance, opts.fields, opts.exclude)
# if initial was provided, it should override the values from instance
if initial is not None:
@ -228,165 +246,31 @@ class BaseModelForm(BaseForm):
error_class, label_suffix, empty_permitted)
def clean(self):
self.validate_unique()
return self.cleaned_data
def validate_unique(self):
unique_checks, date_checks = self._get_unique_checks()
form_errors = []
bad_fields = set()
field_errors, global_errors = self._perform_unique_checks(unique_checks)
bad_fields.union(field_errors)
form_errors.extend(global_errors)
field_errors, global_errors = self._perform_date_checks(date_checks)
bad_fields.union(field_errors)
form_errors.extend(global_errors)
for field_name in bad_fields:
del self.cleaned_data[field_name]
if form_errors:
# Raise the unique together errors since they are considered
# form-wide.
raise ValidationError(form_errors)
def _get_unique_checks(self):
from django.db.models.fields import FieldDoesNotExist, Field as ModelField
# Gather a list of checks to perform. We only perform unique checks
# for fields present and not None in cleaned_data. Since this is a
# ModelForm, some fields may have been excluded; we can't perform a unique
# check on a form that is missing fields involved in that check. It also does
# not make sense to check data that didn't validate, and since NULL does not
# equal NULL in SQL we should not do any unique checking for NULL values.
unique_checks = []
# these are checks for the unique_for_<date/year/month>
date_checks = []
for check in self.instance._meta.unique_together[:]:
fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None]
if len(fields_on_form) == len(check):
unique_checks.append(check)
# Gather a list of checks for fields declared as unique and add them to
# the list of checks. Again, skip empty fields and any that did not validate.
for name in self.fields:
opts = self._meta
self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
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
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)
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)
}
return self.cleaned_data
def save(self, commit=True):
"""
@ -401,7 +285,7 @@ class BaseModelForm(BaseForm):
else:
fail_message = 'changed'
return save_instance(self, self.instance, self._meta.fields,
fail_message, commit, exclude=self._meta.exclude)
fail_message, commit, construct=False)
save.alters_data = True
@ -530,7 +414,7 @@ class BaseModelFormSet(BaseFormSet):
break
else:
return
unique_checks, date_checks = form._get_unique_checks()
unique_checks, date_checks = form.instance._get_unique_checks()
errors = []
# Do each of the unique checks (unique and unique_together)
for unique_check in unique_checks:
@ -743,6 +627,9 @@ class BaseInlineFormSet(BaseModelFormSet):
# Remove the foreign key from the form's data
form.data[form.add_prefix(self.fk.name)] = None
# Set the fk value here so that the form can do it's validation.
setattr(form.instance, self.fk.get_attname(), self.instance.pk)
return form
#@classmethod

View File

@ -1,7 +1,11 @@
from django.utils.html import conditional_escape
from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode
from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe
# Import ValidationError so that it can be imported from this
# module to maintain backwards compatibility.
from django.core.exceptions import ValidationError
def flatatt(attrs):
"""
Convert a dictionary of attributes to a single string.
@ -48,21 +52,3 @@ class ErrorList(list, StrAndUnicode):
def __repr__(self):
return repr([force_unicode(e) for e in self])
class ValidationError(Exception):
def __init__(self, message):
"""
ValidationError can be passed any object that can be printed (usually
a string) or a list of objects.
"""
if isinstance(message, list):
self.messages = ErrorList([smart_unicode(msg) for msg in message])
else:
message = smart_unicode(message)
self.messages = ErrorList([message])
def __str__(self):
# This is needed because, without a __str__(), printing an exception
# instance would result in this:
# AttributeError: ValidationError instance has no attribute 'args'
# See http://www.python.org/doc/current/tut/node10.html#handling
return repr(self.messages)

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

View File

@ -3,6 +3,8 @@
Form and field validation
=========================
.. versionchanged:: 1.2
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
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
``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
for cleaning the data in a way that is generic for that type of field.
For example, a FloatField will turn the data into a Python ``float`` or
raise a ``ValidationError``. This method returns the clean data, which
is then inserted into the ``cleaned_data`` dictionary of the form.
Validation of a Form is split into several steps, which can be customized or
overridden:
* The ``to_python()`` method on a Field is the first step in every
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
``<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
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
keep it simple and assume e-mail validation is contained in a function called
``is_valid_email()``. The full class looks like this::
containing comma-separated e-mail addresses. The full class looks like this::
from django import forms
from django.core.validators import validate_email
class MultiEmailField(forms.Field):
def clean(self, value):
"""
Check that the field contains one or more comma-separated emails
and normalizes the data to a list of the email strings.
"""
def to_python(self, value):
"Normalize data to a list of strings."
# Return an empty list if no input was given.
if not value:
raise forms.ValidationError('Enter at least one e-mail address.')
emails = value.split(',')
for email in emails:
if not is_valid_email(email):
raise forms.ValidationError('%s is not a valid e-mail address.' % email)
return []
return value.split(',')
# Always return the cleaned data.
return emails
def validate(self, value):
"Check if value consists only of valid emails."
Every form that uses this field will have this ``clean()`` method 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.
# Use the parent's handling of required fields, etc.
super(MultiEmailField, self).validate(value)
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
field::
@ -183,7 +243,8 @@ field::
Simply use ``MultiEmailField`` like any other form field. When the
``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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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
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``
-------------
@ -284,6 +295,17 @@ underscores to spaces. See :ref:`Verbose field names <verbose-field-names>`.
.. _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
===========

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
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
==============

View File

@ -68,7 +68,7 @@ class ImprovedArticleWithParentLink(models.Model):
article = models.OneToOneField(Article, parent_link=True)
class BetterWriter(Writer):
pass
score = models.IntegerField()
class WriterProfile(models.Model):
writer = models.OneToOneField(Writer, primary_key=True)
@ -555,6 +555,8 @@ inserted as 'initial' data in each Field.
<option value="3">Third test</option>
</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.errors
{}
>>> f.is_valid()
True
>>> 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()
[]
>>> bw = BetterWriter(name=u'Joe Better')
>>> bw = BetterWriter(name=u'Joe Better', score=10)
>>> bw.save()
>>> 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 Meta:
@ -1102,16 +1114,6 @@ True
>>> 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.is_valid()
True
@ -1358,27 +1360,35 @@ __test__['API_TESTS'] += """
... class Meta:
... model = CommaSeparatedInteger
>>> f = CommaSeparatedIntegerForm().fields['field']
>>> f.clean('1,2,3')
u'1,2,3'
>>> f.clean('1a,2')
Traceback (most recent call last):
...
ValidationError: [u'Enter only digits separated by commas.']
>>> f.clean(',,,,')
u',,,,'
>>> f.clean('1.2')
Traceback (most recent call last):
...
ValidationError: [u'Enter only digits separated by commas.']
>>> f.clean('1,a,2')
Traceback (most recent call last):
...
ValidationError: [u'Enter only digits separated by commas.']
>>> f.clean('1,,2')
u'1,,2'
>>> f.clean('1')
u'1'
>>> f = CommaSeparatedIntegerForm({'field': '1,2,3'})
>>> f.is_valid()
True
>>> f.cleaned_data
{'field': u'1,2,3'}
>>> f = CommaSeparatedIntegerForm({'field': '1a,2'})
>>> f.errors
{'field': [u'Enter only digits separated by commas.']}
>>> f = CommaSeparatedIntegerForm({'field': ',,,,'})
>>> f.is_valid()
True
>>> f.cleaned_data
{'field': u',,,,'}
>>> f = CommaSeparatedIntegerForm({'field': '1.2'})
>>> f.errors
{'field': [u'Enter only digits separated by commas.']}
>>> f = CommaSeparatedIntegerForm({'field': '1,a,2'})
>>> f.errors
{'field': [u'Enter only digits separated by commas.']}
>>> f = CommaSeparatedIntegerForm({'field': '1,,2'})
>>> 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
@ -1415,13 +1425,16 @@ False
>>> form._errors
{'__all__': [u'Price with this Price and Quantity already exists.']}
# This form is never valid because quantity is blank=False.
>>> class PriceForm(ModelForm):
... class Meta:
... model = Price
... exclude = ('quantity',)
>>> form = PriceForm({'price': '6.00'})
>>> form.is_valid()
True
Traceback (most recent call last):
...
UnresolvableValidationError: {'quantity': [u'This field cannot be null.']}
# Unique & unique together with null values
>>> class BookForm(ModelForm):

View File

@ -543,10 +543,6 @@ This is used in the admin for save_as functionality.
... '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')
>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True)
>>> [book for book in formset.save() if book.author.pk == new_author.pk]
@ -1035,19 +1031,6 @@ False
>>> formset._non_form_errors
[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)
>>> author = Author.objects.order_by('id')[0]
>>> 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 ###################################################################
>>> e = {'required': 'REQUIRED'}
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
>>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)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.clean('')
Traceback (most recent call last):
@ -26,8 +26,8 @@ ValidationError: [u'LENGTH 11, MAX LENGTH 10']
>>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s'
>>> e['max_value'] = 'MAX VALUE IS %s'
>>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> f = IntegerField(min_value=5, max_value=10, error_messages=e)
>>> f.clean('')
Traceback (most recent call last):
@ -50,8 +50,8 @@ ValidationError: [u'MAX VALUE IS 10']
>>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s'
>>> e['max_value'] = 'MAX VALUE IS %s'
>>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> f = FloatField(min_value=5, max_value=10, error_messages=e)
>>> f.clean('')
Traceback (most recent call last):
@ -74,8 +74,8 @@ ValidationError: [u'MAX VALUE IS 10']
>>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID'
>>> e['min_value'] = 'MIN VALUE IS %s'
>>> e['max_value'] = 'MAX VALUE IS %s'
>>> e['min_value'] = 'MIN VALUE IS %(limit_value)s'
>>> e['max_value'] = 'MAX VALUE IS %(limit_value)s'
>>> e['max_digits'] = 'MAX DIGITS IS %s'
>>> e['max_decimal_places'] = 'MAX DP IS %s'
>>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s'
@ -156,8 +156,8 @@ ValidationError: [u'INVALID']
>>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID'
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
>>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)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.clean('')
Traceback (most recent call last):
@ -180,8 +180,8 @@ ValidationError: [u'LENGTH 11, MAX LENGTH 10']
>>> e = {'required': 'REQUIRED'}
>>> e['invalid'] = 'INVALID'
>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
>>> e['min_length'] = 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)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.clean('')
Traceback (most recent call last):

View File

@ -386,7 +386,7 @@ class FieldsTests(TestCase):
def test_regexfield_31(self):
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, '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'1234567890', f.clean('1234567890'))
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/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 ################################################################
def test_booleanfield_44(self):

View File

@ -28,7 +28,7 @@ u'C1064AAB'
>>> f.clean('C1064AABB')
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')
Traceback (most recent call last):
...
@ -44,7 +44,7 @@ ValidationError: [u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('500')
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')
Traceback (most recent call last):
...
@ -78,7 +78,7 @@ u'C1064AAB'
>>> f.clean('C1064AABB')
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')
Traceback (most recent call last):
...
@ -94,7 +94,7 @@ ValidationError: [u'Enter a postal code in the format NNNN or ANNNNAAA.']
>>> f.clean('500')
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')
Traceback (most recent call last):
...

View File

@ -15,11 +15,11 @@ u'230880-3449'
>>> f.clean('230880343')
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')
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')
Traceback (most recent call last):
...
@ -61,18 +61,18 @@ ValidationError: [u'Enter a valid value.']
>>> f.clean('123456')
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')
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')
Traceback (most recent call last):
ValidationError: [u'Enter a valid value.']
>>> f.clean(' 1234567 ')
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 ')
Traceback (most recent call last):
...

View File

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

View File

@ -5,6 +5,7 @@ Tests for forms/util.py module.
tests = r"""
>>> from django.forms.util import *
>>> from django.core.exceptions import ValidationError
>>> from django.utils.translation import ugettext_lazy
###########
@ -24,36 +25,36 @@ u''
###################
# 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>
# 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>
# 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>
# 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>
# 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>
>>> class VeryBadError:
... def __unicode__(self): return u"A very bad error."
# Can take a non-string.
>>> print ValidationError(VeryBadError()).messages
>>> print ErrorList(ValidationError(VeryBadError()).messages)
<ul class="errorlist"><li>A very bad error.</li></ul>
# Escapes non-safe input but not input marked safe.
>>> 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>
>>> 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>
"""

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
"""
# 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')
mother = Parent.objects.create(name=u'mother')
father = Parent.objects.create(name=u'father')
@ -89,13 +89,13 @@ class DeletionTests(TestCase):
'child_set-TOTAL_FORMS': u'1',
'child_set-INITIAL_FORMS': u'0',
'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)
self.assertEqual(formset.is_valid(), True)
objects = formset.save(commit=False)
for obj in objects:
obj.mother = mother
obj.father = father
obj.save()
self.assertEqual(school.child_set.count(), 0)
objects[0].save()
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)
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):
def test_limits(self):
# Ensure that values that are right at the limits can be saved

View File

@ -24,7 +24,7 @@ def custom_create(request):
model = Article
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 create_object(request,