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:
parent
5b96692894
commit
8a6c337f2a
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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 #
|
||||||
#########
|
#########
|
||||||
|
|
Loading…
Reference in New Issue