diff options
| author | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
|---|---|---|
| committer | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
| commit | ae22b6d403dcf25098c77f0dfcf59ae58b186461 (patch) | |
| tree | c37fc631e99a7e4d909d6b6d236f495003731ea7 /django/newforms/fields.py | |
| parent | 0cf7bc439129c66df8d64601e885f83b256b4f25 (diff) | |
per-object-permissions: Merged to trunk [5486] NOTE: Not fully tested, will be working on this over the next few weeks.
git-svn-id: http://code.djangoproject.com/svn/django/branches/per-object-permissions@5488 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/newforms/fields.py')
| -rw-r--r-- | django/newforms/fields.py | 298 |
1 files changed, 245 insertions, 53 deletions
diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 5c6d46ddac..b73dd181e6 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -2,21 +2,25 @@ Field classes """ -from django.utils.translation import gettext -from util import ValidationError, smart_unicode -from widgets import TextInput, PasswordInput, CheckboxInput, Select, SelectMultiple import datetime import re import time +from django.utils.translation import gettext +from django.utils.encoding import smart_unicode + +from util import ErrorList, ValidationError +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple + __all__ = ( 'Field', 'CharField', 'IntegerField', 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'RegexField', 'EmailField', 'URLField', 'BooleanField', - 'ChoiceField', 'MultipleChoiceField', - 'ComboField', + 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', + 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', + 'SplitDateTimeField', ) # These values, if given to to_python(), will trigger the self.required check. @@ -27,16 +31,36 @@ try: except NameError: from sets import Set as set # Python 2.3 fallback +try: + from decimal import Decimal +except ImportError: + from django.utils._decimal import Decimal # Python 2.3 fallback + 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". # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None, label=None): + def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None): + # required -- Boolean that specifies whether the field is required. + # True by default. + # widget -- A Widget class, or instance of a Widget class, that should + # be used for this Field when displaying it. Each Field has a + # default Widget that it'll use if you don't specify this. In + # most cases, the default widget is TextInput. + # label -- A verbose name for this field, for use in displaying this + # field in a form. By default, Django will use a "pretty" + # version of the form field name, if the Field is part of a + # Form. + # initial -- A value to use in this Field's initial display. This value + # is *not* used as a fallback if data isn't given. + # help_text -- An optional string to use as "help text" for this Field. if label is not None: label = smart_unicode(label) - self.required, self.label = required, label + self.required, self.label, self.initial = required, label, initial + self.help_text = smart_unicode(help_text or '') widget = widget or self.widget if isinstance(widget, type): widget = widget() @@ -72,17 +96,15 @@ class Field(object): return {} class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): + def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length - Field.__init__(self, required, widget, label) + super(CharField, self).__init__(*args, **kwargs) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." - Field.clean(self, value) + super(CharField, self).clean(value) if value in EMPTY_VALUES: - value = u'' - if not self.required: - return value + return u'' value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) @@ -95,18 +117,18 @@ class CharField(Field): return {'maxlength': str(self.max_length)} class IntegerField(Field): - def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None): + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value - Field.__init__(self, required, widget, label) + super(IntegerField, self).__init__(*args, **kwargs) def clean(self, value): """ Validates that int() can be called on the input. Returns the result - of int(). + of int(). Returns None for empty values. """ super(IntegerField, self).clean(value) - if not self.required and value in EMPTY_VALUES: - return u'' + if value in EMPTY_VALUES: + return None try: value = int(value) except (ValueError, TypeError): @@ -117,6 +139,67 @@ class IntegerField(Field): raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) return value +class FloatField(Field): + 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): + """ + Validates that float() can be called on the input. Returns a float. + Returns None for empty values. + """ + super(FloatField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return None + try: + value = float(value) + except (ValueError, TypeError): + raise ValidationError(gettext('Enter a number.')) + if self.max_value is not None and value > self.max_value: + raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value) + return value + +decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$') + +class DecimalField(Field): + 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): + """ + 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: + return None + value = value.strip() + match = decimal_re.search(value) + if not match: + raise ValidationError(gettext('Enter a number.')) + else: + value = Decimal(value) + digits = len(match.group('digits') or '') + decimals = len(match.group('decimals') or '') + if self.max_value is not None and value > self.max_value: + raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value) + if self.max_digits is not None and (digits + decimals) > self.max_digits: + raise ValidationError(gettext('Ensure that there are no more than %s digits in total.') % self.max_digits) + if self.decimal_places is not None and decimals > self.decimal_places: + raise ValidationError(gettext('Ensure that there are no more than %s decimal places.') % self.decimal_places) + if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places): + raise ValidationError(gettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places)) + return value + DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' @@ -126,8 +209,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None): - Field.__init__(self, required, widget, label) + def __init__(self, input_formats=None, *args, **kwargs): + super(DateField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS def clean(self, value): @@ -135,7 +218,7 @@ class DateField(Field): Validates that the input can be converted to a date. Returns a Python datetime.date object. """ - Field.clean(self, value) + super(DateField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.datetime): @@ -155,8 +238,8 @@ DEFAULT_TIME_INPUT_FORMATS = ( ) class TimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None): - Field.__init__(self, required, widget, label) + def __init__(self, input_formats=None, *args, **kwargs): + super(TimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS def clean(self, value): @@ -164,7 +247,7 @@ class TimeField(Field): Validates that the input can be converted to a time. Returns a Python datetime.time object. """ - Field.clean(self, value) + super(TimeField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.time): @@ -189,8 +272,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None): - Field.__init__(self, required, widget, label) + def __init__(self, input_formats=None, *args, **kwargs): + super(DateTimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS def clean(self, value): @@ -198,7 +281,7 @@ class DateTimeField(Field): Validates that the input can be converted to a datetime. Returns a Python datetime.datetime object. """ - Field.clean(self, value) + super(DateTimeField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.datetime): @@ -213,14 +296,13 @@ class DateTimeField(Field): raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, max_length=None, min_length=None, error_message=None, - required=True, widget=None, label=None): + def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget, label) + super(RegexField, self).__init__(*args, **kwargs) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex @@ -232,10 +314,11 @@ class RegexField(Field): Validates that the input matches the regular expression. Returns a Unicode object. """ - Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + super(RegexField, self).clean(value) + if value in EMPTY_VALUES: + value = u'' value = smart_unicode(value) - if not self.required and value == u'': + if value == u'': return value if self.max_length is not None and len(value) > self.max_length: raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) @@ -251,8 +334,9 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): - RegexField.__init__(self, email_re, max_length, min_length, gettext(u'Enter a valid e-mail address.'), required, widget, label) + def __init__(self, max_length=None, min_length=None, *args, **kwargs): + RegexField.__init__(self, email_re, max_length, min_length, + gettext(u'Enter a valid e-mail address.'), *args, **kwargs) url_re = re.compile( r'^https?://' # http:// or https:// @@ -268,14 +352,16 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, max_length=None, min_length=None, required=True, verify_exists=False, widget=None, label=None, - validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, max_length, min_length, gettext(u'Enter a valid URL.'), required, widget, label) + 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, gettext(u'Enter a valid URL.'), *args, **kwargs) self.verify_exists = verify_exists self.user_agent = validator_user_agent def clean(self, value): - value = RegexField.clean(self, value) + value = super(URLField, self).clean(value) + if value == u'': + return value if self.verify_exists: import urllib2 from django.conf import settings @@ -300,33 +386,55 @@ class BooleanField(Field): def clean(self, value): "Returns a Python boolean object." - Field.clean(self, value) + super(BooleanField, self).clean(value) return bool(value) +class NullBooleanField(BooleanField): + """ + A field whose valid values are None, True and False. Invalid values are + cleaned to None. + """ + widget = NullBooleanSelect + + def clean(self, value): + return {True: True, False: False}.get(value, None) + class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select, label=None): - if isinstance(widget, type): - widget = widget(choices=choices) - Field.__init__(self, required, widget, label) + widget = Select + + def __init__(self, choices=(), required=True, widget=None, label=None, initial=None, help_text=None): + super(ChoiceField, self).__init__(required, widget, label, initial, help_text) self.choices = choices + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + def clean(self, value): """ Validates that the input is in self.choices. """ - value = Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + value = super(ChoiceField, self).clean(value) + if value in EMPTY_VALUES: + value = u'' value = smart_unicode(value) - if not self.required and value == u'': + if value == u'': return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value) + raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.')) return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None): - ChoiceField.__init__(self, choices, required, widget, label) + hidden_widget = MultipleHiddenInput + widget = SelectMultiple def clean(self, value): """ @@ -350,8 +458,11 @@ class MultipleChoiceField(ChoiceField): return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None, label=None): - Field.__init__(self, required, widget, label) + """ + A Field whose clean() method calls multiple Field clean() methods. + """ + def __init__(self, fields=(), *args, **kwargs): + super(ComboField, self).__init__(*args, **kwargs) # Set 'required' to False on the individual fields, because the # required validation will be handled by ComboField, not by those # individual fields. @@ -364,7 +475,88 @@ class ComboField(Field): Validates the given value against all of self.fields, which is a list of Field instances. """ - Field.clean(self, value) + super(ComboField, self).clean(value) for field in self.fields: value = field.clean(value) return value + +class MultiValueField(Field): + """ + A Field that is composed of multiple Fields. + + Its clean() method takes a "decompressed" list of values. Each value in + this list is cleaned by the corresponding field -- the first value is + cleaned by the first field, the second value is cleaned by the second + field, etc. Once all fields are cleaned, the list of clean values is + "compressed" into a single value. + + Subclasses should implement compress(), which specifies how a list of + valid values should be converted to a single value. Subclasses should not + have to implement clean(). + + You'll probably want to use this with MultiWidget. + """ + def __init__(self, fields=(), *args, **kwargs): + super(MultiValueField, self).__init__(*args, **kwargs) + # Set 'required' to False on the individual fields, because the + # required validation will be handled by MultiValueField, not by those + # individual fields. + for f in fields: + f.required = False + self.fields = fields + + def clean(self, value): + """ + Validates every value in the given list. A value is validated against + the corresponding Field in self.fields. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), clean() would call + DateField.clean(value[0]) and TimeField.clean(value[1]). + """ + clean_data = [] + errors = ErrorList() + if self.required and not value: + raise ValidationError(gettext(u'This field is required.')) + elif not self.required and not value: + return self.compress([]) + if not isinstance(value, (list, tuple)): + raise ValidationError(gettext(u'Enter a list of values.')) + for i, field in enumerate(self.fields): + try: + field_value = value[i] + except IndexError: + field_value = None + if self.required and field_value in EMPTY_VALUES: + raise ValidationError(gettext(u'This field is required.')) + try: + clean_data.append(field.clean(field_value)) + except ValidationError, e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + return self.compress(clean_data) + + def compress(self, data_list): + """ + Returns a single value for the given list of values. The values can be + assumed to be valid. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), this might return a datetime + object created by combining the date and time in data_list. + """ + raise NotImplementedError('Subclasses must implement this method.') + +class SplitDateTimeField(MultiValueField): + def __init__(self, *args, **kwargs): + fields = (DateField(), TimeField()) + super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + return datetime.datetime.combine(*data_list) + return None |
