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 decimal
import inspect
import itertools
import re
import uuid
@ -109,32 +108,54 @@ def set_value(dictionary, keys, 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.
flatten_choice(1) -> [(1, 1)]
flatten_choice((1, '1st')) -> [(1, '1st')]
flatten_choice(('Grp', ((1, '1st'), (2, '2nd')))) -> [(1, '1st'), (2, '2nd')]
pairwise_choices([1]) -> [(1, 1)]
pairwise_choices([(1, '1st')]) -> [(1, '1st')]
pairwise_choices([('Group', ((1, '1st'), 2))]) -> [(1, '1st'), (2, 2)]
"""
# Allow single, paired or grouped choices style:
# choices = [1, 2, 3]
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
if (not isinstance(choice, (list, tuple))):
# single choice
return [(choice, choice)]
else:
key, display_value = choice
if isinstance(display_value, (list, tuple)):
# grouped choices
sub_choices = [flatten_choice(c) for c in display_value]
return list(itertools.chain(*sub_choices))
ret = []
for choice in choices:
if (not isinstance(choice, (list, tuple))):
# single choice
item = (choice, choice)
ret.append(item)
else:
# paired choice
return [(key, display_value)]
key, value = choice
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):
@ -1140,8 +1161,8 @@ class ChoiceField(Field):
}
def __init__(self, choices, **kwargs):
flat_choices = [flatten_choice(c) for c in choices]
self.choices = OrderedDict(list(itertools.chain(*flat_choices)))
self.grouped_choices = pairwise_choices(choices)
self.choices = OrderedDict(flatten_choices(self.grouped_choices))
# Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either
@ -1168,6 +1189,30 @@ class ChoiceField(Field):
return 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):
default_error_messages = {

View File

@ -10,8 +10,14 @@
{% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% for select in field.iter_options %}
{% 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 %}
</select>

View File

@ -10,8 +10,14 @@
<div class="col-sm-10">
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% for select in field.iter_options %}
{% 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 %}
<option>{{ no_items }}</option>
{% endfor %}

View File

@ -9,9 +9,14 @@
{% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% for select in field.iter_options %}
{% 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 %}
</select>
</div>

View File

@ -9,9 +9,15 @@
{% endif %}
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% empty %}
{% for select in field.iter_options %}
{% 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 %}
<option>{{ no_items }}</option>
{% endfor %}
</select>

View File

@ -9,9 +9,14 @@
{% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% for select in field.iter_options %}
{% 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 %}
</select>

View File

@ -9,8 +9,14 @@
{% endif %}
<select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% for select in field.iter_options %}
{% 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 %}
<option>{{ no_items }}</option>
{% endfor %}

View File

@ -107,10 +107,10 @@ def get_field_kwargs(field_name, model_field):
isinstance(model_field, models.TextField)):
kwargs['allow_blank'] = True
if model_field.flatchoices:
if model_field.choices:
# If this model field contains choices, then return early.
# Further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices
kwargs['choices'] = model_field.choices
return kwargs
# 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)
default_field = IntegerField(required=False)
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:
# This particular case is too awkward to resolve fully across