[Django]-Django ModelChoiceField optgroup tag

80👍

You don’t need to create any custom field, Django already does the job, just pass the choices well formatted:

MEDIA_CHOICES = (
 ('Audio', (
   ('vinyl', 'Vinyl'),
   ('cd', 'CD'),
  )
 ),
 ('Video', (
   ('vhs', 'VHS Tape'),
   ('dvd', 'DVD'),
  )
 ),
)

7👍

ModelChoiceField uses a ModelChoiceIterator to convert the queryset to a list of choices.
You can easily override this class to introduce groups.
Here is an example that groups cities by country:

from itertools import groupby
from django.forms.models import ModelChoiceField, ModelChoiceIterator
from .models import City

class CityChoiceIterator(ModelChoiceIterator):
    def __iter__(self):
        queryset = self.queryset.select_related('country').order_by('country__name', 'name')
        groups = groupby(queryset, key=lambda x: x.country)
        for country, cities in groups:
            yield [
                country.name,
                [
                    (city.id, city.name)
                    for city in cities
                ]
            ]

class CityChoiceField(ModelChoiceField):
    iterator = CityChoiceIterator

    def __init__(self, *args, **kwargs):
        super().__init__(City.objects.all(), *args, **kwargs)

Note: I didn’t have time to check that this technique is compatible with the new ModelChoiceIteratorValue introduced in Django 3.1.

3👍

An extension of @Stefan Manastirliu answer to use with django-categories. (Downside is that get_tree_data() function below allows only one level) . In combination with javascript plugins like bootstrap multiselect you can get multi-select like this

Models.py

from categories.models import CategoryBase
class SampleCategory(CategoryBase):
    class Meta:
        verbose_name_plural = 'sample categories'

class SampleProfile(models.Model):
    categories = models.ManyToManyField('myapp.SampleCategory')

forms.py

from myapp.models import SampleCategory

    def get_tree_data():
        def rectree(toplevel):
            children_list_of_tuples = list()
            if toplevel.children.active():
                for child in toplevel.children.active():
                    children_list_of_tuples.append(tuple((child.id,child.name)))

            return children_list_of_tuples

        data = list()
        t = SampleCategory.objects.filter(active=True).filter(level=0)
        for toplevel in t:
            childrens = rectree(toplevel)
            data.append(
                tuple(
                    (
                        toplevel.name,
                        tuple(
                            childrens
                            )
                        ) 
                    )
            )
        return tuple(data)

class SampleProfileForm(forms.ModelForm):
    categories = forms.MultipleChoiceField(choices=get_tree_data())
    class Meta:
        model = SampleProfile

2👍

Here’s a good snippet:

Choice Field and Select Widget With Optional Optgroups:
http://djangosnippets.org/snippets/200/

0👍

In your form.py, you need to use a ChoiceField or a MultipleChoiceField
instead of a ModelChoiceField if you want to use the Django built-in "choices" that natively handles the optgroup:

models.py

class Config(models.Model):
    label = models.Charfield(verbose_name="Config",max_length=20,)

    def __str__(self):
        return self.label

class Link(models.Model):
    config = models.ForeignKey(Config)
    name = models.URLField(u'Name', null=True, max_length=50)
    gateway = models.IPAddressField(u'Gateway', null=True)
    weight = models.IntegerField(u'Weight', null=True)
    description = models.TextField(u'Description', blank=True)

    def __str__(self):
        return self.name

forms.py

class LinkForm(ModelForm):
    config = forms.ChoiceField(label="Config")

class Meta:
    model = Link

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.fields["config"].choices = [
        ["Configuration", [[c.id, c.label] for c in Config.objects.all()]]
    ]

Leave a comment