diff options
| author | Joseph Kocherhans <joseph@jkocherhans.com> | 2010-01-05 03:56:19 +0000 |
|---|---|---|
| committer | Joseph Kocherhans <joseph@jkocherhans.com> | 2010-01-05 03:56:19 +0000 |
| commit | 471596fc1afcb9c6258d317c619eaf5fd394e797 (patch) | |
| tree | 193767161be3cc23dc2e6be5e4f16d8fd21a2925 /django/forms | |
| parent | 4e89105d64bb9e04c409139a41e9c7aac263df4c (diff) | |
Merged soc2009/model-validation to trunk. Thanks, Honza!
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12098 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/forms')
| -rw-r--r-- | django/forms/__init__.py | 2 | ||||
| -rw-r--r-- | django/forms/fields.py | 370 | ||||
| -rw-r--r-- | django/forms/forms.py | 3 | ||||
| -rw-r--r-- | django/forms/formsets.py | 3 | ||||
| -rw-r--r-- | django/forms/models.py | 231 | ||||
| -rw-r--r-- | django/forms/util.py | 24 |
6 files changed, 236 insertions, 397 deletions
diff --git a/django/forms/__init__.py b/django/forms/__init__.py index 0d9c68f9e0..dc8b5212c4 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -10,7 +10,7 @@ TODO: "This form field requires foo.js" and form.js_includes() """ -from util import ValidationError +from django.core.exceptions import ValidationError from widgets import * from fields import * from forms import * diff --git a/django/forms/fields.py b/django/forms/fields.py index 1194196fd1..2e6eb821f3 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -14,15 +14,21 @@ try: except ImportError: from StringIO import StringIO -import django.core.exceptions +from django.core.exceptions import ValidationError +from django.core import validators import django.utils.copycompat as copy from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_unicode, smart_str from django.utils.formats import get_format from django.utils.functional import lazy -from util import ErrorList, ValidationError -from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget +# Provide this import for backwards compatibility. +from django.core.validators import EMPTY_VALUES + +from util import ErrorList +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \ + FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \ + DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget __all__ = ( 'Field', 'CharField', 'IntegerField', @@ -36,9 +42,6 @@ __all__ = ( 'TypedChoiceField' ) -# These values, if given to to_python(), will trigger the self.required check. -EMPTY_VALUES = (None, '') - def en_format(name): """ Helper function to stay backward compatible. @@ -57,6 +60,7 @@ DEFAULT_DATETIME_INPUT_FORMATS = lazy(lambda: en_format('DATETIME_INPUT_FORMATS' class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". + default_validators = [] # Default set of validators default_error_messages = { 'required': _(u'This field is required.'), 'invalid': _(u'Enter a valid value.'), @@ -66,7 +70,8 @@ class Field(object): creation_counter = 0 def __init__(self, required=True, widget=None, label=None, initial=None, - help_text=None, error_messages=None, show_hidden_initial=False): + help_text=None, error_messages=None, show_hidden_initial=False, + validators=[]): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should @@ -82,6 +87,7 @@ class Field(object): # help_text -- An optional string to use as "help text" for this Field. # show_hidden_initial -- Boolean that specifies if it is needed to render a # hidden widget with initial value after widget. + # validators -- List of addtional validators to use if label is not None: label = smart_unicode(label) self.required, self.label, self.initial = required, label, initial @@ -105,16 +111,39 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 - def set_class_error_messages(messages, klass): - for base_class in klass.__bases__: - set_class_error_messages(messages, base_class) - messages.update(getattr(klass, 'default_error_messages', {})) - messages = {} - set_class_error_messages(messages, self.__class__) + for c in reversed(self.__class__.__mro__): + messages.update(getattr(c, 'default_error_messages', {})) messages.update(error_messages or {}) self.error_messages = messages + self.validators = self.default_validators + validators + + def to_python(self, value): + return value + + def validate(self, value): + if value in validators.EMPTY_VALUES and self.required: + raise ValidationError(self.error_messages['required']) + + def run_validators(self, value): + if value in validators.EMPTY_VALUES: + return + errors = [] + for v in self.validators: + try: + v(value) + except ValidationError, e: + if hasattr(e, 'code') and e.code in self.error_messages: + message = self.error_messages[e.code] + if e.params: + message = message % e.params + errors.append(message) + else: + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + def clean(self, value): """ Validates the given value and returns its "cleaned" value as an @@ -122,8 +151,9 @@ class Field(object): Raises ValidationError for any errors. """ - if self.required and value in EMPTY_VALUES: - raise ValidationError(self.error_messages['required']) + value = self.to_python(value) + self.validate(value) + self.run_validators(value) return value def widget_attrs(self, widget): @@ -141,27 +171,19 @@ class Field(object): return result class CharField(Field): - default_error_messages = { - 'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'), - 'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'), - } - def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length super(CharField, self).__init__(*args, **kwargs) + if min_length is not None: + self.validators.append(validators.MinLengthValidator(min_length)) + if max_length is not None: + self.validators.append(validators.MaxLengthValidator(max_length)) - def clean(self, value): - "Validates max_length and min_length. Returns a Unicode object." - super(CharField, self).clean(value) - if value in EMPTY_VALUES: + def to_python(self, value): + "Returns a Unicode object." + if value in validators.EMPTY_VALUES: return u'' - value = smart_unicode(value) - value_length = len(value) - if self.max_length is not None and value_length > self.max_length: - raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length}) - if self.min_length is not None and value_length < self.min_length: - raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length}) - return value + return smart_unicode(value) def widget_attrs(self, widget): if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): @@ -171,87 +193,82 @@ class CharField(Field): class IntegerField(Field): default_error_messages = { 'invalid': _(u'Enter a whole number.'), - 'max_value': _(u'Ensure this value is less than or equal to %s.'), - 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + 'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'), } def __init__(self, max_value=None, min_value=None, *args, **kwargs): - self.max_value, self.min_value = max_value, min_value super(IntegerField, self).__init__(*args, **kwargs) - def clean(self, value): + if max_value is not None: + self.validators.append(validators.MaxValueValidator(max_value)) + if min_value is not None: + self.validators.append(validators.MinValueValidator(min_value)) + + def to_python(self, value): """ Validates that int() can be called on the input. Returns the result of int(). Returns None for empty values. """ - super(IntegerField, self).clean(value) - if value in EMPTY_VALUES: + value = super(IntegerField, self).to_python(value) + if value in validators.EMPTY_VALUES: return None + try: value = int(str(value)) except (ValueError, TypeError): raise ValidationError(self.error_messages['invalid']) - if self.max_value is not None and value > self.max_value: - raise ValidationError(self.error_messages['max_value'] % self.max_value) - if self.min_value is not None and value < self.min_value: - raise ValidationError(self.error_messages['min_value'] % self.min_value) return value -class FloatField(Field): +class FloatField(IntegerField): default_error_messages = { 'invalid': _(u'Enter a number.'), - 'max_value': _(u'Ensure this value is less than or equal to %s.'), - 'min_value': _(u'Ensure this value is greater than or equal to %s.'), } - def __init__(self, max_value=None, min_value=None, *args, **kwargs): - self.max_value, self.min_value = max_value, min_value - Field.__init__(self, *args, **kwargs) - - def clean(self, value): + def to_python(self, value): """ - Validates that float() can be called on the input. Returns a float. - Returns None for empty values. + Validates that float() can be called on the input. Returns the result + of float(). Returns None for empty values. """ - super(FloatField, self).clean(value) - if not self.required and value in EMPTY_VALUES: + value = super(IntegerField, self).to_python(value) + if value in validators.EMPTY_VALUES: return None + try: # We always accept dot as decimal separator if isinstance(value, str) or isinstance(value, unicode): value = float(value.replace(get_format('DECIMAL_SEPARATOR'), '.')) except (ValueError, TypeError): raise ValidationError(self.error_messages['invalid']) - if self.max_value is not None and value > self.max_value: - raise ValidationError(self.error_messages['max_value'] % self.max_value) - if self.min_value is not None and value < self.min_value: - raise ValidationError(self.error_messages['min_value'] % self.min_value) return value class DecimalField(Field): default_error_messages = { 'invalid': _(u'Enter a number.'), - 'max_value': _(u'Ensure this value is less than or equal to %s.'), - 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + 'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'), 'max_digits': _('Ensure that there are no more than %s digits in total.'), 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'), 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.') } def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): - self.max_value, self.min_value = max_value, min_value self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, *args, **kwargs) - def clean(self, value): + if max_value is not None: + self.validators.append(validators.MaxValueValidator(max_value)) + if min_value is not None: + self.validators.append(validators.MinValueValidator(min_value)) + + def to_python(self, value): """ Validates that the input is a decimal number. Returns a Decimal instance. Returns None for empty values. Ensures that there are no more than max_digits in the number, and no more than decimal_places digits after the decimal point. """ - super(DecimalField, self).clean(value) - if not self.required and value in EMPTY_VALUES: + if value in validators.EMPTY_VALUES: return None value = smart_str(value).strip() try: @@ -260,7 +277,12 @@ class DecimalField(Field): value = Decimal(value.replace(get_format('DECIMAL_SEPARATOR'), '.')) except DecimalException: raise ValidationError(self.error_messages['invalid']) + return value + def validate(self, value): + super(DecimalField, self).validate(value) + if value in validators.EMPTY_VALUES: + return sign, digittuple, exponent = value.as_tuple() decimals = abs(exponent) # digittuple doesn't include any leading zeros. @@ -273,10 +295,6 @@ class DecimalField(Field): digits = decimals whole_digits = digits - decimals - if self.max_value is not None and value > self.max_value: - raise ValidationError(self.error_messages['max_value'] % self.max_value) - if self.min_value is not None and value < self.min_value: - raise ValidationError(self.error_messages['min_value'] % self.min_value) if self.max_digits is not None and digits > self.max_digits: raise ValidationError(self.error_messages['max_digits'] % self.max_digits) if self.decimal_places is not None and decimals > self.decimal_places: @@ -295,13 +313,12 @@ class DateField(Field): super(DateField, self).__init__(*args, **kwargs) self.input_formats = input_formats - def clean(self, value): + def to_python(self, value): """ Validates that the input can be converted to a date. Returns a Python datetime.date object. """ - super(DateField, self).clean(value) - if value in EMPTY_VALUES: + if value in validators.EMPTY_VALUES: return None if isinstance(value, datetime.datetime): return value.date() @@ -324,13 +341,12 @@ class TimeField(Field): super(TimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats - def clean(self, value): + def to_python(self, value): """ Validates that the input can be converted to a time. Returns a Python datetime.time object. """ - super(TimeField, self).clean(value) - if value in EMPTY_VALUES: + if value in validators.EMPTY_VALUES: return None if isinstance(value, datetime.time): return value @@ -351,13 +367,12 @@ class DateTimeField(Field): super(DateTimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats - def clean(self, value): + def to_python(self, value): """ Validates that the input can be converted to a datetime. Returns a Python datetime.datetime object. """ - super(DateTimeField, self).clean(value) - if value in EMPTY_VALUES: + if value in validators.EMPTY_VALUES: return None if isinstance(value, datetime.datetime): return value @@ -392,40 +407,13 @@ class RegexField(CharField): if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex + self.validators.append(validators.RegexValidator(regex=regex)) - def clean(self, value): - """ - Validates that the input matches the regular expression. Returns a - Unicode object. - """ - value = super(RegexField, self).clean(value) - if value == u'': - return value - if not self.regex.search(value): - raise ValidationError(self.error_messages['invalid']) - return value - -email_re = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE) # domain - -class EmailField(RegexField): +class EmailField(CharField): default_error_messages = { 'invalid': _(u'Enter a valid e-mail address.'), } - - def __init__(self, max_length=None, min_length=None, *args, **kwargs): - RegexField.__init__(self, email_re, max_length, min_length, *args, - **kwargs) - -try: - from django.conf import settings - URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT -except ImportError: - # It's OK if Django settings aren't configured. - URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' - + default_validators = [validators.validate_email] class FileField(Field): widget = FileInput @@ -440,12 +428,9 @@ class FileField(Field): self.max_length = kwargs.pop('max_length', None) super(FileField, self).__init__(*args, **kwargs) - def clean(self, data, initial=None): - super(FileField, self).clean(initial or data) - if not self.required and data in EMPTY_VALUES: + def to_python(self, data): + if data in validators.EMPTY_VALUES: return None - elif not data and initial: - return initial # UploadedFile objects should have name and size attributes. try: @@ -464,21 +449,24 @@ class FileField(Field): return data + def clean(self, data, initial=None): + if not data and initial: + return initial + return super(FileField, self).clean(data) + class ImageField(FileField): default_error_messages = { 'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), } - def clean(self, data, initial=None): + def to_python(self, data): """ Checks that the file-upload field data contains a valid image (GIF, JPG, PNG, possibly others -- whatever the Python Imaging Library supports). """ - f = super(ImageField, self).clean(data, initial) + f = super(ImageField, self).to_python(data) if f is None: return None - elif not data and initial: - return initial from PIL import Image # We need to get a file object for PIL. We might have a path or we might @@ -517,59 +505,34 @@ class ImageField(FileField): f.seek(0) return f -url_re = re.compile( - r'^https?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port - r'(?:/?|/\S+)$', re.IGNORECASE) - -class URLField(RegexField): +class URLField(CharField): default_error_messages = { 'invalid': _(u'Enter a valid URL.'), 'invalid_link': _(u'This URL appears to be a broken link.'), } def __init__(self, max_length=None, min_length=None, verify_exists=False, - validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): - super(URLField, self).__init__(url_re, max_length, min_length, *args, + validator_user_agent=validators.URL_VALIDATOR_USER_AGENT, *args, **kwargs): + super(URLField, self).__init__(max_length, min_length, *args, **kwargs) - self.verify_exists = verify_exists - self.user_agent = validator_user_agent + self.validators.append(validators.URLValidator(verify_exists=verify_exists, validator_user_agent=validator_user_agent)) - def clean(self, value): - # If no URL scheme given, assume http:// - if value and '://' not in value: - value = u'http://%s' % value - # If no URL path given, assume / - if value and not urlparse.urlsplit(value)[2]: - value += '/' - value = super(URLField, self).clean(value) - if value == u'': - return value - if self.verify_exists: - import urllib2 - headers = { - "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", - "Accept-Language": "en-us,en;q=0.5", - "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", - "Connection": "close", - "User-Agent": self.user_agent, - } - try: - req = urllib2.Request(value, None, headers) - u = urllib2.urlopen(req) - except ValueError: - raise ValidationError(self.error_messages['invalid']) - except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(self.error_messages['invalid_link']) - return value + def to_python(self, value): + if value: + if '://' not in value: + # If no URL scheme given, assume http:// + value = u'http://%s' % value + url_fields = list(urlparse.urlsplit(value)) + if not url_fields[2]: + # the path portion may need to be added before query params + url_fields[2] = '/' + value = urlparse.urlunsplit(url_fields) + return super(URLField, self).to_python(value) class BooleanField(Field): widget = CheckboxInput - def clean(self, value): + def to_python(self, value): """Returns a Python boolean object.""" # Explicitly check for the string 'False', which is what a hidden field # will submit for False. Also check for '0', since this is what @@ -579,7 +542,7 @@ class BooleanField(Field): value = False else: value = bool(value) - super(BooleanField, self).clean(value) + value = super(BooleanField, self).to_python(value) if not value and self.required: raise ValidationError(self.error_messages['required']) return value @@ -591,7 +554,7 @@ class NullBooleanField(BooleanField): """ widget = NullBooleanSelect - def clean(self, value): + def to_python(self, value): """ Explicitly checks for the string 'True' and 'False', which is what a hidden field will submit for True and False, and for '1' and '0', which @@ -605,6 +568,9 @@ class NullBooleanField(BooleanField): else: return None + def validate(self, value): + pass + class ChoiceField(Field): widget = Select default_error_messages = { @@ -613,8 +579,8 @@ class ChoiceField(Field): def __init__(self, choices=(), required=True, widget=None, label=None, initial=None, help_text=None, *args, **kwargs): - super(ChoiceField, self).__init__(required, widget, label, initial, - help_text, *args, **kwargs) + super(ChoiceField, self).__init__(required=required, widget=widget, label=label, + initial=initial, help_text=help_text, *args, **kwargs) self.choices = choices def _get_choices(self): @@ -628,19 +594,19 @@ class ChoiceField(Field): choices = property(_get_choices, _set_choices) - def clean(self, value): + def to_python(self, value): + "Returns a Unicode object." + if value in validators.EMPTY_VALUES: + return u'' + return smart_unicode(value) + + def validate(self, value): """ Validates that the input is in self.choices. """ - value = super(ChoiceField, self).clean(value) - if value in EMPTY_VALUES: - value = u'' - value = smart_unicode(value) - if value == u'': - return value - if not self.valid_value(value): + super(ChoiceField, self).validate(value) + if value and not self.valid_value(value): raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) - return value def valid_value(self, value): "Check to see if the provided value is a valid choice" @@ -661,27 +627,24 @@ class TypedChoiceField(ChoiceField): self.empty_value = kwargs.pop('empty_value', '') super(TypedChoiceField, self).__init__(*args, **kwargs) - def clean(self, value): + def to_python(self, value): """ Validate that the value is in self.choices and can be coerced to the right type. """ - value = super(TypedChoiceField, self).clean(value) - if value == self.empty_value or value in EMPTY_VALUES: + value = super(TypedChoiceField, self).to_python(value) + super(TypedChoiceField, self).validate(value) + if value == self.empty_value or value in validators.EMPTY_VALUES: return self.empty_value - - # Hack alert: This field is purpose-made to use with Field.to_python as - # a coercion function so that ModelForms with choices work. However, - # Django's Field.to_python raises - # django.core.exceptions.ValidationError, which is a *different* - # exception than django.forms.util.ValidationError. So we need to catch - # both. try: value = self.coerce(value) - except (ValueError, TypeError, django.core.exceptions.ValidationError): + except (ValueError, TypeError, ValidationError): raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) return value + def validate(self, value): + pass + class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple @@ -690,22 +653,23 @@ class MultipleChoiceField(ChoiceField): 'invalid_list': _(u'Enter a list of values.'), } - def clean(self, value): + def to_python(self, value): + if not value: + return [] + elif not isinstance(value, (list, tuple)): + raise ValidationError(self.error_messages['invalid_list']) + return [smart_unicode(val) for val in value] + + def validate(self, value): """ Validates that the input is a list or tuple. """ if self.required and not value: raise ValidationError(self.error_messages['required']) - elif not self.required and not value: - return [] - if not isinstance(value, (list, tuple)): - raise ValidationError(self.error_messages['invalid_list']) - new_value = [smart_unicode(val) for val in value] # Validate that each value in the value list is in self.choices. - for val in new_value: + for val in value: if not self.valid_value(val): raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) - return new_value class ComboField(Field): """ @@ -760,6 +724,9 @@ class MultiValueField(Field): f.required = False self.fields = fields + def validate(self, value): + pass + def clean(self, value): """ Validates every value in the given list. A value is validated against @@ -772,7 +739,7 @@ class MultiValueField(Field): clean_data = [] errors = ErrorList() if not value or isinstance(value, (list, tuple)): - if not value or not [v for v in value if v not in EMPTY_VALUES]: + if not value or not [v for v in value if v not in validators.EMPTY_VALUES]: if self.required: raise ValidationError(self.error_messages['required']) else: @@ -784,7 +751,7 @@ class MultiValueField(Field): field_value = value[i] except IndexError: field_value = None - if self.required and field_value in EMPTY_VALUES: + if self.required and field_value in validators.EMPTY_VALUES: raise ValidationError(self.error_messages['required']) try: clean_data.append(field.clean(field_value)) @@ -795,7 +762,10 @@ class MultiValueField(Field): errors.extend(e.messages) if errors: raise ValidationError(errors) - return self.compress(clean_data) + + out = self.compress(clean_data) + self.validate(out) + return out def compress(self, data_list): """ @@ -864,30 +834,24 @@ class SplitDateTimeField(MultiValueField): if data_list: # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). - if data_list[0] in EMPTY_VALUES: + if data_list[0] in validators.EMPTY_VALUES: raise ValidationError(self.error_messages['invalid_date']) - if data_list[1] in EMPTY_VALUES: + if data_list[1] in validators.EMPTY_VALUES: raise ValidationError(self.error_messages['invalid_time']) return datetime.datetime.combine(*data_list) return None -ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') -class IPAddressField(RegexField): +class IPAddressField(CharField): default_error_messages = { 'invalid': _(u'Enter a valid IPv4 address.'), } + default_validators = [validators.validate_ipv4_address] - def __init__(self, *args, **kwargs): - super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs) - -slug_re = re.compile(r'^[-\w]+$') -class SlugField(RegexField): +class SlugField(CharField): default_error_messages = { 'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers," u" underscores or hyphens."), } - - def __init__(self, *args, **kwargs): - super(SlugField, self).__init__(slug_re, *args, **kwargs) + default_validators = [validators.validate_slug] diff --git a/django/forms/forms.py b/django/forms/forms.py index 7f6fa51287..d484300a0d 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -2,6 +2,7 @@ Form classes """ +from django.core.exceptions import ValidationError from django.utils.copycompat import deepcopy from django.utils.datastructures import SortedDict from django.utils.html import conditional_escape @@ -10,7 +11,7 @@ from django.utils.safestring import mark_safe from fields import Field, FileField from widgets import Media, media_property, TextInput, Textarea -from util import flatatt, ErrorDict, ErrorList, ValidationError +from util import flatatt, ErrorDict, ErrorList __all__ = ('BaseForm', 'Form') diff --git a/django/forms/formsets.py b/django/forms/formsets.py index bb8e3107e1..d5101c762e 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -1,10 +1,11 @@ from forms import Form +from django.core.exceptions import ValidationError from django.utils.encoding import StrAndUnicode from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from fields import IntegerField, BooleanField from widgets import Media, HiddenInput -from util import ErrorList, ErrorDict, ValidationError +from util import ErrorList __all__ = ('BaseFormSet', 'all_valid') diff --git a/django/forms/models.py b/django/forms/models.py index 1c5f446c2b..ff20c936cb 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -9,10 +9,12 @@ from django.utils.datastructures import SortedDict from django.utils.text import get_text_list, capfirst from django.utils.translation import ugettext_lazy as _, ugettext -from util import ValidationError, ErrorList -from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS -from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES -from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput +from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError +from django.core.validators import EMPTY_VALUES +from util import ErrorList +from forms import BaseForm, get_declared_fields +from fields import Field, ChoiceField +from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput from widgets import media_property from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME @@ -27,20 +29,15 @@ __all__ = ( 'ModelMultipleChoiceField', ) - -def save_instance(form, instance, fields=None, fail_message='saved', - commit=True, exclude=None): +def construct_instance(form, instance, fields=None, exclude=None): """ - Saves bound Form ``form``'s cleaned_data into model instance ``instance``. - - If commit=True, then the changes to ``instance`` will be saved to the - database. Returns ``instance``. + Constructs and returns a model instance from the bound ``form``'s + ``cleaned_data``, but does not save the returned instance to the + database. """ from django.db import models opts = instance._meta - if form.errors: - raise ValueError("The %s could not be %s because the data didn't" - " validate." % (opts.object_name, fail_message)) + cleaned_data = form.cleaned_data file_field_list = [] for f in opts.fields: @@ -65,9 +62,28 @@ def save_instance(form, instance, fields=None, fail_message='saved', for f in file_field_list: f.save_form_data(instance, cleaned_data[f.name]) + return instance + +def save_instance(form, instance, fields=None, fail_message='saved', + commit=True, exclude=None, construct=True): + """ + Saves bound Form ``form``'s cleaned_data into model instance ``instance``. + + If commit=True, then the changes to ``instance`` will be saved to the + database. Returns ``instance``. + + If construct=False, assume ``instance`` has already been constructed and + just needs to be saved. + """ + if construct: + instance = construct_instance(form, instance, fields, exclude) + opts = instance._meta + if form.errors: + raise ValueError("The %s could not be %s because the data didn't" + " validate." % (opts.object_name, fail_message)) + # Wrap up the saving of m2m data as a function. def save_m2m(): - opts = instance._meta cleaned_data = form.cleaned_data for f in opts.many_to_many: if fields and f.name not in fields: @@ -120,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None): the ``fields`` argument. """ # avoid a circular import - from django.db.models.fields.related import ManyToManyField, OneToOneField + from django.db.models.fields.related import ManyToManyField opts = instance._meta data = {} for f in opts.fields + opts.many_to_many: @@ -218,8 +234,10 @@ class BaseModelForm(BaseForm): # if we didn't get an instance, instantiate a new one self.instance = opts.model() object_data = {} + self.instance._adding = True else: self.instance = instance + self.instance._adding = False object_data = model_to_dict(instance, opts.fields, opts.exclude) # if initial was provided, it should override the values from instance if initial is not None: @@ -228,165 +246,31 @@ class BaseModelForm(BaseForm): error_class, label_suffix, empty_permitted) def clean(self): - self.validate_unique() - return self.cleaned_data - - def validate_unique(self): - unique_checks, date_checks = self._get_unique_checks() - form_errors = [] - bad_fields = set() - - field_errors, global_errors = self._perform_unique_checks(unique_checks) - bad_fields.union(field_errors) - form_errors.extend(global_errors) - - field_errors, global_errors = self._perform_date_checks(date_checks) - bad_fields.union(field_errors) - form_errors.extend(global_errors) - - for field_name in bad_fields: - del self.cleaned_data[field_name] - if form_errors: - # Raise the unique together errors since they are considered - # form-wide. - raise ValidationError(form_errors) - - def _get_unique_checks(self): - from django.db.models.fields import FieldDoesNotExist, Field as ModelField - - # Gather a list of checks to perform. We only perform unique checks - # for fields present and not None in cleaned_data. Since this is a - # ModelForm, some fields may have been excluded; we can't perform a unique - # check on a form that is missing fields involved in that check. It also does - # not make sense to check data that didn't validate, and since NULL does not - # equal NULL in SQL we should not do any unique checking for NULL values. - unique_checks = [] - # these are checks for the unique_for_<date/year/month> - date_checks = [] - for check in self.instance._meta.unique_together[:]: - fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None] - if len(fields_on_form) == len(check): - unique_checks.append(check) - - # Gather a list of checks for fields declared as unique and add them to - # the list of checks. Again, skip empty fields and any that did not validate. - for name in self.fields: - try: - f = self.instance._meta.get_field_by_name(name)[0] - except FieldDoesNotExist: - # This is an extra field that's not on the ModelForm, ignore it - continue - if not isinstance(f, ModelField): - # This is an extra field that happens to have a name that matches, - # for example, a related object accessor for this model. So - # get_field_by_name found it, but it is not a Field so do not proceed - # to use it as if it were. - continue - if self.cleaned_data.get(name) is None: - continue - if f.unique: - unique_checks.append((name,)) - if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None: - date_checks.append(('date', name, f.unique_for_date)) - if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None: - date_checks.append(('year', name, f.unique_for_year)) - if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None: - date_checks.append(('month', name, f.unique_for_month)) - return unique_checks, date_checks - - - def _perform_unique_checks(self, unique_checks): - bad_fields = set() - form_errors = [] - - for unique_check in unique_checks: - # Try to look up an existing object with the same values as this - # object's values for all the unique field. - - lookup_kwargs = {} - for field_name in unique_check: - lookup_value = self.cleaned_data[field_name] - # ModelChoiceField will return an object instance rather than - # a raw primary key value, so convert it to a pk value before - # using it in a lookup. - if isinstance(self.fields[field_name], ModelChoiceField): - lookup_value = lookup_value.pk - lookup_kwargs[str(field_name)] = lookup_value - - qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) - - # Exclude the current object from the query if we are editing an - # instance (as opposed to creating a new one) - if self.instance.pk is not None: - qs = qs.exclude(pk=self.instance.pk) - - if qs.exists(): - if len(unique_check) == 1: - self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)]) - else: - form_errors.append(self.unique_error_message(unique_check)) - - # Mark these fields as needing to be removed from cleaned data - # later. - for field_name in unique_check: - bad_fields.add(field_name) - return bad_fields, form_errors - - def _perform_date_checks(self, date_checks): - bad_fields = set() - for lookup_type, field, unique_for in date_checks: - lookup_kwargs = {} - # there's a ticket to add a date lookup, we can remove this special - # case if that makes it's way in - if lookup_type == 'date': - date = self.cleaned_data[unique_for] - lookup_kwargs['%s__day' % unique_for] = date.day - lookup_kwargs['%s__month' % unique_for] = date.month - lookup_kwargs['%s__year' % unique_for] = date.year - else: - lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type) - lookup_kwargs[field] = self.cleaned_data[field] + opts = self._meta + self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + try: + self.instance.full_validate(exclude=self._errors.keys()) + except ValidationError, e: + for k, v in e.message_dict.items(): + if k != NON_FIELD_ERRORS: + self._errors.setdefault(k, ErrorList()).extend(v) - qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) - # Exclude the current object from the query if we are editing an - # instance (as opposed to creating a new one) - if self.instance.pk is not None: - qs = qs.exclude(pk=self.instance.pk) + # Remove the data from the cleaned_data dict since it was invalid + if k in self.cleaned_data: + del self.cleaned_data[k] - if qs.exists(): - self._errors[field] = ErrorList([ - self.date_error_message(lookup_type, field, unique_for) - ]) - bad_fields.add(field) - return bad_fields, [] + if NON_FIELD_ERRORS in e.message_dict: + raise ValidationError(e.message_dict[NON_FIELD_ERRORS]) - def date_error_message(self, lookup_type, field, unique_for): - return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % { - 'field_name': unicode(self.fields[field].label), - 'date_field': unicode(self.fields[unique_for].label), - 'lookup': lookup_type, - } + # If model validation threw errors for fields that aren't on the + # form, the the errors cannot be corrected by the user. Displaying + # those errors would be pointless, so raise another type of + # exception that *won't* be caught and displayed by the form. + if set(e.message_dict.keys()) - set(self.fields.keys() + [NON_FIELD_ERRORS]): + raise UnresolvableValidationError(e.message_dict) - def unique_error_message(self, unique_check): - model_name = capfirst(self.instance._meta.verbose_name) - # A unique field - if len(unique_check) == 1: - field_name = unique_check[0] - field_label = self.fields[field_name].label - # Insert the error into the error dict, very sneaky - return _(u"%(model_name)s with this %(field_label)s already exists.") % { - 'model_name': unicode(model_name), - 'field_label': unicode(field_label) - } - # unique_together - else: - field_labels = [self.fields[field_name].label for field_name in unique_check] - field_labels = get_text_list(field_labels, _('and')) - return _(u"%(model_name)s with this %(field_label)s already exists.") % { - 'model_name': unicode(model_name), - 'field_label': unicode(field_labels) - } + return self.cleaned_data def save(self, commit=True): """ @@ -401,7 +285,7 @@ class BaseModelForm(BaseForm): else: fail_message = 'changed' return save_instance(self, self.instance, self._meta.fields, - fail_message, commit, exclude=self._meta.exclude) + fail_message, commit, construct=False) save.alters_data = True @@ -530,7 +414,7 @@ class BaseModelFormSet(BaseFormSet): break else: return - unique_checks, date_checks = form._get_unique_checks() + unique_checks, date_checks = form.instance._get_unique_checks() errors = [] # Do each of the unique checks (unique and unique_together) for unique_check in unique_checks: @@ -743,6 +627,9 @@ class BaseInlineFormSet(BaseModelFormSet): # Remove the foreign key from the form's data form.data[form.add_prefix(self.fk.name)] = None + + # Set the fk value here so that the form can do it's validation. + setattr(form.instance, self.fk.get_attname(), self.instance.pk) return form #@classmethod diff --git a/django/forms/util.py b/django/forms/util.py index b9b88a61e6..1a1d823495 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -1,7 +1,11 @@ from django.utils.html import conditional_escape -from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode +from django.utils.encoding import StrAndUnicode, force_unicode from django.utils.safestring import mark_safe +# Import ValidationError so that it can be imported from this +# module to maintain backwards compatibility. +from django.core.exceptions import ValidationError + def flatatt(attrs): """ Convert a dictionary of attributes to a single string. @@ -48,21 +52,3 @@ class ErrorList(list, StrAndUnicode): def __repr__(self): return repr([force_unicode(e) for e in self]) -class ValidationError(Exception): - def __init__(self, message): - """ - ValidationError can be passed any object that can be printed (usually - a string) or a list of objects. - """ - if isinstance(message, list): - self.messages = ErrorList([smart_unicode(msg) for msg in message]) - else: - message = smart_unicode(message) - self.messages = ErrorList([message]) - - def __str__(self): - # This is needed because, without a __str__(), printing an exception - # instance would result in this: - # AttributeError: ValidationError instance has no attribute 'args' - # See http://www.python.org/doc/current/tut/node10.html#handling - return repr(self.messages) |
