How to group the choices in a Django Select widget? How to group the choices in a Django Select widget? python python

How to group the choices in a Django Select widget?


After a quick look at the ModelChoiceField code in django.forms.models, I'd say try extending that class and override its choice property.

Set up the property to return a custom iterator, based on the orignial ModelChoiceIterator in the same module (which returns the tuple you're having trouble with) - a new GroupedModelChoiceIterator or some such.

I'm going to have to leave the figuring out of exactly how to write that iterator to you, but my guess is you just need to get it returning a tuple of tuples in a custom manner, instead of the default setup.

Happy to reply to comments, as I'm pretty sure this answer needs a little fine tuning :)

EDIT BELOW

Just had a thought and checked djangosnippets, turns out someone's done just this:ModelChoiceField with optiongroups. It's a year old, so it might need some tweaks to work with the latest django, but it's exactly what I was thinking.


Here's what worked for me, not extending any of the current django classes:

I have a list of types of organism, given the different Kingdoms as the optgroup. In a form OrganismForm, you can select the organism from a drop-down select box, and they are ordered by the optgroup of the Kingdom, and then all of the organisms from that kingdom. Like so:

  [----------------|V]  |Plantae         |  |  Angiosperm    |  |  Conifer       |  |Animalia        |  |  Mammal        |  |  Amphibian     |  |  Marsupial     |  |Fungi           |  |  Zygomycota    |  |  Ascomycota    |  |  Basidiomycota |  |  Deuteromycota |  |...             |  |________________|

models.py

from django.models import Modelclass Kingdom(Model):    name = models.CharField(max_length=16)class Organism(Model):    kingdom = models.ForeignKeyField(Kingdom)    name = models.CharField(max_length=64)

forms.py:

from models import Kingdom, Organismclass OrganismForm(forms.ModelForm):    organism = forms.ModelChoiceField(        queryset=Organism.objects.all().order_by('kingdom__name', 'name')    )    class Meta:        model = Organism

views.py:

from models import Organism, Kingdomfrom forms import OrganismFormform = OrganismForm()form.fields['organism'].choices = list()# Now loop the kingdoms, to get all organisms in each.for k in Kingdom.objects.all():    # Append the tuple of OptGroup Name, Organism.    form.fields['organism'].choices = form.fields['organism'].choices.append(        (            k.name, # First tuple part is the optgroup name/label            list( # Second tuple part is a list of tuples for each option.                (o.id, o.name) for o in Organism.objects.filter(kingdom=k).order_by('name')                # Each option itself is a tuple of id and name for the label.            )        )    )


You don't need custom iterators. You're gonna need to support that code. Just pass the right choices:

from django import formsfrom django.db.models import Prefetchclass ProductForm(forms.ModelForm):    class Meta:        model = Product        fields = [...]    def __init__(self, *args, **kwargs):        super(ProductForm, self).__init__(*args, **kwargs)        cats = Category.objects \            .filter(category__isnull=True) \            .order_by('order') \            .prefetch_related(Prefetch('subcategories',                queryset=Category.objects.order_by('order')))        self.fields['subcategory'].choices = \            [("", self.fields['subcategory'].empty_label)] \            + [(c.name, [                (self.fields['subcategory'].prepare_value(sc),                    self.fields['subcategory'].label_from_instance(sc))                for sc in c.subcategories.all()            ]) for c in cats]

Here,

class Category(models.Model):    category = models.ForeignKey('self', null=True, on_delete=models.CASCADE,        related_name='subcategories', related_query_name='subcategory')class Product(models.Model):    subcategory = models.ForeignKey(Category, on_delete=models.CASCADE,        related_name='products', related_query_name='product')

This same technique can be used to customize a Django admin form. Although, Meta class is not needed in this case.