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