Dynamic choices on a ForeignKey field

Jul 23, 2008 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.