diff options
Diffstat (limited to 'django/newforms/fields.py')
| -rw-r--r-- | django/newforms/fields.py | 275 |
1 files changed, 231 insertions, 44 deletions
diff --git a/django/newforms/fields.py b/django/newforms/fields.py index fada75fe5d..e9e1fb7746 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -3,8 +3,8 @@ Field classes """ from django.utils.translation import gettext -from util import ValidationError, smart_unicode -from widgets import TextInput, CheckboxInput, Select, SelectMultiple +from util import ErrorList, ValidationError, smart_unicode +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple import datetime import re import time @@ -12,10 +12,12 @@ import time __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', + 'SplitDateTimeField', ) # These values, if given to to_python(), will trigger the self.required check. @@ -28,15 +30,37 @@ except NameError: 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): - self.required = required + 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, self.initial = required, label, initial + self.help_text = help_text widget = widget or self.widget if isinstance(widget, type): widget = widget() + + # Hook into self.widget_attrs() for any Field-specific HTML attributes. + extra_attrs = self.widget_attrs(widget) + if extra_attrs: + widget.attrs.update(extra_attrs) + self.widget = widget # Increase the creation counter, and save our local copy. @@ -54,15 +78,24 @@ class Field(object): raise ValidationError(gettext(u'This field is required.')) return value + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None): - Field.__init__(self, required, widget) + 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) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." - Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + super(CharField, self).clean(value) + if value in EMPTY_VALUES: + 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) @@ -70,19 +103,32 @@ class CharField(Field): raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) return value + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + return {'maxlength': str(self.max_length)} + class IntegerField(Field): + 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): """ 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: - return int(value) + value = int(value) except (ValueError, TypeError): raise ValidationError(gettext(u'Enter a whole number.')) + if self.max_value is not None and value > self.max_value: + raise ValidationError(gettext(u'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(u'Ensure this value is greater than or equal to %s.') % self.min_value) + return value DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' @@ -93,8 +139,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + 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): @@ -102,7 +148,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): @@ -116,6 +162,33 @@ class DateField(Field): continue raise ValidationError(gettext(u'Enter a valid date.')) +DEFAULT_TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' +) + +class TimeField(Field): + 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): + """ + 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: + return None + if isinstance(value, datetime.time): + return value + for format in self.input_formats: + try: + return datetime.time(*time.strptime(value, format)[3:6]) + except ValueError: + continue + raise ValidationError(gettext(u'Enter a valid time.')) + DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' '%Y-%m-%d %H:%M', # '2006-10-25 14:30' @@ -129,8 +202,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + 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): @@ -138,7 +211,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): @@ -153,16 +226,17 @@ class DateTimeField(Field): raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, error_message=None, required=True, widget=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) + super(RegexField, self).__init__(*args, **kwargs) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex + self.max_length, self.min_length = max_length, min_length self.error_message = error_message or gettext(u'Enter a valid value.') def clean(self, value): @@ -170,11 +244,16 @@ 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) + if self.min_length is not None and len(value) < self.min_length: + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) if not self.regex.search(value): raise ValidationError(self.error_message) return value @@ -185,8 +264,9 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, gettext(u'Enter a valid e-mail address.'), required, widget) + 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:// @@ -202,14 +282,16 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None, - validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, gettext(u'Enter a valid URL.'), required, widget) + 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 @@ -234,24 +316,43 @@ 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): - if isinstance(widget, type): - widget = widget(choices=choices) - Field.__init__(self, required, widget) + def __init__(self, choices=(), required=True, widget=Select, 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. + self._choices = value + self.widget.choices = 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: @@ -259,8 +360,10 @@ class ChoiceField(Field): return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple): - ChoiceField.__init__(self, choices, required, widget) + hidden_widget = MultipleHiddenInput + + def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None, initial=None, help_text=None): + super(MultipleChoiceField, self).__init__(choices, required, widget, label, initial, help_text) def clean(self, value): """ @@ -284,8 +387,11 @@ class MultipleChoiceField(ChoiceField): return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None): - Field.__init__(self, required, widget) + """ + 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. @@ -298,7 +404,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 KeyError: + 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 |
