Author: russellm
Date: 2008-07-19 02:53:02 -0500 (Sat, 19 Jul 2008)
New Revision: 7977

Modified:
   django/trunk/django/db/models/base.py
   django/trunk/django/db/models/fields/__init__.py
   django/trunk/django/forms/fields.py
   django/trunk/django/forms/widgets.py
   django/trunk/docs/model-api.txt
   django/trunk/docs/newforms.txt
   django/trunk/tests/regressiontests/forms/fields.py
   django/trunk/tests/regressiontests/forms/widgets.py
Log:
Fixed #4412 -- Added support for optgroups, both in the model when defining 
choices, and in the form field and widgets when the optgroups are displayed. 
Thanks to Matt McClanahan <[EMAIL PROTECTED]>, Tai Lee <[EMAIL PROTECTED]> and 
SmileyChris for their contributions at various stages in the life of this 
ticket.

Modified: django/trunk/django/db/models/base.py
===================================================================
--- django/trunk/django/db/models/base.py       2008-07-19 02:46:12 UTC (rev 
7976)
+++ django/trunk/django/db/models/base.py       2008-07-19 07:53:02 UTC (rev 
7977)
@@ -426,7 +426,7 @@
 
     def _get_FIELD_display(self, field):
         value = getattr(self, field.attname)
