Create django grouped select field for ModelChoiceField or ModelMultipleChoiceField

Search for a command to run...

No comments yet. Be the first to comment.
Introduction Django-Syzygy is a Django companion that makes database migrations safer in real production environments. It focuses on one specific, painful problem: migrations that are technically valid, but unsafe during rolling deployments. Syzygy w...

Master the core building blocks of Kafka, including partitions, consumer groups, and how to handle tricky topic migrations.

Avoiding Downtime in Django Migrations with django-migration-linter Deploying Django apps with zero downtime is essential in modern production systems, especially when rolling out updates on Kubernetes or deploying worker-based systems incrementally....

Building beautiful, responsive forms in Django doesn’t have to be a painful experience. By combining the power of django-tailwind with django-widget-tweaks and a few well-crafted custom CSS classes, you can create stunning, consistent forms that look...

What is HTMX? HTMX is a powerful tool that allows developers to create highly interactive and dynamic user interfaces without the need for heavy JavaScript frameworks. At its core, HTMX uses HTML attributes to define behaviour, making it an ideal com...

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.
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)