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 | |
| 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')
| -rw-r--r-- | django/newforms/extras/widgets.py | 3 | ||||
| -rw-r--r-- | django/newforms/fields.py | 298 | ||||
| -rw-r--r-- | django/newforms/forms.py | 137 | ||||
| -rw-r--r-- | django/newforms/models.py | 196 | ||||
| -rw-r--r-- | django/newforms/util.py | 23 | ||||
| -rw-r--r-- | django/newforms/widgets.py | 187 |
6 files changed, 689 insertions, 155 deletions
diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py index 1011934fb8..724dcd9b50 100644 --- a/django/newforms/extras/widgets.py +++ b/django/newforms/extras/widgets.py @@ -2,9 +2,10 @@ Extra HTML Widget classes """ +import datetime + from django.newforms.widgets import Widget, Select from django.utils.dates import MONTHS -import datetime __all__ = ('SelectDateWidget',) 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 diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 201cce3868..6ebebded4b 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -2,11 +2,15 @@ Form classes """ -from django.utils.datastructures import SortedDict, MultiValueDict +import copy + +from django.utils.datastructures import SortedDict from django.utils.html import escape +from django.utils.encoding import StrAndUnicode + from fields import Field -from widgets import TextInput, Textarea, HiddenInput -from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError +from widgets import TextInput, Textarea +from util import flatatt, ErrorDict, ErrorList, ValidationError __all__ = ('BaseForm', 'Form') @@ -26,12 +30,26 @@ class SortedDictFromList(SortedDict): self.keyOrder = [d[0] for d in data] dict.__init__(self, dict(data)) + def copy(self): + return SortedDictFromList([(k, copy.copy(v)) for k, v in self.items()]) + class DeclarativeFieldsMetaclass(type): - "Metaclass that converts Field attributes to a dictionary called 'fields'." + """ + Metaclass that converts Field attributes to a dictionary called + 'base_fields', taking into account parent class 'base_fields' as well. + """ def __new__(cls, name, bases, attrs): - fields = [(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)] + fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) - attrs['fields'] = SortedDictFromList(fields) + + # If this class is subclassing another Form, add that Form's fields. + # Note that we loop over the bases in *reverse*. This is necessary in + # order to preserve the correct order of fields. + for base in bases[::-1]: + if hasattr(base, 'base_fields'): + fields = base.base_fields.items() + fields + + attrs['base_fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) class BaseForm(StrAndUnicode): @@ -39,13 +57,20 @@ class BaseForm(StrAndUnicode): # class is different than Form. See the comments by the Form class for more # information. Any improvements to the form API should be made to *this* # class, not to the Form class. - def __init__(self, data=None, auto_id='id_%s', prefix=None): - self.ignore_errors = data is None + def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): + self.is_bound = data is not None self.data = data or {} self.auto_id = auto_id self.prefix = prefix - self.clean_data = None # Stores the data after clean() has been called. - self.__errors = None # Stores the errors after clean() has been called. + self.initial = initial or {} + self._errors = None # Stores the errors after clean() has been called. + + # The base_fields class attribute is the *class-wide* definition of + # fields. Because a particular *instance* of the class might want to + # alter self.fields, we create self.fields here by copying base_fields. + # Instances should always modify self.fields; they should not modify + # self.base_fields. + self.fields = self.base_fields.copy() def __unicode__(self): return self.as_table() @@ -62,19 +87,19 @@ class BaseForm(StrAndUnicode): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def _errors(self): + def _get_errors(self): "Returns an ErrorDict for self.data" - if self.__errors is None: + if self._errors is None: self.full_clean() - return self.__errors - errors = property(_errors) + return self._errors + errors = property(_get_errors) def is_valid(self): """ Returns True if the form has no errors. Otherwise, False. If errors are being ignored, returns False. """ - return not self.ignore_errors and not bool(self.errors) + return self.is_bound and not bool(self.errors) def add_prefix(self, field_name): """ @@ -85,13 +110,13 @@ class BaseForm(StrAndUnicode): """ return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name - def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row): + def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." top_errors = self.non_field_errors() # Errors that should be displayed above all fields. output, hidden_fields = [], [] for name, field in self.fields.items(): bf = BoundField(self, field, name) - bf_errors = bf.errors # Cache in local variable. + bf_errors = ErrorList([escape(error) for error in bf.errors]) # Escape and cache in local variable. if bf.is_hidden: if bf_errors: top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) @@ -99,8 +124,19 @@ class BaseForm(StrAndUnicode): else: if errors_on_separate_row and bf_errors: output.append(error_row % bf_errors) - label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' - output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': bf}) + if bf.label: + label = escape(bf.label) + # Only add a colon if the label does not end in punctuation. + if label[-1] not in ':?.!': + label += ':' + label = bf.label_tag(label) or '' + else: + label = '' + if field.help_text: + help_text = help_text_html % field.help_text + else: + help_text = u'' + output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': unicode(bf), 'help_text': help_text}) if top_errors: output.insert(0, error_row % top_errors) if hidden_fields: # Insert any hidden fields in the last row. @@ -115,15 +151,15 @@ class BaseForm(StrAndUnicode): def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', False) + return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', False) def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False) + return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False) def as_p(self): "Returns this form rendered as HTML <p>s." - return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True) + return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'<p>%s</p>', '</p>', u' %s', True) def non_field_errors(self): """ @@ -135,13 +171,13 @@ class BaseForm(StrAndUnicode): def full_clean(self): """ - Cleans all of self.data and populates self.__errors and self.clean_data. + Cleans all of self.data and populates self._errors and + self.cleaned_data. """ - self.clean_data = {} - errors = ErrorDict() - if self.ignore_errors: # Stop further processing. - self.__errors = errors + self._errors = ErrorDict() + if not self.is_bound: # Stop further processing. return + self.cleaned_data = {} for name, field in self.fields.items(): # value_from_datadict() gets the data from the dictionary. # Each widget type knows how to retrieve its own data, because some @@ -149,19 +185,20 @@ class BaseForm(StrAndUnicode): value = field.widget.value_from_datadict(self.data, self.add_prefix(name)) try: value = field.clean(value) - self.clean_data[name] = value + self.cleaned_data[name] = value if hasattr(self, 'clean_%s' % name): value = getattr(self, 'clean_%s' % name)() - self.clean_data[name] = value + self.cleaned_data[name] = value except ValidationError, e: - errors[name] = e.messages + self._errors[name] = e.messages + if name in self.cleaned_data: + del self.cleaned_data[name] try: - self.clean_data = self.clean() + self.cleaned_data = self.clean() except ValidationError, e: - errors[NON_FIELD_ERRORS] = e.messages - if errors: - self.clean_data = None - self.__errors = errors + self._errors[NON_FIELD_ERRORS] = e.messages + if self._errors: + delattr(self, 'cleaned_data') def clean(self): """ @@ -170,7 +207,7 @@ class BaseForm(StrAndUnicode): not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ - return self.clean_data + return self.cleaned_data class Form(BaseForm): "A collection of Fields, plus their associated data." @@ -192,6 +229,7 @@ class BoundField(StrAndUnicode): self.label = pretty_name(name) else: self.label = self.field.label + self.help_text = field.help_text or '' def __unicode__(self): "Renders this field as an HTML widget." @@ -200,9 +238,9 @@ class BoundField(StrAndUnicode): value = self.as_widget(self.field.widget) if not isinstance(value, basestring): # Some Widget render() methods -- notably RadioSelect -- return a - # "special" object rather than a string. Call the __str__() on that + # "special" object rather than a string. Call __unicode__() on that # object to get its rendered value. - value = value.__str__() + value = unicode(value) return value def _errors(self): @@ -216,9 +254,15 @@ class BoundField(StrAndUnicode): def as_widget(self, widget, attrs=None): attrs = attrs or {} auto_id = self.auto_id - if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): + if auto_id and 'id' not in attrs and 'id' not in widget.attrs: attrs['id'] = auto_id - return widget.render(self.html_name, self.data, attrs=attrs) + if not self.form.is_bound: + data = self.form.initial.get(self.name, self.field.initial) + if callable(data): + data = data() + else: + data = self.data + return widget.render(self.html_name, data, attrs=attrs) def as_text(self, attrs=None): """ @@ -234,24 +278,29 @@ class BoundField(StrAndUnicode): """ Returns a string of HTML for representing this as an <input type="hidden">. """ - return self.as_widget(HiddenInput(), attrs) + return self.as_widget(self.field.hidden_widget(), attrs) def _data(self): - "Returns the data for this BoundField, or None if it wasn't given." + """ + Returns the data for this BoundField, or None if it wasn't given. + """ return self.field.widget.value_from_datadict(self.form.data, self.html_name) data = property(_data) - def label_tag(self, contents=None): + def label_tag(self, contents=None, attrs=None): """ Wraps the given contents in a <label>, if the field has an ID attribute. Does not HTML-escape the contents. If contents aren't given, uses the field's HTML-escaped label. + + If attrs are given, they're used as HTML attributes on the <label> tag. """ contents = contents or escape(self.label) widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id if id_: - contents = '<label for="%s">%s</label>' % (widget.id_for_label(id_), contents) + attrs = attrs and flatatt(attrs) or '' + contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents) return contents def _is_hidden(self): diff --git a/django/newforms/models.py b/django/newforms/models.py index 6b111d7ee1..d51b06c78c 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -3,36 +3,196 @@ Helper functions for creating Form classes from Django models and database field objects. """ -from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList +from django.utils.translation import gettext -__all__ = ('form_for_model', 'form_for_fields') +from util import ValidationError +from forms import BaseForm, SortedDictFromList +from fields import Field, ChoiceField +from widgets import Select, SelectMultiple, MultipleHiddenInput -def create(self, save=True): - "Creates and returns model instance according to self.clean_data." - if self.errors: - raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name) - obj = self._model(**self.clean_data) - if save: - obj.save() - return obj +__all__ = ( + 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', + 'ModelChoiceField', 'ModelMultipleChoiceField' +) -def form_for_model(model, form=None): +def save_instance(form, instance, fields=None, fail_message='saved', commit=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``. + """ + from django.db import models + opts = instance.__class__._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 + for f in opts.fields: + if not f.editable or isinstance(f, models.AutoField) or not f.name in cleaned_data: + continue + if fields and f.name not in fields: + continue + setattr(instance, f.name, cleaned_data[f.name]) + if commit: + instance.save() + for f in opts.many_to_many: + if fields and f.name not in fields: + continue + if f.name in cleaned_data: + setattr(instance, f.attname, cleaned_data[f.name]) + # GOTCHA: If many-to-many data is given and commit=False, the many-to-many + # data will be lost. This happens because a many-to-many options cannot be + # set on an object until after it's saved. Maybe we should raise an + # exception in that case. + return instance + +def make_model_save(model, fields, fail_message): + "Returns the save() method for a Form." + def save(self, commit=True): + return save_instance(self, model(), fields, fail_message, commit) + return save + +def make_instance_save(instance, fields, fail_message): + "Returns the save() method for a Form." + def save(self, commit=True): + return save_instance(self, instance, fields, fail_message, commit) + return save + +def form_for_model(model, form=BaseForm, fields=None, formfield_callback=lambda f: f.formfield()): """ Returns a Form class for the given Django model class. - Provide 'form' if you want to use a custom BaseForm subclass. + Provide ``form`` if you want to use a custom BaseForm subclass. + + Provide ``formfield_callback`` if you want to define different logic for + determining the formfield for a given database field. It's a callable that + takes a database Field instance and returns a form Field instance. """ opts = model._meta field_list = [] for f in opts.fields + opts.many_to_many: - formfield = f.formfield() + if not f.editable: + continue + if fields and not f.name in fields: + continue + formfield = formfield_callback(f) if formfield: field_list.append((f.name, formfield)) - fields = SortedDictFromList(field_list) - form = form or BaseForm - return type(opts.object_name + 'Form', (form,), {'fields': fields, '_model': model, 'create': create}) + base_fields = SortedDictFromList(field_list) + return type(opts.object_name + 'Form', (form,), + {'base_fields': base_fields, '_model': model, 'save': make_model_save(model, fields, 'created')}) + +def form_for_instance(instance, form=BaseForm, fields=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): + """ + Returns a Form class for the given Django model instance. + + Provide ``form`` if you want to use a custom BaseForm subclass. + + Provide ``formfield_callback`` if you want to define different logic for + determining the formfield for a given database field. It's a callable that + takes a database Field instance, plus **kwargs, and returns a form Field + instance with the given kwargs (i.e. 'initial'). + """ + model = instance.__class__ + opts = model._meta + field_list = [] + for f in opts.fields + opts.many_to_many: + if not f.editable: + continue + if fields and not f.name in fields: + continue + current_value = f.value_from_object(instance) + formfield = formfield_callback(f, initial=current_value) + if formfield: + field_list.append((f.name, formfield)) + base_fields = SortedDictFromList(field_list) + return type(opts.object_name + 'InstanceForm', (form,), + {'base_fields': base_fields, '_model': model, 'save': make_instance_save(instance, fields, 'changed')}) def form_for_fields(field_list): "Returns a Form class for the given list of Django database field instances." - fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list]) - return type('FormForFields', (BaseForm,), {'fields': fields}) + fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable]) + return type('FormForFields', (BaseForm,), {'base_fields': fields}) + +class QuerySetIterator(object): + def __init__(self, queryset, empty_label, cache_choices): + self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices + + def __iter__(self): + if self.empty_label is not None: + yield (u"", self.empty_label) + for obj in self.queryset: + yield (obj._get_pk_val(), str(obj)) + # Clear the QuerySet cache if required. + if not self.cache_choices: + self.queryset._result_cache = None + +class ModelChoiceField(ChoiceField): + "A ChoiceField whose choices are a model QuerySet." + # This class is a subclass of ChoiceField for purity, but it doesn't + # actually use any of ChoiceField's implementation. + def __init__(self, queryset, empty_label=u"---------", cache_choices=False, + required=True, widget=Select, label=None, initial=None, help_text=None): + self.queryset = queryset + self.empty_label = empty_label + self.cache_choices = cache_choices + # Call Field instead of ChoiceField __init__() because we don't need + # ChoiceField.__init__(). + Field.__init__(self, required, widget, label, initial, help_text) + self.widget.choices = self.choices + + def _get_choices(self): + # If self._choices is set, then somebody must have manually set + # the property self.choices. In this case, just return self._choices. + if hasattr(self, '_choices'): + return self._choices + # Otherwise, execute the QuerySet in self.queryset to determine the + # choices dynamically. Return a fresh QuerySetIterator that has not + # been consumed. Note that we're instantiating a new QuerySetIterator + # *each* time _get_choices() is called (and, thus, each time + # self.choices is accessed) so that we can ensure the QuerySet has not + # been consumed. + return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices) + + def _set_choices(self, value): + # This method is copied from ChoiceField._set_choices(). It's necessary + # because property() doesn't allow a subclass to overwrite only + # _get_choices without implementing _set_choices. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def clean(self, value): + Field.clean(self, value) + if value in ('', None): + return None + try: + value = self.queryset.model._default_manager.get(pk=value) + except self.queryset.model.DoesNotExist: + raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.')) + return value + +class ModelMultipleChoiceField(ModelChoiceField): + "A MultipleChoiceField whose choices are a model QuerySet." + hidden_widget = MultipleHiddenInput + def __init__(self, queryset, cache_choices=False, required=True, + widget=SelectMultiple, label=None, initial=None, help_text=None): + super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices, + required, widget, label, initial, help_text) + + def clean(self, value): + if self.required and not value: + raise ValidationError(gettext(u'This field is required.')) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(gettext(u'Enter a list of values.')) + final_values = [] + for val in value: + try: + obj = self.queryset.model._default_manager.get(pk=val) + except self.queryset.model.DoesNotExist: + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val) + else: + final_values.append(obj) + return final_values diff --git a/django/newforms/util.py b/django/newforms/util.py index a78623a17b..891585cba2 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -1,21 +1,14 @@ -from django.conf import settings +from django.utils.html import escape +from django.utils.encoding import smart_unicode -def smart_unicode(s): - if not isinstance(s, basestring): - s = unicode(str(s)) - elif not isinstance(s, unicode): - s = unicode(s, settings.DEFAULT_CHARSET) - return s - -class StrAndUnicode(object): +def flatatt(attrs): """ - A class whose __str__ returns its __unicode__ as a bytestring - according to settings.DEFAULT_CHARSET. - - Useful as a mix-in. + Convert a dictionary of attributes to a single string. + The returned string will contain a leading space followed by key="value", + XML-style pairs. It is assumed that the keys do not need to be XML-escaped. + If the passed dictionary is empty, then return an empty string. """ - def __str__(self): - return self.__unicode__().encode(settings.DEFAULT_CHARSET) + return u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) class ErrorDict(dict): """ diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 996e353775..6ee3177a25 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -2,31 +2,35 @@ HTML Widget classes """ -__all__ = ( - 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput', - 'Textarea', 'CheckboxInput', - 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', -) - -from util import StrAndUnicode, smart_unicode -from django.utils.datastructures import MultiValueDict -from django.utils.html import escape -from itertools import chain - try: set # Only available in Python 2.4+ except NameError: from sets import Set as set # Python 2.3 fallback +from itertools import chain -# Converts a dictionary to a single string with key="value", XML-style with -# a leading space. Assumes keys do not need to be XML-escaped. -flatatt = lambda attrs: u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) +from django.utils.datastructures import MultiValueDict +from django.utils.html import escape +from django.utils.translation import gettext +from django.utils.encoding import StrAndUnicode, smart_unicode + +from util import flatatt + +__all__ = ( + 'Widget', 'TextInput', 'PasswordInput', + 'HiddenInput', 'MultipleHiddenInput', + 'FileInput', 'Textarea', 'CheckboxInput', + 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', + 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', +) class Widget(object): is_hidden = False # Determines whether this corresponds to an <input type="hidden">. def __init__(self, attrs=None): - self.attrs = attrs or {} + if attrs is not None: + self.attrs = attrs.copy() + else: + self.attrs = {} def render(self, name, value, attrs=None): """ @@ -83,14 +87,48 @@ class TextInput(Input): class PasswordInput(Input): input_type = 'password' + def __init__(self, attrs=None, render_value=True): + self.attrs = attrs or {} + self.render_value = render_value + + def render(self, name, value, attrs=None): + if not self.render_value: value=None + return super(PasswordInput, self).render(name, value, attrs) + class HiddenInput(Input): input_type = 'hidden' is_hidden = True +class MultipleHiddenInput(HiddenInput): + """ + A widget that handles <input type="hidden"> for fields that have a list + of values. + """ + def __init__(self, attrs=None, choices=()): + # choices can be any iterable + self.attrs = attrs or {} + self.choices = choices + + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) + return u'\n'.join([(u'<input%s />' % flatatt(dict(value=smart_unicode(v), **final_attrs))) for v in value]) + + def value_from_datadict(self, data, name): + if isinstance(data, MultiValueDict): + return data.getlist(name) + return data.get(name, None) + class FileInput(Input): input_type = 'file' class Textarea(Widget): + def __init__(self, attrs=None): + # The 'rows' and 'cols' attributes are required for HTML correctness. + self.attrs = {'cols': '40', 'rows': '10'} + if attrs: + self.attrs.update(attrs) + def render(self, name, value, attrs=None): if value is None: value = '' value = smart_unicode(value) @@ -118,9 +156,11 @@ class CheckboxInput(Widget): class Select(Widget): def __init__(self, attrs=None, choices=()): - # choices can be any iterable self.attrs = attrs or {} - self.choices = choices + # choices can be any iterable, but we may need to render this widget + # multiple times. Thus, collapse it into a list so it can be consumed + # more than once. + self.choices = list(choices) def render(self, name, value, attrs=None, choices=()): if value is None: value = '' @@ -134,6 +174,25 @@ class Select(Widget): output.append(u'</select>') return u'\n'.join(output) +class NullBooleanSelect(Select): + """ + A Select Widget intended to be used with NullBooleanField. + """ + def __init__(self, attrs=None): + choices = ((u'1', gettext('Unknown')), (u'2', gettext('Yes')), (u'3', gettext('No'))) + super(NullBooleanSelect, self).__init__(attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + try: + value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value] + except KeyError: + value = u'1' + return super(NullBooleanSelect, self).render(name, value, attrs, choices) + + def value_from_datadict(self, data, name): + value = data.get(name, None) + return {u'2': True, u'3': False, True: True, False: False}.get(value, None) + class SelectMultiple(Widget): def __init__(self, attrs=None, choices=()): # choices can be any iterable @@ -162,17 +221,18 @@ class RadioInput(StrAndUnicode): def __init__(self, name, value, attrs, choice, index): self.name, self.value = name, value self.attrs = attrs - self.choice_value, self.choice_label = choice + self.choice_value = smart_unicode(choice[0]) + self.choice_label = smart_unicode(choice[1]) self.index = index def __unicode__(self): return u'<label>%s %s</label>' % (self.tag(), self.choice_label) def is_checked(self): - return self.value == smart_unicode(self.choice_value) + return self.value == self.choice_value def tag(self): - if self.attrs.has_key('id'): + if 'id' in self.attrs: self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): @@ -202,8 +262,8 @@ class RadioSelect(Select): "Returns a RadioFieldRenderer instance rather than a Unicode string." if value is None: value = '' str_value = smart_unicode(value) # Normalize to string. - attrs = attrs or {} - return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices))) + final_attrs = self.build_attrs(attrs) + return RadioFieldRenderer(name, str_value, final_attrs, list(chain(self.choices, choices))) def id_for_label(self, id_): # RadioSelect is represented by multiple <input type="radio"> fields, @@ -218,11 +278,16 @@ class RadioSelect(Select): class CheckboxSelectMultiple(SelectMultiple): def render(self, name, value, attrs=None, choices=()): if value is None: value = [] + has_id = attrs and 'id' in attrs final_attrs = self.build_attrs(attrs, name=name) output = [u'<ul>'] str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. - cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) - for option_value, option_label in chain(self.choices, choices): + for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): + # If an ID attribute was given, add a numeric index as a suffix, + # so that the checkboxes don't all have the same ID attribute. + if has_id: + final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) + cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) option_value = smart_unicode(option_value) rendered_cb = cb.render(name, option_value) output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(smart_unicode(option_label)))) @@ -235,3 +300,77 @@ class CheckboxSelectMultiple(SelectMultiple): id_ += '_0' return id_ id_for_label = classmethod(id_for_label) + +class MultiWidget(Widget): + """ + A widget that is composed of multiple widgets. + + Its render() method takes a "decompressed" list of values, not a single + value. Each value in this list is rendered in the corresponding widget -- + the first value is rendered in the first widget, the second value is + rendered in the second widget, etc. + + Subclasses should implement decompress(), which specifies how a single + value should be converted to a list of values. Subclasses should not + have to implement clean(). + + Subclasses may implement format_output(), which takes the list of rendered + widgets and returns HTML that formats them any way you'd like. + + You'll probably want to use this with MultiValueField. + """ + def __init__(self, widgets, attrs=None): + self.widgets = [isinstance(w, type) and w() or w for w in widgets] + super(MultiWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + # value is a list of values, each corresponding to a widget + # in self.widgets. + if not isinstance(value, list): + value = self.decompress(value) + output = [] + final_attrs = self.build_attrs(attrs) + id_ = final_attrs.get('id', None) + for i, widget in enumerate(self.widgets): + try: + widget_value = value[i] + except IndexError: + widget_value = None + if id_: + final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) + output.append(widget.render(name + '_%s' % i, widget_value, final_attrs)) + return self.format_output(output) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + + def value_from_datadict(self, data, name): + return [widget.value_from_datadict(data, name + '_%s' % i) for i, widget in enumerate(self.widgets)] + + def format_output(self, rendered_widgets): + return u''.join(rendered_widgets) + + def decompress(self, value): + """ + Returns a list of decompressed values for the given compressed value. + The given value can be assumed to be valid, but not necessarily + non-empty. + """ + raise NotImplementedError('Subclasses must implement this method.') + +class SplitDateTimeWidget(MultiWidget): + """ + A Widget that splits datetime input into two <input type="text"> boxes. + """ + def __init__(self, attrs=None): + widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs)) + super(SplitDateTimeWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + return [value.date(), value.time()] + return [None, None] |
