Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py (revision 7871)
+++ django/db/models/base.py (working copy)
@@ -429,7 +429,13 @@
def _get_FIELD_display(self, field):
value = getattr(self, field.attname)
- return force_unicode(dict(field.choices).get(value, value), strings_only=True)
+ flatchoices = []
+ for choice in field.choices:
+ if type(choice[1]) in (list, tuple):
+ flatchoices.extend(choice[1])
+ else:
+ flatchoices.append(choice)
+ return force_unicode(dict(flatchoices).get(value, value), strings_only=True)
def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs):
op = is_next and 'gt' or 'lt'
Index: django/db/models/fields/__init__.py
===================================================================
--- django/db/models/fields/__init__.py (revision 7871)
+++ django/db/models/fields/__init__.py (working copy)
@@ -299,7 +299,14 @@
else:
field_objs = [oldforms.SelectField]
- params['choices'] = self.get_choices_default()
+ flatchoices = []
+ for choice in self.get_choices_default():
+ if type(choice[1]) in (list, tuple):
+ flatchoices.extend(choice[1])
+ else:
+ flatchoices.append(choice)
+
+ params['choices'] = flatchoices
else:
field_objs = self.get_manipulator_field_objs()
return (field_objs, params)
Index: django/newforms/fields.py
===================================================================
--- django/newforms/fields.py (revision 7871)
+++ django/newforms/fields.py (working copy)
@@ -615,7 +615,13 @@
value = smart_unicode(value)
if value == u'':
return value
- valid_values = set([smart_unicode(k) for k, v in self.choices])
+ valid_values = []
+ for k, v in self.choices:
+ if type(v) in (tuple, list):
+ valid_values.extend([k2[0] for k2 in v])
+ else:
+ valid_values.append(k)
+ valid_values = set([smart_unicode(v) for v in valid_values])
if value not in valid_values:
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
@@ -640,7 +646,13 @@
raise ValidationError(self.error_messages['invalid_list'])
new_value = [smart_unicode(val) for val in value]
# Validate that each value in the value list is in self.choices.
- valid_values = set([smart_unicode(k) for k, v in self.choices])
+ valid_values = []
+ for k, v in self.choices:
+ if type(v) in (tuple, list):
+ valid_values.extend([k2[0] for k2 in v])
+ else:
+ valid_values.append(k)
+ valid_values = set([smart_unicode(v) for v in valid_values])
for val in new_value:
if val not in valid_values:
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
Index: django/newforms/widgets.py
===================================================================
--- django/newforms/widgets.py (revision 7871)
+++ django/newforms/widgets.py (working copy)
@@ -212,17 +212,32 @@
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
output = [u'')
+ options = self.render_options(choices, [value])
+ if options:
+ output.append(options)
+ output.append('')
return mark_safe(u'\n'.join(output))
+ def render_options(self, choices, selected_choices):
+ def render_option(option_value, option_label):
+ option_value = force_unicode(option_value)
+ selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
+ return u'' % (
+ escape(option_value), selected_html,
+ conditional_escape(force_unicode(option_label)))
+ # Normalize to strings.
+ selected_choices = set([force_unicode(v) for v in selected_choices])
+ output = []
+ for option_value, option_label in chain(self.choices, choices):
+ if isinstance(option_label, (list, tuple)):
+ output.append(u'')
+ else:
+ output.append(render_option(option_value, option_label))
+ return u'\n'.join(output)
+
class NullBooleanSelect(Select):
"""
A Select Widget intended to be used with NullBooleanField.
@@ -242,24 +257,15 @@
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=()):
- super(SelectMultiple, self).__init__(attrs)
- # choices can be any iterable
- self.choices = choices
-
+class SelectMultiple(Select):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = []
final_attrs = self.build_attrs(attrs, name=name)
output = [u'')
+ options = self.render_options(choices, value)
+ if options:
+ output.append(options)
+ output.append('')
return mark_safe(u'\n'.join(output))
def value_from_datadict(self, data, files, name):
Index: tests/regressiontests/forms/widgets.py
===================================================================
--- tests/regressiontests/forms/widgets.py (revision 7871)
+++ tests/regressiontests/forms/widgets.py (working copy)
@@ -419,6 +419,33 @@
+Choices can be nested one level in order to create HTML optgroups:
+>>> w.choices=(('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2'))))
+>>> print w.render('nestchoice', None)
+
+>>> print w.render('nestchoice', 'outer1')
+
+>>> print w.render('nestchoice', 'inner1')
+
+
# NullBooleanSelect Widget ####################################################
>>> w = NullBooleanSelect()
@@ -573,6 +600,41 @@
>>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')])
u''
+# Choices can be nested one level in order to create HTML optgroups:
+>>> w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2'))))
+>>> print w.render('nestchoice', None)
+
+>>> print w.render('nestchoice', ['outer1'])
+
+>>> print w.render('nestchoice', ['inner1'])
+
+>>> print w.render('nestchoice', ['outer1', 'inner2'])
+
+
# RadioSelect Widget ##########################################################
>>> w = RadioSelect()
Index: docs/model-api.txt
===================================================================
--- docs/model-api.txt (revision 7871)
+++ docs/model-api.txt (working copy)
@@ -568,6 +568,29 @@
class Foo(models.Model):
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
+You can also collect your available choices into named groups, which will
+be used for display purposes::
+
+ MEDIA_CHOICES = (
+ ('Audio', (
+ ('vinyl', 'Vinyl'),
+ ('cd', 'CD'),
+ )
+ ),
+ ('Video', (
+ ('vhs', 'VHS Tape'),
+ ('dvd', 'DVD'),
+ )
+ ),
+ ('unknown', 'Unknown'),
+ )
+
+The first element in each tuple is the name to apply to the group. The
+second element is an iterable of 2-tuples, with each 2-tuple containing
+a value and a human-readable name for an option. Grouped options may be
+combined with ungrouped options within a single list (such as the
+`unknown` option in this example).
+
For each model field that has ``choices`` set, Django will add a method to
retrieve the human-readable name for the field's current value. See
`get_FOO_display`_ in the database API documentation.
Index: docs/newforms.txt
===================================================================
--- docs/newforms.txt (revision 7871)
+++ docs/newforms.txt (working copy)
@@ -1233,8 +1233,12 @@
* Error message keys: ``required``, ``invalid_choice``
Takes one extra argument, ``choices``, which is an iterable (e.g., a list or
-tuple) of 2-tuples to use as choices for this field.
+tuple) of 2-tuples to use as choices for this field. This argument accepts
+the same formats as the ``choices`` argument to a model field. See the
+`model API documentation on choices`_ for more details.
+.. _model API documentation on choices: ../model-api#choices
+
``DateField``
~~~~~~~~~~~~~