diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index 30364e1ebc..5ec27653cf 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -2,16 +2,26 @@
HTML Widget classes
"""
-__all__ = ('Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput', 'Textarea', 'CheckboxInput', 'Select')
+__all__ = (
+ 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput',
+ 'Textarea', 'CheckboxInput',
+ 'Select', 'SelectMultiple',
+)
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
+
# Converts a dictionary to a single string with key="value", XML-style.
# Assumes keys do not need to be XML-escaped.
flatatt = lambda attrs: ' '.join(['%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.
def __init__(self, attrs=None):
self.attrs = attrs or {}
@@ -70,14 +80,31 @@ class Select(Widget):
final_attrs.update(attrs)
output = [u'')
return u'\n'.join(output)
class SelectMultiple(Widget):
- pass
+ requires_data_list = True
+ 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 = dict(self.attrs, name=name)
+ if attrs:
+ final_attrs.update(attrs)
+ output = [u'')
+ return u'\n'.join(output)
class RadioSelect(Widget):
pass
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 2644355951..49907cd9e9 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -236,6 +236,110 @@ If 'choices' is passed to both the constructor and render(), then they'll both b
+# SelectMultiple Widget #######################################################
+
+>>> w = SelectMultiple()
+>>> print w.render('beatles', ['J'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+>>> print w.render('beatles', ['J', 'P'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+>>> print w.render('beatles', ['J', 'P', 'R'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+
+If the value is None, none of the options are selected:
+>>> print w.render('beatles', None, choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+
+If the value corresponds to a label (but not to an option value), none of the options are selected:
+>>> print w.render('beatles', ['John'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+
+If multiple values are given, but some of them are not valid, the valid ones are selected:
+>>> print w.render('beatles', ['J', 'G', 'foo'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
+
+
+The value is compared to its str():
+>>> print w.render('nums', [2], choices=[('1', '1'), ('2', '2'), ('3', '3')])
+
+>>> print w.render('nums', ['2'], choices=[(1, 1), (2, 2), (3, 3)])
+
+>>> print w.render('nums', [2], choices=[(1, 1), (2, 2), (3, 3)])
+
+
+The 'choices' argument can be any iterable:
+>>> def get_choices():
+... for i in range(5):
+... yield (i, i)
+>>> print w.render('nums', [2], choices=get_choices())
+
+
+You can also pass 'choices' to the constructor:
+>>> w = SelectMultiple(choices=[(1, 1), (2, 2), (3, 3)])
+>>> print w.render('nums', [2])
+
+
+If 'choices' is passed to both the constructor and render(), then they'll both be in the output:
+>>> print w.render('nums', [2], choices=[(4, 4), (5, 5)])
+
+
# CharField ###################################################################
>>> f = CharField(required=False)