Dynamic choices on a ForeignKey field

Posted by: barbara | Date: Jul 23, 2008 | Updated: Sep 09, 2008 | Category: django    

One proviso before I get started - this example works with the old (as of a few days ago) version of admin, prior to the newforms-admin merge, where the models still use inner admin classes. I haven't started changing my current project to work with the merge, but as I do, I'll update with new examples.

Most field types take choices as an argument (actually, the Django documentation on choices says 'most', but the model page that describes field options says 'all').

A choices list should be a tuple of tuples (a 2-tuple?) - think of it as a list of key/value pairs. All of the official Django documentation shows choices examples as lists of static values **:

    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )

With the list name passed as an argument like so:

    gender = models.ForeignKey(MyModel, choices=GENDER_CHOICES)

It is possible to pass in a dynamic list, though, by using a method to generate the choices tuple.

When you create your model, start with a custom model manager at the top:

from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.sites.models import Site
from django.contrib.auth.models import User

class CategoryManager(models.Manager):

    def category_choices(self):
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("""
            SELECT c.id, c.title
            FROM category_users c
            WHERE c.active = '1'
            ORDER BY c.title ASC""")
        choice_list = cursor.fetchall()
        return choice_list

Note that the above manager class contains a method that will populate a choices tuple. There are probably shorter, faster, easier ways to do that, but this does work - I'm using plain SQL to return a set of rows with just two columns - an id and a value. In other words, a list of key/value pairs.

In the model that your choices values will come from, create relationship with the Manager model:

class Category(models.Model):
    user = models.ForeignKey(User)
    title = models.CharField(max_length=40)
    active = models.BooleanField()
    created_at = models.DateField(auto_now_add=True)
    updated_at = models.DateField(null=True, blank=True)
    inactivated_at = models.DateField(null=True, blank=True)

    objects = WhitepapersManager()

    class Admin:
        list_display = ('title',)

(Remember that the 'id' requested as 'c.id' in the query above is also a part of this model - it's created automatically as the primary key.)

Then in the model where you need the choices list, get the tuple by calling up to the model method that returns it:

class CategoryUsers(models.Model):

    CATEGORY_CHOICES = Category.objects.category_choices()

    category = models.ForeignKey(Category, choices=CATEGORY_CHOICES)

    class Admin:
        list_display = ('category',)

** In most of the posts and comments I've seen, it's recommended that you only use choices for static k/v pairs. So even though dynamic choices are possible, they're not recommended. And after testing out this method on my development environment today, I see why - every time I added new rows to the main Category table, I had to restart before the values would show up in my choices list.

Nice. I've been asking how to do this on StackOverflow, and couldn't get anyone who know.
Comment by Paul on Apr 13, 2010:
Thanks for this post, headache over.
Comment by tn on Aug 18, 2010:
Use can also do same thing without a sql query: CATEGORY_CHOICES = Category.objects.filter(active=True).order_by('title').values_list("id", "title").iterator() To make new categories show up in choices you can reassign Field._choices: CategoryUsers._meta.get_field_by_name("category")._choices = Category.objects.filter(active=True).order_by('title').values_list("id", "title").iterator()