How to use Grouped Model Choice Field in Django?

Published On: 01/09/2022 | Category: Django


Hi Dev,

Are you looking for example of how to use grouped model choice field in django. you can understand a concept of how to group the choices in a django select widget. Here you will learn django get choices from model. This post will give you simple example of django model choices from another model. Let's get started with how to get choice field value in django.

The Django provides forms API have two field types to work with multiple options: ChoiceField and ModelChoiceField.

So, both use select input as the default widget and they work in a similar way, except that ModelChoiceField is designed to handle and use QuerySets and work with foreign key relationships.

Here i explained simply step by step example of how to use grouped model choice field in django.

Example 1: Using ChoiceField

First of all here we go basic implementation using a ChoiceField would be:

forms.py
from django import forms

class ChoicesForm(forms.Form):
    CHOICES = (
        (1, 'Django'),
        (2, 'Python'),
        (3, 'PHP'),
        (4, 'JAVA'),
        (5, 'Laravel'),
        (6, 'Javascript'),
    )
    language = forms.CharField(max_length=100,
                                widget=forms.TextInput(attrs={'placeholder': 'Enter Language',
                                'class': 'form-control',
                            }))

    category = forms.ChoiceField(choices=CHOICES,
                                widget=forms.Select(attrs={'class': 'form-control',
                                }))
Preview

Example 2: Using Grouped Choice Field

In this second example you can also organize the choices in groups to generate the html <optgroup> tags looks like this:

forms.py
from django import forms

class ChoicesForm(forms.Form):
    CHOICES = (
        ('Gujarat', (
            (1, 'Rajkot'),
            (2, 'Ahmedabad'),
            (3, 'Surat'),
        )),
        ('Maharashtra', (
            (4, 'Mumbai'),
            (5, 'Pune'),
        )),
        ('Uttar Pradesh', (
            (6, 'Lucknow'),
            (7, 'Agra'),
        )),
    )
    state = forms.CharField(max_length=100,
                                widget=forms.TextInput(attrs={'placeholder': 'Enter State    Name',
                                'class': 'form-control',
                            }))

    city = forms.ChoiceField(choices=CHOICES,
                                widget=forms.Select(attrs={'class': 'form-control',
                            }))
Preview

Example 3: Grouped Model Choice Field

In this last example When you are using a ModelChoiceField unfortunately there is no built-in solution so i have add opt_group in ModelChoiceField.

If you we create a ModelForm using this model, the result will be very similar to our first example.

So in this below now our category instead of being a regular choices field it is now a model and the Expense model have a relationship with it using a foreign key.

models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=30)
    parent = models.ForeignKey('Category', on_delete=models.CASCADE, null=True)

    def __str__(self):
        return self.name

class Expense(models.Model):
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    date = models.DateField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    def __str__(self):
        return self.amount

To simulate a grouped categories you will need the code below. First create a new module named fields.py:

fields.py
from functools import partial
from itertools import groupby
from operator import attrgetter

from django.forms.models import ModelChoiceIterator, ModelChoiceField


class GroupedModelChoiceIterator(ModelChoiceIterator):
    def __init__(self, field, groupby):
        self.groupby = groupby
        super().__init__(field)

    def __iter__(self):
        if self.field.empty_label is not None:
            yield ("", self.field.empty_label)
        queryset = self.queryset
        # Can't use iterator() when queryset uses prefetch_related()
        if not queryset._prefetch_related_lookups:
            queryset = queryset.iterator()
        for group, objs in groupby(queryset, self.groupby):
            yield (group, [self.choice(obj) for obj in objs])


class GroupedModelChoiceField(ModelChoiceField):
    def __init__(self, *args, choices_groupby, **kwargs):
        if isinstance(choices_groupby, str):
            choices_groupby = attrgetter(choices_groupby)
        elif not callable(choices_groupby):
            raise TypeError('choices_groupby must either be a str or a callable accepting a single argument')
        self.iterator = partial(GroupedModelChoiceIterator, groupby=choices_groupby)
        super().__init__(*args, **kwargs)

And here is how you use it in your forms:

forms.py
from django import forms
from .fields import GroupedModelChoiceField
from .models import Category, Expense

class ExpenseForm(forms.ModelForm):
    category = GroupedModelChoiceField(
        queryset=Category.objects.exclude(parent=None), 
        choices_groupby='parent'
    )

    class Meta:
        model = Expense
        fields = ('amount', 'date', 'category')

In above example i used a self-referencing relationship I had to add the exclude(parent=None) to hide the “group categories” from showing up in the select input as a valid option.

I hope it will help you....