Here's a solution for current versions of Django (~2.1).
## forms.py
from itertools import groupby
from django import forms
from django.forms.models import ModelChoiceIterator, ModelMultipleChoiceField
from .models import Feature, Widget
class GroupedModelMultipleChoiceField(ModelMultipleChoiceField):
def __init__(self, group_by_field, group_label=None, *args, **kwargs):
"""
``group_by_field`` is the name of a field on the model
``group_label`` is a function to return a label for each choice group
"""
super(GroupedModelMultipleChoiceField, self).__init__(*args, **kwargs)
self.group_by_field = group_by_field
if group_label is None:
self.group_label = lambda group: group
else:
self.group_label = group_label
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return GroupedModelChoiceIterator(self)
choices = property(_get_choices, ModelMultipleChoiceField._set_choices)
class GroupedModelChoiceIterator(ModelChoiceIterator):
def __iter__(self):
"""Now yields grouped choices."""
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
for group, choices in groupby(
self.queryset.all(),
lambda row: getattr(row, self.field.group_by_field)):
if group is None:
for ch in choices:
yield self.choice(ch)
else:
yield (
self.field.group_label(group),
[self.choice(ch) for ch in choices])
class WidgetForm(forms.ModelForm):
class Meta:
model = Widget
fields = ['features',]
def __init__(self, *args, **kwargs):
super(WidgetForm, self).__init__(*args, **kwargs)
self.fields['features'] = GroupedModelMultipleChoiceField(
group_by_field='category',
queryset=Feature.objects.all(),
widget=forms.CheckboxSelectMultiple(),
required=False)
Then you can use {{ form.as_p }}
in the template for properly grouped choices.
If you would like to use the regroup
template tag and iterate over the choices, you will also need to reference the following custom widget:
class GroupedCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
def optgroups(self, name, value, attrs=None):
"""
The group name is passed as an argument to the ``create_option`` method (below).
"""
groups = []
has_selected = False
for index, (option_value, option_label) in enumerate(self.choices):
if option_value is None:
option_value = ''
subgroup = []
if isinstance(option_label, (list, tuple)):
group_name = option_value
subindex = 0
choices = option_label
else:
group_name = None
subindex = None
choices = [(option_value, option_label)]
groups.append((group_name, subgroup, index))
for subvalue, sublabel in choices:
selected = (
str(subvalue) in value and
(not has_selected or self.allow_multiple_selected)
)
has_selected |= selected
subgroup.append(self.create_option(
name, subvalue, sublabel, selected, index,
subindex=subindex, attrs=attrs, group=group_name,
))
if subindex is not None:
subindex += 1
return groups
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None, group=None):
"""
Added a ``group`` argument which is included in the returned dictionary.
"""
index = str(index) if subindex is None else "%s_%s" % (index, subindex)
if attrs is None:
attrs = {}
option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
if selected:
option_attrs.update(self.checked_attribute)
if 'id' in option_attrs:
option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
return {
'name': name,
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
'wrap_label': True,
'group': group,
}
class WidgetForm(forms.ModelForm):
class Meta:
model = Widget
fields = ['features',]
def __init__(self, *args, **kwargs):
super(WidgetForm, self).__init__(*args, **kwargs)
self.fields['features'] = GroupedModelMultipleChoiceField(
group_by_field='category',
queryset=Feature.objects.all(),
widget=GroupedCheckboxSelectMultiple(),
required=False)
Then the following should work in your template:
{% regroup form.features by data.group as feature_list %}
{% for group in feature_list %}
<h6>{{ group.grouper|default:"Other Features" }}</h6>
<ul>
{% for choice in group.list %}
<li>{{ choice }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
Credit to the following page for part of the solution:
https://mounirmesselmeni.github.io/2013/11/25/django-grouped-select-field-for-modelchoicefield-or-modelmultiplechoicefield/