pythondjangowidget

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?

Two widgets with one grouped

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.


Solution

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