Django tip: Showing <optgroup>s in a ModelForm

For my first Django tip on this brand new blog, I will provide some insight into my most recent head-scratcher: How do you get a ModelForm to display something like this…

…when you have your models laid out like this:

class Category(models.Model):
    name = models.CharField(max_length=50)  # Auto, Beauty, etc.

class SubCategory(models.Model):
    category = models.ForeignKey(Category)
    name = models.CharField(max_length=50)  # Parts, Fragrances, etc.

class Merchant(models.Model):
    sub_categories = models.ManyToManyField(SubCategory, verbose_name='Areas your business deals in')

On Dealit, the SubCategory model is used by many other models in a ManyToMany relationship, for example, when a merchant signs up (as I’m showing in the Merchant model above). But building out a ModelForm for Merchant will only display SubCategories without the related Category in an <optgroup>.

In order to fix that, we do this in our form…

class MerchantForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(MerchantForm, self).__init__(*args, **kwargs)
        self.fields['sub_categories'].choices = categories_as_choices()

def categories_as_choices():
    categories = []
    for category in Category.objects.all():
        new_category = []
        sub_categories = []
        for sub_category in category.subcategory_set.all():
            sub_categories.append([sub_category.id, sub_category.name])

        new_category = [category.name, sub_categories]
        categories.append(new_category)

    return categories

Here, we create a lot of nested lists with categories_as_choices() and tell Django to override the default list in the form. This is how Django wants it if you want it displaying <optgroup>s. Here’s an example from the documentation:

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

This is exactly what we end up creating with categories_as_choices(), except we create lists instead of tuples. Both are acceptable.

Hope that helps.

Advertisements

19 responses to “Django tip: Showing <optgroup>s in a ModelForm

  1. nice one. didn’t know about this. thx for sharing

  2. Victor Cinaglia

    Man, this is sweet. Thanks for sharing it.

  3. oh boy was that great!
    now onto some js 😉

  4. Very useful tip!
    Thanks!

  5. Pingback: Django Forms and ModelChoiceField: Add HTML optgroup element to specific objects in QuerySet? « Django Blog - Django – The Web Framework for perfectionists with deadlines

  6. Great solution, thanks!

  7. and how do you save it ?
    the usal way ? form.save() ?

  8. thanks a lot, it was really useful

  9. Pingback: Python:Use <optgroup> with form.fields.queryset? – IT Sprite

  10. very new to django, and trying to implement this into the default admin page…. not sure where and how to place the form code, should it simply go into admin.py where i have
    from django.contrib import admin
    from ahmia.models import Website, Category, SubCategory

    admin.site.register(Category)
    admin.site.register(SubCategory)
    admin.site.register(Website)

    • nevermind, Good Sir,
      figured it out, below works for me
      in models:
      class Category(models.Model):
      “””website Category”””
      name = models.CharField(max_length=50)
      def __unicode__(self):
      return self.name

      class SubCategory(models.Model):
      “””website Sub Category”””
      category = models.ForeignKey(Category)
      name = models.CharField(max_length=50)
      def __unicode__(self):
      return self.name

      sub_categories = models.ManyToManyField(SubCategory, default=1)

      and then in admin.py

      from django.contrib import admin
      from rango.models import Website, Category, SubCategory
      from django.forms import ModelForm

      class WebsiteForm(ModelForm):
      def __init__(self, *args, **kwargs):
      super(WebsiteForm, self).__init__(*args, **kwargs)
      self.fields[‘sub_categories’].choices = categories_as_choices()

      def categories_as_choices():
      categories = []
      for category in Category.objects.all():
      new_category = []
      sub_categories = []
      for sub_category in category.subcategory_set.all():
      sub_categories.append([sub_category.id, sub_category.name])

      new_category = [category.name, sub_categories]
      categories.append(new_category)

      return categories

      class WebsiteAdmin(admin.ModelAdmin):
      form = WebsiteForm

      admin.site.register(Website, WebsiteAdmin)
      admin.site.register(Category)
      admin.site.register(SubCategory)

  11. uhm.. back again 😉
    how would you put this in a template?
    might be using this as a wrong example, but since it worked perfectly in the admin interface for me.
    when i have a regular template and use something like this

    ———
    {% for category in categories %}
    {{ category }}
    {% endfor %}

    the result i get is a dopdown with rows containing values like this
    [u’communications’, [[29, u’press’], [30, u’chat’], [31, u’email’], [32, u’communications-other’]]]

    which is exactly the values i need, however not displayed like that

    thnx

  12. sorry, formatting above seems to be messed up

    ———
    {% for category in categories %}
    {{ category }}
    {% endfor %}

  13. apologies for the mess, can’t seem to paste it here

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s