-        return force_unicode(dict(field.choices).get(value, value), 
strings_only=True)
+        return force_unicode(dict(field.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'

Modified: django/trunk/django/db/models/fields/__init__.py
===================================================================
--- django/trunk/django/db/models/fields/__init__.py    2008-07-19 02:46:12 UTC 
(rev 7976)
+++ django/trunk/django/db/models/fields/__init__.py    2008-07-19 07:53:02 UTC 
(rev 7977)
@@ -288,7 +288,7 @@
         if self.choices:
             field_objs = [oldforms.SelectField]
 
-            params['choices'] = self.get_choices_default()
+            params['choices'] = self.flatchoices
         else:
             field_objs = self.get_manipulator_field_objs()
         return (field_objs, params)
@@ -407,6 +407,16 @@
             return self._choices
     choices = property(_get_choices)
 
+    def _get_flatchoices(self):
+        flat = []
+        for choice, value in self.get_choices_default():
+            if type(value) in (list, tuple):
+                flat.extend(value)
+            else:
+                flat.append((choice,value))
+        return flat
+    flatchoices = property(_get_flatchoices)
+    
     def save_form_data(self, instance, data):
         setattr(instance, self.name, data)
 

Modified: django/trunk/django/forms/fields.py
===================================================================
--- django/trunk/django/forms/fields.py 2008-07-19 02:46:12 UTC (rev 7976)
+++ django/trunk/django/forms/fields.py 2008-07-19 07:53:02 UTC (rev 7977)
@@ -585,7 +585,7 @@
 class ChoiceField(Field):
     widget = Select
     default_error_messages = {
-        'invalid_choice': _(u'Select a valid choice. That choice is not one of 
the available choices.'),
+        'invalid_choice': _(u'Select a valid choice. %(value)s is not one of 
the available choices.'),
     }
 
     def __init__(self, choices=(), required=True, widget=None, label=None,
@@ -615,11 +615,23 @@
         value = smart_unicode(value)
         if value == u'':
             return value
-        valid_values = set([smart_unicode(k) for k, v in self.choices])
-        if value not in valid_values:
+        if not self.valid_value(value):
             raise ValidationError(self.error_messages['invalid_choice'] % 
{'value': value})
         return value
 
+    def valid_value(self, value):
+        "Check to see if the provided value is a valid choice"
+        for k, v in self.choices:
+            if type(v) in (tuple, list):
+                # This is an optgroup, so look inside the group for options
+                for k2, v2 in v:
+                    if value == smart_unicode(k2):
+                        return True
+            else:
+                if value == smart_unicode(k):
+                    return True
+        return False
+        
 class MultipleChoiceField(ChoiceField):
     hidden_widget = MultipleHiddenInput
     widget = SelectMultiple
@@ -640,9 +652,8 @@
             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])
         for val in new_value:
-            if val not in valid_values:
+            if not self.valid_value(val):
                 raise ValidationError(self.error_messages['invalid_choice'] % 
{'value': val})
         return new_value
 

Modified: django/trunk/django/forms/widgets.py
===================================================================
--- django/trunk/django/forms/widgets.py        2008-07-19 02:46:12 UTC (rev 
7976)
+++ django/trunk/django/forms/widgets.py        2008-07-19 07:53:02 UTC (rev 
7977)
@@ -345,17 +345,32 @@
         if value is None: value = ''
         final_attrs = self.build_attrs(attrs, name=name)
         output = [u'<select%s>' % flatatt(final_attrs)]
-        # Normalize to string.
-        str_value = force_unicode(value)
-        for option_value, option_label in chain(self.choices, choices):
-            option_value = force_unicode(option_value)
-            selected_html = (option_value == str_value) and u' 
selected="selected"' or ''
-            output.append(u'<option value="%s"%s>%s</option>' % (
-                    escape(option_value), selected_html,
-                    conditional_escape(force_unicode(option_label))))
-        output.append(u'</select>')
+        options = self.render_options(choices, [value])
+        if options:
+            output.append(options)
+        output.append('</select>')
         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'<option value="%s"%s>%s</option>' % (
+                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'<optgroup label="%s">' % 
escape(force_unicode(option_value)))
+                for option in option_label:
+                    output.append(render_option(*option))
+                output.append(u'</optgroup>')
+            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.
@@ -380,24 +395,15 @@
         # same thing as False.
         return bool(initial) != bool(data)
 
-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'<select multiple="multiple"%s>' % flatatt(final_attrs)]
-        str_values = set([force_unicode(v) for v in value]) # Normalize to 
strings.
-        for option_value, option_label in chain(self.choices, choices):
-            option_value = force_unicode(option_value)
-            selected_html = (option_value in str_values) and ' 
selected="selected"' or ''
-            output.append(u'<option value="%s"%s>%s</option>' % (
-                    escape(option_value), selected_html,
-                    conditional_escape(force_unicode(option_label))))
-        output.append(u'</select>')
+        options = self.render_options(choices, value)
+        if options:
+            output.append(options)
+        output.append('</select>')
         return mark_safe(u'\n'.join(output))
 
     def value_from_datadict(self, data, files, name):

Modified: django/trunk/docs/model-api.txt
===================================================================
--- django/trunk/docs/model-api.txt     2008-07-19 02:46:12 UTC (rev 7976)
+++ django/trunk/docs/model-api.txt     2008-07-19 07:53:02 UTC (rev 7977)
@@ -554,6 +554,29 @@
     class Foo(models.Model):
         gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
 
+You can also collect your available choices into named groups that can
+be used for organizational 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.

Modified: django/trunk/docs/newforms.txt
===================================================================
--- django/trunk/docs/newforms.txt      2008-07-19 02:46:12 UTC (rev 7976)
+++ django/trunk/docs/newforms.txt      2008-07-19 07:53:02 UTC (rev 7977)
@@ -1236,8 +1236,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``
 ~~~~~~~~~~~~~
 
@@ -1444,7 +1448,9 @@
     * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``
 
 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.
 
 ``NullBooleanField``
 ~~~~~~~~~~~~~~~~~~~~

Modified: django/trunk/tests/regressiontests/forms/fields.py
===================================================================
--- django/trunk/tests/regressiontests/forms/fields.py  2008-07-19 02:46:12 UTC 
(rev 7976)
+++ django/trunk/tests/regressiontests/forms/fields.py  2008-07-19 07:53:02 UTC 
(rev 7977)
@@ -980,7 +980,7 @@
 
 # ChoiceField #################################################################
 
->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')])
+>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')])
 >>> f.clean('')
 Traceback (most recent call last):
 ...
@@ -996,9 +996,9 @@
 >>> f.clean('3')
 Traceback (most recent call last):
 ...
-ValidationError: [u'Select a valid choice. That choice is not one of the 
available choices.']
+ValidationError: [u'Select a valid choice. 3 is not one of the available 
choices.']
 
->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False)
+>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False)
 >>> f.clean('')
 u''
 >>> f.clean(None)
@@ -1010,7 +1010,7 @@
 >>> f.clean('3')
 Traceback (most recent call last):
 ...
-ValidationError: [u'Select a valid choice. That choice is not one of the 
available choices.']
+ValidationError: [u'Select a valid choice. 3 is not one of the available 
choices.']
 
 >>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')])
 >>> f.clean('J')
@@ -1018,8 +1018,26 @@
 >>> f.clean('John')
 Traceback (most recent call last):
 ...
-ValidationError: [u'Select a valid choice. That choice is not one of the 
available choices.']
+ValidationError: [u'Select a valid choice. John is not one of the available 
choices.']
 
+>>> f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), 
('Letters', (('3','A'),('4','B'))), ('5','Other')])
+>>> f.clean(1)
+u'1'
+>>> f.clean('1')
+u'1'
+>>> f.clean(3)
+u'3'
+>>> f.clean('3')
+u'3'
+>>> f.clean(5)
+u'5'
+>>> f.clean('5')
+u'5'
+>>> f.clean('6')
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 6 is not one of the available 
choices.']
+
 # NullBooleanField ############################################################
 
 >>> f = NullBooleanField()
@@ -1036,7 +1054,7 @@
 
 # MultipleChoiceField #########################################################
 
->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')])
+>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')])
 >>> f.clean('')
 Traceback (most recent call last):
 ...
@@ -1072,7 +1090,7 @@
 ...
 ValidationError: [u'Select a valid choice. 3 is not one of the available 
choices.']
 
->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')], required=False)
+>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], 
required=False)
 >>> f.clean('')
 []
 >>> f.clean(None)
@@ -1100,6 +1118,29 @@
 ...
 ValidationError: [u'Select a valid choice. 3 is not one of the available 
choices.']
 
+>>> f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 
'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')])
+>>> f.clean([1])
+[u'1']
+>>> f.clean(['1'])
+[u'1']
+>>> f.clean([1, 5])
+[u'1', u'5']
+>>> f.clean([1, '5'])
+[u'1', u'5']
+>>> f.clean(['1', 5])
+[u'1', u'5']
+>>> f.clean(['1', '5'])
+[u'1', u'5']
+>>> f.clean(['6'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 6 is not one of the available 
choices.']
+>>> f.clean(['1','6'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 6 is not one of the available 
choices.']
+
+
 # ComboField ##################################################################
 
 ComboField takes a list of fields that should be used to validate a value,
@@ -1165,7 +1206,7 @@
 >>> f.clean('fields.py')
 Traceback (most recent call last):
 ...
-ValidationError: [u'Select a valid choice. That choice is not one of the 
available choices.']
+ValidationError: [u'Select a valid choice. fields.py is not one of the 
available choices.']
 >>> fix_os_paths(f.clean(path + 'fields.py'))
 u'.../django/forms/fields.py'
 >>> f = forms.FilePathField(path=path, match='^.*?\.py$')

Modified: django/trunk/tests/regressiontests/forms/widgets.py
===================================================================
--- django/trunk/tests/regressiontests/forms/widgets.py 2008-07-19 02:46:12 UTC 
(rev 7976)
+++ django/trunk/tests/regressiontests/forms/widgets.py 2008-07-19 07:53:02 UTC 
(rev 7977)
@@ -458,6 +458,35 @@
 <option value="4">4</option>
 </select>
 
+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)
+<select name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
+>>> print w.render('nestchoice', 'outer1')
+<select name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
+>>> print w.render('nestchoice', 'inner1')
+<select name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1" selected="selected">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
 # NullBooleanSelect Widget ####################################################
 
 >>> w = NullBooleanSelect()
@@ -626,6 +655,44 @@
 >>> w._has_changed([1, 2], [u'1', u'3'])
 True
 
+# 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)
+<select multiple="multiple" name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
+>>> print w.render('nestchoice', ['outer1'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
+>>> print w.render('nestchoice', ['inner1'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1" selected="selected">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
+>>> print w.render('nestchoice', ['outer1', 'inner2'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group &quot;1&quot;">
+<option value="inner1">Inner 1</option>
+<option value="inner2" selected="selected">Inner 2</option>
+</optgroup>
+</select>
+
 # RadioSelect Widget ##########################################################
 
 >>> w = RadioSelect()


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to