Runtime ChoiceField filtering in Django’s admin

Django 1.x brought with it much finer grained control over the admin application with admin forms and inline form sets. However, I still keep running into the same problem that I have since I started using Django – you cannot provide a limited queryset for a select field that depends on other instance variables.

Take this trivial example:

from django.db import models
 
class Sport(object):
    name = models.CharField(max_length=50)
 
class Season(models.Model):
    starts = models.DateField()
    ends = models.DateField()
    sport = models.ForeignKey(Sport)
 
class Team(object):
    name = models.CharField(max_length=100)
    sport = models.ForeignKey(Sport)
 
class Game(object):
    season = models.ForeignKey(Season)
    home_team = models.ForeignKey(Team, related_name="home_games")
    away_team = modesl.ForeignKey(Team, related_name="away_games")

In the admin change form for Game, it is obviously desirable to only permit teams to be selected that match the Season‘s Sport. Unfortunately, because fields are defined on the class rather than the instance (such as inside of __init__), there is no obvious way to create a relationship based on the values in the instance.

Inside the ModelAdmin class is the method get_formset(self, request, obj=None, **kwargs). The parameter obj stores the current instance, if any. The significance of this is that this method is a hook with access to the instance data and is called for every form as it is built.

That makes it possible to filter the Teams based on the current form’s instance.

from django.contrib import admin
from django import forms
from myapp.models import Team, Game
 
def game_form_factory(sport):
    class RuntimeGameForm(forms.ModelForm):
        home_team = forms.ModelChoiceField(label="Home",
                queryset=Team.objects.filter(sport=sport))
        away_team = forms.ModelChoiceField(label="Away",
                queryset=Team.objects.filter(sport=sport))
 
        class Meta:
            model = Game
 
    return RuntimeGameForm
 
class GameAdmin(admin.modelAdmin):
    model = Game
 
    def get_formset(self, request, obj=None, **kwargs):
        if obj is not None:
            self.form = game_form_factory(obj.season.sport)
        return super(GameAdmin, self).get_formset(request, obj, **kwargs)

Here is how it works. When the GameAdmin form is built, get_formset is called. If this is an edit form (add form’s will not have instance data) the Game instance is passed as the obj parameter. In this case, the instance sets the form attribute to be the result of calling game_form_factory, which is a class factory function.

What if we want the Game form to be an inline form for the Season form? The major difference with inline form sets is that the instance passed to get_formset is now that of the parent form, rather than the form set model (in this case, Season instead of Game.)

The class factory function remains essentially unchanged. The Game admin model requires only a small change.

class GameAdminInline(admin.TabularInline):
    model = Game
 
    def get_formset(self, request, obj=None, **kwargs):
        if obj is not None:
            self.form = game_form_factory(obj.sport) # obj is a Season
        return super(GameAdminInline, self).get_formset(request, obj,
                **kwargs)

10 thoughts on “Runtime ChoiceField filtering in Django’s admin

  1. I did this last year…I think some guy on the mailing list gave me the code. I’m glad to see a blog post about this, because there was none at the time so it took me forever to find the solution. This is one of those things that I think a lot of people need at some point in Django development whenever the models get more complex like this and it’s not exactly obvious how to do it. Thanks!

  2. You might also do something like:

    class RunTimeGameForm(forms.ModelForm):
        [...]
        def __init__(*args, **kwargs):
            super(RunTimeGameForm, self).__init__(*args, **kwargs)
            if self.instance.id:
                self.fields['home_team'].queryset=Team.objects.filter(
                    sport=self.instance.season.sport)
    

    Then there’s no need to override ModelAdmin.get_formset, or have a game_form_factory function.

    • That’s a good point. How would that work with a form set, though? I didn’t see that the parent instance is passed to the form set’s constructor.

  3. Thanks very much for writing about this. I’m new to both python and django and there didn’t seem to be a way to handle this kind of problem without abandoning the Admin screens from the django doc.

  4. The only way I could think to handle a formset is to sublcass django.forms.models.BaseInlineFormSet, override the _construct_form method and pass in some an additional kwarg(the parent instance) that will then get passed to the form. So you would need to then override the __init__ method in the form and use the extra kwarg to change the queryset on the field. The pseudo code would be something like:


    class BilletAdminFormSet(BaseInlineFormSet):
    def _construct_form(self, i, **kwargs):
    kwargs.update({'instance': self.instance})
    return super(BilletAdminFormSet, self)._construct_form(i, **kwargs)

    class BilletAdminForm(CustomModelForm):
    ...
    def __init__(self, *args, **kwargs):
    super(BilletAdminForm, self).__init__(*args, **kwargs)
    if 'instance' in kwargs and kwargs['instance']:
    self.fields['employees'].queryset = SomeQuerySet()

    • Also you would need to assign the form and formset to the inline.

      admin.py

      class BilletInline(admin.StackedInline):
      model = Billet
      extra = 1
      form = BilletAdminForm
      formset = BilletAdminFormSet

  5. Thank you very much for posting this, it’s definitely the most clever approach i’ve found to tackle this issue. I can’t believe the django doc completely ignore this subject.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>