Skip to main content

Command Palette

Search for a command to run...

Create django grouped select field for ModelChoiceField or ModelMultipleChoiceField

Updated
2 min read
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)

More from this blog

Mounir's blog

18 posts

Software Developer/Architect Mostly working with Django/Python