diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 37c902954..1d12b1d92 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -108,6 +108,53 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value
+def to_choices_dict(choices):
+ """
+ Convert choices into key/value dicts.
+
+ pairwise_choices([1]) -> {1: 1}
+ pairwise_choices([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'}
+ pairwise_choices([('Group', ((1, '1st'), 2))]) -> {'Group': {1: '1st', 2: '2nd'}}
+ """
+ # 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')]
+ ret = OrderedDict()
+ for choice in choices:
+ if (not isinstance(choice, (list, tuple))):
+ # single choice
+ ret[choice] = choice
+ else:
+ key, value = choice
+ if isinstance(value, (list, tuple)):
+ # grouped choices (category, sub choices)
+ ret[key] = to_choices_dict(value)
+ else:
+ # paired choice (key, display value)
+ ret[key] = value
+ return ret
+
+
+def flatten_choices_dict(choices):
+ """
+ Convert a group choices dict into a flat dict of choices.
+
+ flatten_choices({1: '1st', 2: '2nd'}) -> {1: '1st', 2: '2nd'}
+ flatten_choices({'Group': {1: '1st', 2: '2nd'}}) -> {1: '1st', 2: '2nd'}
+ """
+ ret = OrderedDict()
+ for key, value in choices.items():
+ if isinstance(value, dict):
+ # grouped choices (category, sub choices)
+ for sub_key, sub_value in value.items():
+ ret[sub_key] = sub_value
+ else:
+ # choice (key, display value)
+ ret[key] = value
+ return ret
+
+
class CreateOnlyDefault(object):
"""
This class may be used to provide default values that are only used
@@ -1111,17 +1158,8 @@ class ChoiceField(Field):
}
def __init__(self, choices, **kwargs):
- # Allow either single or paired choices style:
- # choices = [1, 2, 3]
- # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
- pairs = [
- isinstance(item, (list, tuple)) and len(item) == 2
- for item in choices
- ]
- if all(pairs):
- self.choices = OrderedDict([(key, display_value) for key, display_value in choices])
- else:
- self.choices = OrderedDict([(item, item) for item in choices])
+ self.grouped_choices = to_choices_dict(choices)
+ self.choices = flatten_choices_dict(self.grouped_choices)
# Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either
@@ -1148,6 +1186,38 @@ class ChoiceField(Field):
return value
return self.choice_strings_to_values.get(six.text_type(value), value)
+ def iter_options(self):
+ """
+ Helper method for use with templates rendering select widgets.
+ """
+ class StartOptionGroup(object):
+ start_option_group = True
+ end_option_group = False
+
+ def __init__(self, label):
+ self.label = label
+
+ class EndOptionGroup(object):
+ start_option_group = False
+ end_option_group = True
+
+ class Option(object):
+ start_option_group = False
+ end_option_group = False
+
+ def __init__(self, value, display_text):
+ self.value = value
+ self.display_text = display_text
+
+ for key, value in self.grouped_choices.items():
+ if isinstance(value, dict):
+ yield StartOptionGroup(label=key)
+ for sub_key, sub_value in value.items():
+ 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 = {
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
index 8957eef9f..4aa3db3de 100644
--- a/rest_framework/templates/rest_framework/horizontal/select.html
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -10,8 +10,14 @@
{% if field.allow_null or field.allow_blank %}
{% endif %}
- {% for key, text in field.choices.items %}
-
+ {% for select in field.iter_options %}
+ {% if select.start_option_group %}
+
+ {% else %}
+
+ {% endif %}
{% endfor %}
diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
index 407b356ff..b1ca2f143 100644
--- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html
+++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
@@ -10,8 +10,14 @@
diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html
index b9b8bb4ea..3bae43eb8 100644
--- a/rest_framework/templates/rest_framework/inline/select_multiple.html
+++ b/rest_framework/templates/rest_framework/inline/select_multiple.html
@@ -9,9 +9,15 @@
{% endif %}
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
index dc32d39dd..ce30022d8 100644
--- a/rest_framework/templates/rest_framework/vertical/select.html
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -9,9 +9,14 @@
{% if field.allow_null or field.allow_blank %}
{% endif %}
-
- {% for key, text in field.choices.items %}
-
+ {% for select in field.iter_options %}
+ {% if select.start_option_group %}
+
+ {% else %}
+
+ {% endif %}
{% endfor %}
diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html
index 2bb3d5ae4..cb65cec52 100644
--- a/rest_framework/templates/rest_framework/vertical/select_multiple.html
+++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html
@@ -9,8 +9,14 @@
{% endif %}