Limit foreign key choices in select in an inline form in admin

Used request instance as temporary container for obj. Overrided Inline method formfield_for_foreignkey to modify queryset. This works at least on django 1.2.3.

class RoomInline(admin.TabularInline):

    model = Room

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(RoomInline, self).formfield_for_foreignkey(db_field, request, **kwargs)

        if db_field.name == 'inside_room':
            if request._obj_ is not None:
                field.queryset = field.queryset.filter(building__exact = request._obj_)  
            else:
                field.queryset = field.queryset.none()

        return field



class BuildingAdmin(admin.ModelAdmin):

    inlines = (RoomInline,)

    def get_form(self, request, obj=None, **kwargs):
        # just save obj reference for future processing in Inline
        request._obj_ = obj
        return super(BuildingAdmin, self).get_form(request, obj, **kwargs)

There is limit_choices_to ForeignKey option that allows to limit the available admin choices for the object


After reading through this post and experimenting a lot I think I have found a rather definitive answer to this question. As this is a design pattern that is ofter used I have written a Mixin for the Django admin to make use of it.

(Dynamically) limiting the queryset for ForeignKey fields is now as simple as subclassing LimitedAdminMixin and defining a get_filters(obj) method to return the relevant filters. Alternateively, a filters property can be set on the admin if dynamic filtering is not required.

Example usage:

class MyInline(LimitedAdminInlineMixin, admin.TabularInline):
    def get_filters(self, obj):
        return (('<field_name>', dict(<filters>)),)

Here, <field_name> is the name of the FK field to be filtered and <filters> is a list of parameters as you would normally specify them in the filter() method of querysets.


You can create a couple of custom classes that will then pass along a reference to the parent instance to the form.

from django.forms.models import BaseInlineFormSet
from django.forms import ModelForm

class ParentInstInlineFormSet(BaseInlineFormSet):
    def _construct_forms(self):
        # instantiate all the forms and put them in self.forms
        self.forms = []
        for i in xrange(self.total_form_count()):
            self.forms.append(self._construct_form(i, parent_instance=self.instance))

    def _get_empty_form(self, **kwargs):
        return super(ParentInstInlineFormSet, self)._get_empty_form(parent_instance=self.instance)
    empty_form = property(_get_empty_form)


class ParentInlineModelForm(ModelForm):
    def __init__(self, *args, **kwargs):
        self.parent_instance = kwargs.pop('parent_instance', None)
        super(ParentInlineModelForm, self).__init__(*args, **kwargs)

in class RoomInline just add:

class RoomInline(admin.TabularInline):
      formset = ParentInstInlineFormset
      form = RoomInlineForm #(or something)

In your form you now have access in the init method to self.parent_instance! parent_instance can now be used to filter choices and whatnot

something like:

class RoomInlineForm(ParentInlineModelForm):
    def __init__(self, *args, **kwargs):
        super(RoomInlineForm, self).__init__(*args, **kwargs)
        building = self.parent_instance
        #Filtering and stuff

The problem in @nogus answer there's still wrong url in popup /?_to_field=id&_popup=1

which allow user to select wrong item in popup

To finally make it work I had to change field.widget.rel.limit_choices_to dict

class RoomInline(admin.TabularInline):
    model = Room

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(RoomInline, self).formfield_for_foreignkey(
            db_field, request, **kwargs)

        if db_field.name == 'inside_room':
            building = request._obj_
            if building is not None:
                field.queryset = field.queryset.filter(
                    building__exact=building)
                # widget changed to filter by building
                field.widget.rel.limit_choices_to = {'building_id': building.id}
            else:
                field.queryset = field.queryset.none()

        return field

class BuildingAdmin(admin.ModelAdmin):

    inlines = (RoomInline,)

    def get_form(self, request, obj=None, **kwargs):
        # just save obj reference for future processing in Inline
        request._obj_ = obj
        return super(BuildingAdmin, self).get_form(request, obj, **kwargs)