¿Cómo agrupar las opciones en un widget Django Select?

¿Es posible crear grupos de opciones con nombre en un widget de selección (desplegable) de Django, cuando ese widget se encuentra en un formulario que se genera automáticamente a partir de un modelo de datos? ¿Puedo crear el widget en la imagen de la izquierda a continuación?

Dos widgets con uno agrupado.

Mi primer experimento en la creación de un formulario con grupos con nombre, se realizó de forma manual , como este

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 

Y funcionó muy bien, creando el widget desplegable a la izquierda, con grupos con nombre.

Luego creé un modelo de datos que tenía básicamente la misma estructura y usé la capacidad de Django para generar automáticamente formularios a partir de modelos. Funcionó, en el sentido de que mostraba todas las opciones. Pero las opciones no estaban en grupos con nombre, y hasta ahora, no he descubierto cómo hacerlo, si es posible.

He encontrado varias preguntas, donde la respuesta fue “crear un constructor de formularios y hacer cualquier procesamiento especial allí”. Pero parece que las formas.ChoiceField requiere una tupla para grupos nombrados, y no estoy seguro de cómo convertir una tupla en un QuerySet (lo que probablemente sea imposible de todos modos, si entiendo QuerySets correctamente como puntero a los datos, no el Información actual).

El código que utilicé para el modelo de datos es:

 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 

Cambiaré el modelo de datos, si es necesario, pero esto parece una estructura sencilla. Tal vez demasiados ForeignKeys? ¿Contraer los datos y aceptar la desnormalización? 🙂 ¿O hay alguna manera de convertir una tupla en un QuerySet, o algo aceptable para un ModelChoiceField?

Actualización: código final, basado en la respuesta de meshantz :

 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 

(Por supuesto, en el código real, haré que algo extraiga los datos del elemento de la base de datos ).

El cambio más grande del djangosnippet que vinculó, parece ser que Django ha incorporado parte del código, lo que hace posible asignar directamente un iterador a las opciones , en lugar de tener que anular toda la clase. Que es muy bonito

Después de echar un vistazo rápido al código ModelChoiceField en django.forms.models, diría que intenta extender esa clase y anular su propiedad de elección.

Configure la propiedad para devolver un iterador personalizado, basado en el original ModelChoiceIterator en el mismo módulo (que devuelve la tupla con la que está teniendo problemas): un nuevo GroupedModelChoiceIterator o algo similar.

Voy a tener que dejar de descifrar exactamente cómo escribirle ese iterador, pero supongo que solo necesita que le devuelva una tupla de tuplas de forma personalizada, en lugar de la configuración predeterminada.

Estoy feliz de responder a los comentarios, ya que estoy bastante seguro de que esta respuesta necesita un poco de ajuste fino 🙂

EDITAR ABAJO

Acabo de tener un pensamiento y comprobado djangosnippets, resulta que alguien acaba de hacer esto: ModelChoiceField con grupos de opciones . Tiene un año, por lo que podría necesitar algunos ajustes para trabajar con el último django, pero es exactamente lo que estaba pensando.

Esto es lo que funcionó para mí, sin extender ninguna de las clases actuales de django:

Tengo una lista de tipos de organismos, dados los diferentes Reinos como el grupo opt. En una forma OrganismForm, puede seleccionar el organismo de un cuadro de selección desplegable, y están ordenados por el grupo opcional del Reino, y luego todos los organismos de ese reino. Al igual que:

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

modelos.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. ) ) ) 

No necesitas iteradores personalizados . Vas a necesitar apoyar ese código . Simplemente pasa las elecciones correctas :

 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] 

Aquí,

 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') 

Esta misma técnica se puede utilizar para personalizar un formulario de administración de Django . Aunque, la clase Meta no es necesaria en este caso.