Support grouped choices

This commit is contained in:
Tom Christie 2015-08-06 11:43:03 +01:00
parent 95a1550388
commit 27ac5a3680
9 changed files with 118 additions and 39 deletions

View File

@ -5,7 +5,6 @@ import copy
import datetime import datetime
import decimal import decimal
import inspect import inspect
import itertools
import re import re
import uuid import uuid
@ -109,32 +108,54 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value dictionary[keys[-1]] = value
def flatten_choice(choice): def pairwise_choices(choices):
""" """
Convert a single choices choice into a flat list of choices. Convert any single choices into key/value pairs instead.
Returns a list of choices pairs. pairwise_choices([1]) -> [(1, 1)]
pairwise_choices([(1, '1st')]) -> [(1, '1st')]
flatten_choice(1) -> [(1, 1)] pairwise_choices([('Group', ((1, '1st'), 2))]) -> [(1, '1st'), (2, 2)]
flatten_choice((1, '1st')) -> [(1, '1st')]
flatten_choice(('Grp', ((1, '1st'), (2, '2nd')))) -> [(1, '1st'), (2, '2nd')]
""" """
# Allow single, paired or grouped choices style: # Allow single, paired or grouped choices style:
# choices = [1, 2, 3] # choices = [1, 2, 3]
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')] # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')] # choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
if (not isinstance(choice, (list, tuple))): ret = []
# single choice for choice in choices:
return [(choice, choice)] if (not isinstance(choice, (list, tuple))):
else: # single choice
key, display_value = choice item = (choice, choice)
if isinstance(display_value, (list, tuple)): ret.append(item)
# grouped choices
sub_choices = [flatten_choice(c) for c in display_value]
return list(itertools.chain(*sub_choices))
else: else:
# paired choice key, value = choice
return [(key, display_value)] if isinstance(value, (list, tuple)):
# grouped choices (category, sub choices)
item = (key, pairwise_choices(value))
ret.append(item)
else:
# paired choice (key, display value)
item = (key, value)
ret.append(item)
return ret
def flatten_choices(choices):
"""
Convert a group choices into a flat list of choices.
flatten_choices([(1, '1st')]) -> [(1, '1st')]
flatten_choices([('Grp', ((1, '1st'), (2, '2nd')))]) -> [(1, '1st'), (2, '2nd')]
"""
ret = []
for key, value in choices:
if isinstance(value, (list, tuple)):
# grouped choices (category, sub choices)
ret.extend(flatten_choices(value))
else:
# choice (key, display value)
item = (key, value)
ret.append(item)
return ret
class CreateOnlyDefault(object): class CreateOnlyDefault(object):
@ -1140,8 +1161,8 @@ class ChoiceField(Field):
} }
def __init__(self, choices, **kwargs): def __init__(self, choices, **kwargs):
flat_choices = [flatten_choice(c) for c in choices] self.grouped_choices = pairwise_choices(choices)
self.choices = OrderedDict(list(itertools.chain(*flat_choices))) self.choices = OrderedDict(flatten_choices(self.grouped_choices))
# Map the string representation of choices to the underlying value. # Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either # Allows us to deal with eg. integer choices while supporting either
@ -1168,6 +1189,30 @@ class ChoiceField(Field):
return value return value
return self.choice_strings_to_values.get(six.text_type(value), value) return self.choice_strings_to_values.get(six.text_type(value), value)
def iter_options(self):
class StartOptionGroup(object):
start_option_group = True
def __init__(self, label):
self.label = label
class EndOptionGroup(object):
end_option_group = True
class Option(object):
def __init__(self, value, display_text):
self.value = value
self.display_text = display_text
for key, value in self.grouped_choices:
if isinstance(value, (list, tuple)):
yield StartOptionGroup(label=key)
for sub_key, sub_value in value:
yield Option(value=sub_key, display_text=sub_value)
yield EndOptionGroup()
else:
yield Option(value=key, display_text=value)
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
default_error_messages = { default_error_messages = {

View File

@ -10,8 +10,14 @@
{% if field.allow_null or field.allow_blank %} {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option> <option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %} {% endif %}
{% for key, text in field.choices.items %} {% for select in field.iter_options %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> {% if select.start_option_group %}
<optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -10,8 +10,14 @@
<div class="col-sm-10"> <div class="col-sm-10">
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for select in field.iter_options %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> {% if select.start_option_group %}
<optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% empty %} {% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>
{% endfor %} {% endfor %}

View File

@ -9,9 +9,14 @@
{% if field.allow_null or field.allow_blank %} {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option> <option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %} {% endif %}
{% for select in field.iter_options %}
{% for key, text in field.choices.items %} {% if select.start_option_group %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> <optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -9,9 +9,15 @@
{% endif %} {% endif %}
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for select in field.iter_options %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> {% if select.start_option_group %}
{% empty %} <optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -9,9 +9,14 @@
{% if field.allow_null or field.allow_blank %} {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option> <option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %} {% endif %}
{% for select in field.iter_options %}
{% for key, text in field.choices.items %} {% if select.start_option_group %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> <optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -9,8 +9,14 @@
{% endif %} {% endif %}
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for select in field.iter_options %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> {% if select.start_option_group %}
<optgroup label="{{ select.label }}">
{% elif select.end_option_group %}
</optgroup>
{% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endif %}
{% empty %} {% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>
{% endfor %} {% endfor %}

View File

@ -107,10 +107,10 @@ def get_field_kwargs(field_name, model_field):
isinstance(model_field, models.TextField)): isinstance(model_field, models.TextField)):
kwargs['allow_blank'] = True kwargs['allow_blank'] = True
if model_field.flatchoices: if model_field.choices:
# If this model field contains choices, then return early. # If this model field contains choices, then return early.
# Further keyword arguments are not valid. # Further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices kwargs['choices'] = model_field.choices
return kwargs return kwargs
# Ensure that max_length is passed explicitly as a keyword arg, # Ensure that max_length is passed explicitly as a keyword arg,

View File

@ -181,7 +181,7 @@ class TestRegularFieldMappings(TestCase):
null_field = IntegerField(allow_null=True, required=False) null_field = IntegerField(allow_null=True, required=False)
default_field = IntegerField(required=False) default_field = IntegerField(required=False)
descriptive_field = IntegerField(help_text='Some help text', label='A label') descriptive_field = IntegerField(help_text='Some help text', label='A label')
choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')]) choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
""") """)
if six.PY2: if six.PY2:
# This particular case is too awkward to resolve fully across # This particular case is too awkward to resolve fully across