newforms: Added MultiValueField, SplitDateTimeField, MultiWidget, SplitDateTimeWidget

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4403 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2007-01-23 20:23:07 +00:00
parent 5b96692894
commit 8a6c337f2a
3 changed files with 236 additions and 2 deletions

View File

@ -3,7 +3,7 @@ Field classes
""" """
from django.utils.translation import gettext from django.utils.translation import gettext
from util import ValidationError, smart_unicode from util import ErrorList, ValidationError, smart_unicode
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, SelectMultiple from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, SelectMultiple
import datetime import datetime
import re import re
@ -16,7 +16,8 @@ __all__ = (
'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
'RegexField', 'EmailField', 'URLField', 'BooleanField', 'RegexField', 'EmailField', 'URLField', 'BooleanField',
'ChoiceField', 'MultipleChoiceField', 'ChoiceField', 'MultipleChoiceField',
'ComboField', 'ComboField', 'MultiValueField',
'SplitDateTimeField',
) )
# These values, if given to to_python(), will trigger the self.required check. # These values, if given to to_python(), will trigger the self.required check.
@ -376,6 +377,9 @@ class MultipleChoiceField(ChoiceField):
return new_value return new_value
class ComboField(Field): class ComboField(Field):
"""
A Field whose clean() method calls multiple Field clean() methods.
"""
def __init__(self, fields=(), required=True, widget=None, label=None, initial=None): def __init__(self, fields=(), required=True, widget=None, label=None, initial=None):
super(ComboField, self).__init__(required, widget, label, initial) super(ComboField, self).__init__(required, widget, label, initial)
# Set 'required' to False on the individual fields, because the # Set 'required' to False on the individual fields, because the
@ -394,3 +398,84 @@ class ComboField(Field):
for field in self.fields: for field in self.fields:
value = field.clean(value) value = field.clean(value)
return 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=(), required=True, widget=None, label=None, initial=None):
super(MultiValueField, self).__init__(required, widget, label, initial)
# 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, required=True, widget=None, label=None, initial=None):
fields = (DateField(), TimeField())
super(SplitDateTimeField, self).__init__(fields, required, widget, label, initial)
def compress(self, data_list):
if data_list:
return datetime.datetime.combine(*data_list)
return None

View File

@ -6,6 +6,7 @@ __all__ = (
'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput',
'FileInput', 'Textarea', 'CheckboxInput', 'FileInput', 'Textarea', 'CheckboxInput',
'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple',
'MultiWidget', 'SplitDateTimeWidget',
) )
from util import flatatt, StrAndUnicode, smart_unicode from util import flatatt, StrAndUnicode, smart_unicode
@ -252,3 +253,66 @@ class CheckboxSelectMultiple(SelectMultiple):
id_ += '_0' id_ += '_0'
return id_ return id_
id_for_label = classmethod(id_for_label) 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]

View File

@ -684,6 +684,39 @@ If 'choices' is passed to both the constructor and render(), then they'll both b
>>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')])
u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>' u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>'
# MultiWidget #################################################################
>>> class MyMultiWidget(MultiWidget):
... def decompress(self, value):
... if value:
... return value.split('__')
... return ['', '']
... def format_output(self, rendered_widgets):
... return u'<br />'.join(rendered_widgets)
>>> w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})))
>>> w.render('name', ['john', 'lennon'])
u'<input type="text" class="big" value="john" name="name_0" /><br /><input type="text" class="small" value="lennon" name="name_1" />'
>>> w.render('name', 'john__lennon')
u'<input type="text" class="big" value="john" name="name_0" /><br /><input type="text" class="small" value="lennon" name="name_1" />'
# SplitDateTimeWidget #########################################################
>>> w = SplitDateTimeWidget()
>>> w.render('date', '')
u'<input type="text" name="date_0" /><input type="text" name="date_1" />'
>>> w.render('date', None)
u'<input type="text" name="date_0" /><input type="text" name="date_1" />'
>>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30))
u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" name="date_1" value="07:30:00" />'
>>> w.render('date', [datetime.date(2006, 1, 10), datetime.time(7, 30)])
u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" name="date_1" value="07:30:00" />'
You can also pass 'attrs' to the constructor. In this case, the attrs will be
included on both widgets.
>>> w = SplitDateTimeWidget(attrs={'class': 'pretty'})
>>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30))
u'<input type="text" class="pretty" value="2006-01-10" name="date_0" /><input type="text" class="pretty" value="07:30:00" name="date_1" />'
########## ##########
# Fields # # Fields #
########## ##########
@ -1536,6 +1569,58 @@ u''
>>> f.clean(None) >>> f.clean(None)
u'' u''
# SplitDateTimeField ##########################################################
>>> f = SplitDateTimeField()
>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])
datetime.datetime(2006, 1, 10, 7, 30)
>>> f.clean(None)
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean('')
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean('hello')
Traceback (most recent call last):
...
ValidationError: [u'Enter a list of values.']
>>> f.clean(['hello', 'there'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid date.', u'Enter a valid time.']
>>> f.clean(['2006-01-10', 'there'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid time.']
>>> f.clean(['hello', '07:30'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid date.']
>>> f = SplitDateTimeField(required=False)
>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])
datetime.datetime(2006, 1, 10, 7, 30)
>>> f.clean(None)
>>> f.clean('')
>>> f.clean('hello')
Traceback (most recent call last):
...
ValidationError: [u'Enter a list of values.']
>>> f.clean(['hello', 'there'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid date.', u'Enter a valid time.']
>>> f.clean(['2006-01-10', 'there'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid time.']
>>> f.clean(['hello', '07:30'])
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid date.']
######### #########
# Forms # # Forms #
######### #########