How to group the choices in a Django Select widget?

Is it possible to created named choice groups in a Django select (dropdown) widget, when that widget is on a form that is auto-generated from a data Model? Can I create the widget on the left-side picture below?

My first experiment in creating a form with named groups, was done manually, like this:

class GroupMenuOrderForm(forms.Form):
    food_list = [(1, 'burger'), (2, 'pizza'), (3, 'taco'),]
        drink_list = [(4, 'coke'), (5, 'pepsi'), (6, 'root beer'),]
        item_list = ( ('food', tuple(food_list)), ('drinks', tuple(drink_list)),)
        itemsField = forms.ChoiceField(choices = tuple(item_list))

    def GroupMenuOrder(request):
        theForm = GroupMenuOrderForm()
        return render_to_response(menu_template, {'form': theForm,})
        # generates the widget in left-side picture

And it worked nicely, creating the dropdown widget on the left, with named groups.

I then created a data Model that had basically the same structure, and used Django's ability to auto-generate forms from Models. It worked - in the sense that it showed all of the options. But the options were not in named groups, and so far, I haven't figured out how to do so - if it's even possible.

I have found several questions, where the answer was, “create a form constructor and do any special processing there”. But It seems like the forms.ChoiceField requires a tuple for named groups, and I’m not sure how to convert a tuple to a QuerySet (which is probably impossible anyway, if I understand QuerySets correctly as being pointer to the data, not the actual data).

The code I used for the data Model is:

class ItemDesc(models.Model):
    ''' one of "food", "drink", where ID of “food” = 1, “drink” = 2 '''
    desc = models.CharField(max_length=10, unique=True)
    def __unicode__(self):
        return self.desc

class MenuItem(models.Model):
    ''' one of ("burger", 1), ("pizza", 1), ("taco", 1),
        ("coke", 2), ("pepsi", 2), ("root beer", 2) '''
    name = models.CharField(max_length=50, unique=True)
    itemDesc = models.ForeignKey(ItemDesc)
    def __unicode__(self):
        return self.name

class PatronOrder(models.Model):
    itemWanted = models.ForeignKey(MenuItem)

class ListMenuOrderForm(forms.ModelForm):
    class Meta:
        model = PatronOrder

def ListMenuOrder(request):
    theForm = ListMenuOrderForm()
    return render_to_response(menu_template, {'form': theForm,})
    # generates the widget in right-side picture

I'll change the data model, if need be, but this seemed like a straightforward structure. Maybe too many ForeignKeys? Collapse the data and accept denormalization? :) Or is there some way to convert a tuple to a QuerySet, or something acceptable to a ModelChoiceField?

Update: final code, based on meshantz' answer:

class FooIterator(forms.models.ModelChoiceIterator):
    def __init__(self, *args, **kwargs):
        super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
    def __iter__(self):
            yield ('food', [(1L, u'burger'), (2L, u'pizza')])
            yield ('drinks', [(3L, u'coke'), (4L, u'pepsi')])

class ListMenuOrderForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ListMenuOrderForm, self).__init__(*args, **kwargs)
        self.fields['itemWanted'].choices = FooIterator()
    class Meta:
        model = PatronOrder

(Of course the actual code, I'll have something pull the item data from the database.)

The biggest change from the djangosnippet he linked, appears to be that Django has incorporated some of the code, making it possible to directly assign an Iterator to choices, rather than having to override the entire class. Which is very nice.

Answers


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 Model

class 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, Organism

class 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, Kingdom
from forms import OrganismForm
form = 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 forms
from django.db.models import Prefetch

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


Need Your Help

Python PostgreSQL modules. Which is best?

python postgresql module

I've seen a number of postgresql modules for python like pygresql, pypgsql, psyco. Most of them are Python DB API 2.0 compliant, some are not being actively developed anymore.

Heroku + Node: Cannot find module error

javascript git node.js heroku sequelize.js

My Node app is running fine locally, but has run into an error when deploying to Heroku. The app uses Sequelize in a /models folder, which contains index.js, Company.js and Users.js. Locally, I am ...