Django

Code

Changeset 7977

Show
Ignore:
Timestamp:
07/19/08 02:53:02 (3 months ago)
Author:
russellm
Message:

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? <cardinal@dodds.net>, Tai Lee <real.human@mrmachine.net> and SmileyChris? for their contributions at various stages in the life of this ticket.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/db/models/base.py

    r7967 r7977  
    427427    def _get_FIELD_display(self, field): 
    428428        value = getattr(self, field.attname) 
    429         return force_unicode(dict(field.choices).get(value, value), strings_only=True) 
     429        return force_unicode(dict(field.flatchoices).get(value, value), strings_only=True) 
    430430 
    431431    def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): 
  • django/trunk/django/db/models/fields/__init__.py

    r7971 r7977  
    289289            field_objs = [oldforms.SelectField] 
    290290 
    291             params['choices'] = self.get_choices_default() 
     291            params['choices'] = self.flatchoices 
    292292        else: 
    293293            field_objs = self.get_manipulator_field_objs() 
     
    408408    choices = property(_get_choices) 
    409409 
     410    def _get_flatchoices(self): 
     411        flat = [] 
     412        for choice, value in self.get_choices_default(): 
     413            if type(value) in (list, tuple): 
     414                flat.extend(value) 
     415            else: 
     416                flat.append((choice,value)) 
     417        return flat 
     418    flatchoices = property(_get_flatchoices) 
     419     
    410420    def save_form_data(self, instance, data): 
    411421        setattr(instance, self.name, data) 
  • django/trunk/django/forms/fields.py

    r7971 r7977  
    586586    widget = Select 
    587587    default_error_messages = { 
    588         'invalid_choice': _(u'Select a valid choice. That choice is not one of the available choices.'), 
     588        'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'), 
    589589    } 
    590590 
     
    616616        if value == u'': 
    617617            return value 
    618         valid_values = set([smart_unicode(k) for k, v in self.choices]) 
    619         if value not in valid_values: 
     618        if not self.valid_value(value): 
    620619            raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) 
    621620        return value 
    622621 
     622    def valid_value(self, value): 
     623        "Check to see if the provided value is a valid choice" 
     624        for k, v in self.choices: 
     625            if type(v) in (tuple, list): 
     626                # This is an optgroup, so look inside the group for options 
     627                for k2, v2 in v: 
     628                    if value == smart_unicode(k2): 
     629                        return True 
     630            else: 
     631                if value == smart_unicode(k): 
     632                    return True 
     633        return False 
     634         
    623635class MultipleChoiceField(ChoiceField): 
    624636    hidden_widget = MultipleHiddenInput 
     
    641653        new_value = [smart_unicode(val) for val in value] 
    642654        # Validate that each value in the value list is in self.choices. 
    643         valid_values = set([smart_unicode(k) for k, v in self.choices]) 
    644655        for val in new_value: 
    645             if val not in valid_values
     656            if not self.valid_value(val)
    646657                raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) 
    647658        return new_value 
  • django/trunk/django/forms/widgets.py

    r7971 r7977  
    346346        final_attrs = self.build_attrs(attrs, name=name) 
    347347        output = [u'<select%s>' % flatatt(final_attrs)] 
    348         # Normalize to string. 
    349         str_value = force_unicode(value) 
     348        options = self.render_options(choices, [value]) 
     349        if options: 
     350            output.append(options) 
     351        output.append('</select>') 
     352        return mark_safe(u'\n'.join(output)) 
     353 
     354    def render_options(self, choices, selected_choices): 
     355        def render_option(option_value, option_label): 
     356            option_value = force_unicode(option_value) 
     357            selected_html = (option_value in selected_choices) and u' selected="selected"' or '' 
     358            return u'<option value="%s"%s>%s</option>' % ( 
     359                escape(option_value), selected_html, 
     360                conditional_escape(force_unicode(option_label))) 
     361        # Normalize to strings. 
     362        selected_choices = set([force_unicode(v) for v in selected_choices]) 
     363        output = [] 
    350364        for option_value, option_label in chain(self.choices, choices): 
    351             option_value = force_unicode(option_value) 
    352             selected_html = (option_value == str_value) and u' selected="selected"' or '' 
    353             output.append(u'<option value="%s"%s>%s</option>' % ( 
    354                     escape(option_value), selected_html, 
    355                     conditional_escape(force_unicode(option_label)))) 
    356         output.append(u'</select>') 
    357         return mark_safe(u'\n'.join(output)) 
     365            if isinstance(option_label, (list, tuple)): 
     366                output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value))) 
     367                for option in option_label: 
     368                    output.append(render_option(*option)) 
     369                output.append(u'</optgroup>') 
     370            else: 
     371                output.append(render_option(option_value, option_label)) 
     372        return u'\n'.join(output) 
    358373 
    359374class NullBooleanSelect(Select): 
     
    381396        return bool(initial) != bool(data) 
    382397 
    383 class SelectMultiple(Widget): 
    384     def __init__(self, attrs=None, choices=()): 
    385         super(SelectMultiple, self).__init__(attrs) 
    386         # choices can be any iterable 
    387         self.choices = choices 
    388  
     398class SelectMultiple(Select): 
    389399    def render(self, name, value, attrs=None, choices=()): 
    390400        if value is None: value = [] 
    391401        final_attrs = self.build_attrs(attrs, name=name) 
    392402        output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)] 
    393         str_values = set([force_unicode(v) for v in value]) # Normalize to strings. 
    394         for option_value, option_label in chain(self.choices, choices): 
    395             option_value = force_unicode(option_value) 
    396             selected_html = (option_value in str_values) and ' selected="selected"' or '' 
    397             output.append(u'<option value="%s"%s>%s</option>' % ( 
    398                     escape(option_value), selected_html, 
    399                     conditional_escape(force_unicode(option_label)))) 
    400         output.append(u'</select>') 
     403        options = self.render_options(choices, value) 
     404        if options: 
     405            output.append(options) 
     406        output.append('</select>') 
    401407        return mark_safe(u'\n'.join(output)) 
    402408 
  • django/trunk/docs/model-api.txt

    r7967 r7977  
    554554    class Foo(models.Model): 
    555555        gender = models.CharField(max_length=1, choices=GENDER_CHOICES) 
     556 
     557You can also collect your available choices into named groups that can 
     558be used for organizational purposes:: 
     559 
     560    MEDIA_CHOICES = ( 
     561        ('Audio', ( 
     562                ('vinyl', 'Vinyl'), 
     563                ('cd', 'CD'), 
     564            ) 
     565        ), 
     566        ('Video', ( 
     567                ('vhs', 'VHS Tape'), 
     568                ('dvd', 'DVD'), 
     569            ) 
     570        ), 
     571        ('unknown', 'Unknown'), 
     572    ) 
     573 
     574The first element in each tuple is the name to apply to the group. The  
     575second element is an iterable of 2-tuples, with each 2-tuple containing 
     576a value and a human-readable name for an option. Grouped options may be  
     577combined with ungrouped options within a single list (such as the  
     578`unknown` option in this example). 
    556579 
    557580For each model field that has ``choices`` set, Django will add a method to 
  • django/trunk/docs/newforms.txt

    r7967 r7977  
    12371237 
    12381238Takes one extra argument, ``choices``, which is an iterable (e.g., a list or 
    1239 tuple) of 2-tuples to use as choices for this field. 
     1239tuple) of 2-tuples to use as choices for this field. This argument accepts 
     1240the same formats as the ``choices`` argument to a model field. See the  
     1241`model API documentation on choices`_ for more details. 
     1242 
     1243.. _model API documentation on choices: ../model-api#choices 
    12401244 
    12411245``DateField`` 
     
    14451449 
    14461450Takes one extra argument, ``choices``, which is an iterable (e.g., a list or 
    1447 tuple) of 2-tuples to use as choices for this field. 
     1451tuple) of 2-tuples to use as choices for this field. This argument accepts 
     1452the same formats as the ``choices`` argument to a model field. See the  
     1453`model API documentation on choices`_ for more details. 
    14481454 
    14491455``NullBooleanField`` 
  • django/trunk/tests/regressiontests/forms/fields.py

    r7971 r7977  
    981981# ChoiceField ################################################################# 
    982982 
    983 >>> f = ChoiceField(choices=[('1', '1'), ('2', '2')]) 
     983>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) 
    984984>>> f.clean('') 
    985985Traceback (most recent call last): 
     
    997997Traceback (most recent call last): 
    998998... 
    999 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] 
    1000  
    1001 >>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False) 
     999ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] 
     1000 
     1001>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) 
    10021002>>> f.clean('') 
    10031003u'' 
     
    10111011Traceback (most recent call last): 
    10121012... 
    1013 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] 
     1013ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] 
    10141014 
    10151015>>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) 
     
    10191019Traceback (most recent call last): 
    10201020... 
    1021 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] 
     1021ValidationError: [u'Select a valid choice. John is not one of the available choices.'] 
     1022 
     1023>>> f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) 
     1024>>> f.clean(1) 
     1025u'1' 
     1026>>> f.clean('1') 
     1027u'1' 
     1028>>> f.clean(3) 
     1029u'3' 
     1030>>> f.clean('3') 
     1031u'3' 
     1032>>> f.clean(5) 
     1033u'5' 
     1034>>> f.clean('5') 
     1035u'5' 
     1036>>> f.clean('6') 
     1037Traceback (most recent call last): 
     1038... 
     1039ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] 
    10221040 
    10231041# NullBooleanField ############################################################ 
     
    10371055# MultipleChoiceField ######################################################### 
    10381056 
    1039 >>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) 
     1057>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) 
    10401058>>> f.clean('') 
    10411059Traceback (most recent call last): 
     
    10731091ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] 
    10741092 
    1075 >>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')], required=False) 
     1093>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) 
    10761094>>> f.clean('') 
    10771095[] 
     
    11011119ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] 
    11021120 
     1121>>> f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) 
     1122>>> f.clean([1]) 
     1123[u'1'] 
     1124>>> f.clean(['1']) 
     1125[u'1'] 
     1126>>> f.clean([1, 5]) 
     1127[u'1', u'5'] 
     1128>>> f.clean([1, '5']) 
     1129[u'1', u'5'] 
     1130>>> f.clean(['1', 5]) 
     1131[u'1', u'5'] 
     1132>>> f.clean(['1', '5']) 
     1133[u'1', u'5'] 
     1134>>> f.clean(['6']) 
     1135Traceback (most recent call last): 
     1136... 
     1137ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] 
     1138>>> f.clean(['1','6']) 
     1139Traceback (most recent call last): 
     1140... 
     1141ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] 
     1142 
     1143 
    11031144# ComboField ################################################################## 
    11041145 
     
    11661207Traceback (most recent call last): 
    11671208... 
    1168 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] 
     1209ValidationError: [u'Select a valid choice. fields.py is not one of the available choices.'] 
    11691210>>> fix_os_paths(f.clean(path + 'fields.py')) 
    11701211u'.../django/forms/fields.py' 
  • django/trunk/tests/regressiontests/forms/widgets.py

    r7971 r7977  
    459459</select> 
    460460 
     461Choices can be nested one level in order to create HTML optgroups: 
     462>>> w.choices=(('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) 
     463>>> print w.render('nestchoice', None) 
     464<select name="nestchoice"> 
     465<option value="outer1">Outer 1</option> 
     466<optgroup label="Group &quot;1&quot;"> 
     467<option value="inner1">Inner 1</option> 
     468<option value="inner2">Inner 2</option> 
     469</optgroup> 
     470</select> 
     471 
     472>>> print w.render('nestchoice', 'outer1') 
     473<select name="nestchoice"> 
     474<option value="outer1" selected="selected">Outer 1</option> 
     475<optgroup label="Group &quot;1&quot;"> 
     476<option value="inner1">Inner 1</option> 
     477<option value="inner2">Inner 2</option> 
     478</optgroup> 
     479</select> 
     480 
     481>>> print w.render('nestchoice', 'inner1') 
     482<select name="nestchoice"> 
     483<option value="outer1">Outer 1</option> 
     484<optgroup label="Group &quot;1&quot;"> 
     485<option value="inner1" selected="selected">Inner 1</option> 
     486<option value="inner2">Inner 2</option> 
     487</optgroup> 
     488</select> 
     489 
    461490# NullBooleanSelect Widget #################################################### 
    462491 
     
    626655>>> w._has_changed([1, 2], [u'1', u'3']) 
    627656True 
     657 
     658# Choices can be nested one level in order to create HTML optgroups: 
     659>>> w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) 
     660>>> print w.render('nestchoice', None) 
     661<select multiple="multiple" name="nestchoice"> 
     662<option value="outer1">Outer 1</option> 
     663<optgroup label="Group &quot;1&quot;"> 
     664<option value="inner1">Inner 1</option> 
     665<option value="inner2">Inner 2</option> 
     666</optgroup> 
     667</select> 
     668 
     669>>> print w.render('nestchoice', ['outer1']) 
     670<select multiple="multiple" name="nestchoice"> 
     671<option value="outer1" selected="selected">Outer 1</option> 
     672<optgroup label="Group &quot;1&quot;"> 
     673<option value="inner1">Inner 1</option> 
     674<option value="inner2">Inner 2</option> 
     675</optgroup> 
     676</select> 
     677 
     678>>> print w.render('nestchoice', ['inner1']) 
     679<select multiple="multiple" name="nestchoice"> 
     680<option value="outer1">Outer 1</option> 
     681<optgroup label="Group &quot;1&quot;"> 
     682<option value="inner1" selected="selected">Inner 1</option> 
     683<option value="inner2">Inner 2</option> 
     684</optgroup> 
     685</select> 
     686 
     687>>> print w.render('nestchoice', ['outer1', 'inner2']) 
     688<select multiple="multiple" name="nestchoice"> 
     689<option value="outer1" selected="selected">Outer 1</option> 
     690<optgroup label="Group &quot;1&quot;"> 
     691<option value="inner1">Inner 1</option> 
     692<option value="inner2" selected="selected">Inner 2</option> 
     693</optgroup> 
     694</select> 
    628695 
    629696# RadioSelect Widget ##########################################################