summaryrefslogtreecommitdiff
path: root/django/newforms
diff options
context:
space:
mode:
authorRobin Munn <robin.munn@gmail.com>2007-01-31 23:43:09 +0000
committerRobin Munn <robin.munn@gmail.com>2007-01-31 23:43:09 +0000
commitfe361e678a46dc4c717c79c2f12b3ba32293b81a (patch)
tree8f42488e7d95244bab3db7b2bf934e006940521a /django/newforms
parent122426e7453ed638a0c5be7e8b925adcddea3889 (diff)
Merged revisions 4186 to 4454 from trunk.
git-svn-id: http://code.djangoproject.com/svn/django/branches/sqlalchemy@4455 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/newforms')
-rw-r--r--django/newforms/__init__.py2
-rw-r--r--django/newforms/extras/__init__.py1
-rw-r--r--django/newforms/extras/widgets.py59
-rw-r--r--django/newforms/fields.py275
-rw-r--r--django/newforms/forms.py120
-rw-r--r--django/newforms/models.py99
-rw-r--r--django/newforms/util.py5
-rw-r--r--django/newforms/widgets.py157
8 files changed, 612 insertions, 106 deletions
diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py
index a445a21bfb..0d9c68f9e0 100644
--- a/django/newforms/__init__.py
+++ b/django/newforms/__init__.py
@@ -13,5 +13,5 @@ TODO:
from util import ValidationError
from widgets import *
from fields import *
-from forms import Form
+from forms import *
from models import *
diff --git a/django/newforms/extras/__init__.py b/django/newforms/extras/__init__.py
new file mode 100644
index 0000000000..a7f6a9b3f6
--- /dev/null
+++ b/django/newforms/extras/__init__.py
@@ -0,0 +1 @@
+from widgets import *
diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py
new file mode 100644
index 0000000000..1011934fb8
--- /dev/null
+++ b/django/newforms/extras/widgets.py
@@ -0,0 +1,59 @@
+"""
+Extra HTML Widget classes
+"""
+
+from django.newforms.widgets import Widget, Select
+from django.utils.dates import MONTHS
+import datetime
+
+__all__ = ('SelectDateWidget',)
+
+class SelectDateWidget(Widget):
+ """
+ A Widget that splits date input into three <select> boxes.
+
+ This also serves as an example of a Widget that has more than one HTML
+ element and hence implements value_from_datadict.
+ """
+ month_field = '%s_month'
+ day_field = '%s_day'
+ year_field = '%s_year'
+
+ def __init__(self, attrs=None, years=None):
+ # years is an optional list/tuple of years to use in the "year" select box.
+ self.attrs = attrs or {}
+ if years:
+ self.years = years
+ else:
+ this_year = datetime.date.today().year
+ self.years = range(this_year, this_year+10)
+
+ def render(self, name, value, attrs=None):
+ try:
+ value = datetime.date(*map(int, value.split('-')))
+ year_val, month_val, day_val = value.year, value.month, value.day
+ except (AttributeError, TypeError, ValueError):
+ year_val = month_val = day_val = None
+
+ output = []
+
+ month_choices = MONTHS.items()
+ month_choices.sort()
+ select_html = Select(choices=month_choices).render(self.month_field % name, month_val)
+ output.append(select_html)
+
+ day_choices = [(i, i) for i in range(1, 32)]
+ select_html = Select(choices=day_choices).render(self.day_field % name, day_val)
+ output.append(select_html)
+
+ year_choices = [(i, i) for i in self.years]
+ select_html = Select(choices=year_choices).render(self.year_field % name, year_val)
+ output.append(select_html)
+
+ return u'\n'.join(output)
+
+ def value_from_datadict(self, data, name):
+ y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
+ if y and m and d:
+ return '%s-%s-%s' % (y, m, d)
+ return None
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
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index e0b3d500b5..7e29941bef 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -2,11 +2,13 @@
Form classes
"""
-from django.utils.datastructures import SortedDict
+from django.utils.datastructures import SortedDict, MultiValueDict
from django.utils.html import escape
from fields import Field
-from widgets import TextInput, Textarea, HiddenInput
-from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError
+from widgets import TextInput, Textarea, HiddenInput, MultipleHiddenInput
+from util import flatatt, StrAndUnicode, ErrorDict, ErrorList, ValidationError
+
+__all__ = ('BaseForm', 'Form')
NON_FIELD_ERRORS = '__all__'
@@ -24,24 +26,35 @@ class SortedDictFromList(SortedDict):
self.keyOrder = [d[0] for d in data]
dict.__init__(self, dict(data))
+ def copy(self):
+ return SortedDictFromList(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'."
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)
+ attrs['base_fields'] = SortedDictFromList(fields)
return type.__new__(cls, name, bases, attrs)
-class Form(StrAndUnicode):
- "A collection of Fields, plus their associated data."
- __metaclass__ = DeclarativeFieldsMetaclass
-
- def __init__(self, data=None, auto_id=False): # TODO: prefix stuff
- self.ignore_errors = data is None
+class BaseForm(StrAndUnicode):
+ # This is the main implementation of all the Form logic. Note that this
+ # 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, initial=None):
+ self.is_bound = data is not None
self.data = data or {}
self.auto_id = auto_id
- self.clean_data = None # Stores the data after clean() has been called.
+ self.prefix = prefix
+ 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()
@@ -70,9 +83,18 @@ class Form(StrAndUnicode):
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):
+ """
+ Returns the field name with a prefix appended, if this Form has a
+ prefix set.
+
+ Subclasses may wish to override.
+ """
+ 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 = [], []
@@ -86,7 +108,12 @@ class Form(StrAndUnicode):
else:
if errors_on_separate_row and bf_errors:
output.append(error_row % bf_errors)
- output.append(normal_row % {'errors': bf_errors, 'label': bf.label_tag(escape(bf.verbose_name+':')), 'field': bf})
+ label = bf.label and bf.label_tag(escape(bf.label + ':')) or ''
+ 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.
@@ -101,15 +128,15 @@ class Form(StrAndUnicode):
def as_table(self):
"Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
- return self._html_output(u'<tr><td>%(label)s</td><td>%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', True)
+ 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):
"""
@@ -123,16 +150,16 @@ class Form(StrAndUnicode):
"""
Cleans all of self.data and populates self.__errors and self.clean_data.
"""
- self.clean_data = {}
errors = ErrorDict()
- if self.ignore_errors: # Stop further processing.
+ if not self.is_bound: # Stop further processing.
self.__errors = errors
return
+ self.clean_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
# widgets split data over several HTML fields.
- value = field.widget.value_from_datadict(self.data, name)
+ value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
try:
value = field.clean(value)
self.clean_data[name] = value
@@ -146,7 +173,7 @@ class Form(StrAndUnicode):
except ValidationError, e:
errors[NON_FIELD_ERRORS] = e.messages
if errors:
- self.clean_data = None
+ delattr(self, 'clean_data')
self.__errors = errors
def clean(self):
@@ -158,12 +185,26 @@ class Form(StrAndUnicode):
"""
return self.clean_data
+class Form(BaseForm):
+ "A collection of Fields, plus their associated data."
+ # This is a separate class from BaseForm in order to abstract the way
+ # self.fields is specified. This class (Form) is the one that does the
+ # fancy metaclass stuff purely for the semantic sugar -- it allows one
+ # to define a form using declarative syntax.
+ # BaseForm itself has no way of designating self.fields.
+ __metaclass__ = DeclarativeFieldsMetaclass
+
class BoundField(StrAndUnicode):
"A Field plus data"
def __init__(self, form, field, name):
self.form = form
self.field = field
self.name = name
+ self.html_name = form.add_prefix(name)
+ if self.field.label is None:
+ self.label = pretty_name(name)
+ else:
+ self.label = self.field.label
def __unicode__(self):
"Renders this field as an HTML widget."
@@ -190,7 +231,11 @@ class BoundField(StrAndUnicode):
auto_id = self.auto_id
if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'):
attrs['id'] = auto_id
- return widget.render(self.name, self.data, attrs=attrs)
+ if not self.form.is_bound:
+ data = self.form.initial.get(self.name, self.field.initial)
+ else:
+ data = self.data
+ return widget.render(self.html_name, data, attrs=attrs)
def as_text(self, attrs=None):
"""
@@ -206,28 +251,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."
- return self.form.data.get(self.name, None)
+ """
+ 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 _verbose_name(self):
- return pretty_name(self.name)
- verbose_name = property(_verbose_name)
-
- 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 verbose_name.
+ field's HTML-escaped label.
+
+ If attrs are given, they're used as HTML attributes on the <label> tag.
"""
- contents = contents or escape(self.verbose_name)
+ 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):
@@ -242,8 +288,8 @@ class BoundField(StrAndUnicode):
"""
auto_id = self.form.auto_id
if auto_id and '%s' in str(auto_id):
- return str(auto_id) % self.name
+ return str(auto_id) % self.html_name
elif auto_id:
- return self.name
+ return self.html_name
return ''
auto_id = property(_auto_id)
diff --git a/django/newforms/models.py b/django/newforms/models.py
index d99619c44a..a938b6350e 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -1,13 +1,98 @@
"""
-Helper functions for creating Forms from Django models and database field objects.
+Helper functions for creating Form classes from Django models
+and database field objects.
"""
-__all__ = ('form_for_model', 'form_for_fields')
+from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
-def form_for_model(model):
- "Returns a Form instance for the given Django model class."
- raise NotImplementedError
+__all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields')
+
+def model_save(self, commit=True):
+ """
+ Creates and returns model instance according to self.clean_data.
+
+ This method is created for any form_for_model Form.
+ """
+ if self.errors:
+ raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name)
+ return save_instance(self, self._model(), commit)
+
+def save_instance(form, instance, commit=True):
+ """
+ Saves bound Form ``form``'s clean_data into model instance ``instance``.
+
+ Assumes ``form`` has a field for every non-AutoField database field in
+ ``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 changed because the data didn't validate." % opts.object_name)
+ clean_data = form.clean_data
+ for f in opts.fields:
+ if isinstance(f, models.AutoField):
+ continue
+ setattr(instance, f.attname, clean_data[f.name])
+ if commit:
+ instance.save()
+ for f in opts.many_to_many:
+ setattr(instance, f.attname, clean_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_instance_save(instance):
+ "Returns the save() method for a form_for_instance Form."
+ def save(self, commit=True):
+ return save_instance(self, instance, commit)
+ return save
+
+def form_for_model(model, form=BaseForm, 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 ``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 = formfield_callback(f)
+ if formfield:
+ field_list.append((f.name, formfield))
+ fields = SortedDictFromList(field_list)
+ return type(opts.object_name + 'Form', (form,), {'base_fields': fields, '_model': model, 'save': model_save})
+
+def form_for_instance(instance, form=BaseForm, 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:
+ current_value = f.value_from_object(instance)
+ formfield = formfield_callback(f, initial=current_value)
+ if formfield:
+ field_list.append((f.name, formfield))
+ fields = SortedDictFromList(field_list)
+ return type(opts.object_name + 'InstanceForm', (form,),
+ {'base_fields': fields, '_model': model, 'save': make_instance_save(instance)})
def form_for_fields(field_list):
- "Returns a Form instance for the given list of Django database field instances."
- raise NotImplementedError
+ "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,), {'base_fields': fields})
diff --git a/django/newforms/util.py b/django/newforms/util.py
index a78623a17b..2fec9e4e2e 100644
--- a/django/newforms/util.py
+++ b/django/newforms/util.py
@@ -1,4 +1,9 @@
from django.conf import settings
+from django.utils.html import escape
+
+# 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()])
def smart_unicode(s):
if not isinstance(s, basestring):
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index 1a2d7d67c9..585e12cc18 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -3,13 +3,16 @@ HTML Widget classes
"""
__all__ = (
- 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput',
- 'Textarea', 'CheckboxInput',
- 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple',
+ 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput',
+ 'FileInput', 'Textarea', 'CheckboxInput',
+ 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple',
+ 'MultiWidget', 'SplitDateTimeWidget',
)
-from util import StrAndUnicode, smart_unicode
+from util import flatatt, StrAndUnicode, smart_unicode
+from django.utils.datastructures import MultiValueDict
from django.utils.html import escape
+from django.utils.translation import gettext
from itertools import chain
try:
@@ -17,18 +20,19 @@ try:
except NameError:
from sets import Set as set # Python 2.3 fallback
-# 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()])
-
class Widget(object):
- requires_data_list = False # Determines whether render()'s 'value' argument should be a list.
is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
def __init__(self, attrs=None):
self.attrs = attrs or {}
- def render(self, name, value):
+ def render(self, name, value, attrs=None):
+ """
+ Returns this Widget rendered as HTML, as a Unicode string.
+
+ The 'value' given is not guaranteed to be valid input, so subclass
+ implementations should program defensively.
+ """
raise NotImplementedError
def build_attrs(self, extra_attrs=None, **kwargs):
@@ -64,6 +68,7 @@ class Input(Widget):
type='radio', which are special).
"""
input_type = None # Subclasses must define this.
+
def render(self, name, value, attrs=None):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
@@ -80,6 +85,26 @@ 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'
@@ -111,9 +136,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 = ''
@@ -127,8 +154,26 @@ 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):
- requires_data_list = True
def __init__(self, attrs=None, choices=()):
# choices can be any iterable
self.attrs = attrs or {}
@@ -146,19 +191,25 @@ class SelectMultiple(Widget):
output.append(u'</select>')
return u'\n'.join(output)
+ def value_from_datadict(self, data, name):
+ if isinstance(data, MultiValueDict):
+ return data.getlist(name)
+ return data.get(name, None)
+
class RadioInput(StrAndUnicode):
"An object used by RadioFieldRenderer that represents a single <input type='radio'>."
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'):
@@ -178,6 +229,10 @@ class RadioFieldRenderer(StrAndUnicode):
for i, choice in enumerate(self.choices):
yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
+ def __getitem__(self, idx):
+ choice = self.choices[idx] # Let the IndexError propogate
+ return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
+
def __unicode__(self):
"Outputs a <ul> for this set of radio fields."
return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % w for w in self])
@@ -203,11 +258,16 @@ class RadioSelect(Select):
class CheckboxSelectMultiple(SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = []
+ has_id = attrs and attrs.has_key('id')
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))))
@@ -220,3 +280,66 @@ 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 = []
+ for i, widget in enumerate(self.widgets):
+ try:
+ widget_value = value[i]
+ except KeyError:
+ widget_value = None
+ output.append(widget.render(name + '_%s' % i, widget_value, attrs))
+ return self.format_output(output)
+
+ def value_from_datadict(self, data, name):
+ return [data.get(name + '_%s' % i) for i in range(len(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]