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.
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.
)
)
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With