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:
parent
4e89105d64
commit
471596fc1a
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)."),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 _
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
@ -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) #
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
--------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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
|
||||
===========
|
||||
|
||||
|
|
|
@ -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
|
||||
==============
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
@ -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]
|
||||
)
|
|
@ -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'])
|
||||
|
|
@ -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))
|
|
@ -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',])
|
||||
|
|
@ -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!']
|
||||
)
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
...
|
||||
|
|
|
@ -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):
|
||||
...
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: <a href="http://www.example.com/">example</a></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>
|
||||
"""
|
||||
|
|
|
@ -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))
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue