Create django grouped select field for ModelChoiceField or ModelMultipleChoiceField
Django forms's builtin fields like ChoiceField, ModelChoiceField use by default forms.Select
widget, but what we do if we need a Select with grouped options.
I found this awesome snippets that's helped me to achieve this, just in my case I needed to add a line of code to show only options with parent.
Example
Here's a model example:
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', null=True, blank=True)
def __unicode__(self):
return u'{0}'.format(self.name)
I created a form that use this model:
class ExampleForm(forms.Form):
category = GroupedModelChoiceField(
label=_('Category'),
group_by_field='parent',
queryset=Category.objects.all(),
)
The snippet show categories with None label, I don't need to make user able to select Categories, only childs can be selected. So, I just edited the line 50 of the snippet to remove group categories.
from itertools import groupby
from django.forms.models import ModelChoiceIterator, ModelChoiceField
class GroupedModelChoiceField(ModelChoiceField):
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(GroupedModelChoiceField, 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):
"""
Exactly as per ModelChoiceField except returns new iterator class
"""
if hasattr(self, '_choices'):
return self._choices
return GroupedModelChoiceIterator(self)
choices = property(_get_choices, ModelChoiceField._set_choices)
class GroupedModelChoiceIterator(ModelChoiceIterator):
def __iter__(self):
if self.field.empty_label is not None:
yield (u"", self.field.empty_label)
if self.field.cache_choices:
if self.field.choice_cache is None:
self.field.choice_cache = [
(self.field.group_label(group), [
self.choice(ch) for ch in choices])
for group, choices in groupby(
self.queryset.all(),
key=lambda row: getattr(row, self.field.group_by_field))
]
for choice in self.field.choice_cache:
yield choice
else:
for group, choices in groupby(
self.queryset.all(),
key=lambda row: getattr(
row, self.field.group_by_field)):
if group is not None: #Line added
yield (
self.field.group_label(group),
[self.choice(ch) for ch in choices])
Okay, now this custom field work, but what if I need a grouped select multiple with ModelMultipleChoiceField
?
We can just add a new field like GroupedModelChoiceField
that inherits from ModelMultipleChoiceField
from django.forms.models import ModelMultipleChoiceField
class GroupedMultipleModelChoiceField(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(GroupedMultipleModelChoiceField, 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):
"""
Exactly as per ModelChoiceField except returns new iterator class
"""
if hasattr(self, '_choices'):
return self._choices
return GroupedModelChoiceIterator(self)
choices = property(_get_choices, ModelMultipleChoiceField._set_choices)