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

41,610

Solution 1

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)

Solution 2

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

Solution 3

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.

Solution 4

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

Solution 5

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)
Share:
41,610

Related videos on Youtube

mightyhal
Author by

mightyhal

Updated on July 05, 2022

Comments

  • mightyhal
    mightyhal almost 2 years

    The logic is of the model is:

    • A Building has many Rooms
    • A Room may be inside another Room (a closet, for instance--ForeignKey on 'self')
    • A Room can only be inside another Room in the same building (this is the tricky part)

    Here's the code I have:

    #spaces/models.py
    from django.db import models    
    
    class Building(models.Model):
        name=models.CharField(max_length=32)
        def __unicode__(self):
            return self.name
    
    class Room(models.Model):
        number=models.CharField(max_length=8)
        building=models.ForeignKey(Building)
        inside_room=models.ForeignKey('self',blank=True,null=True)
        def __unicode__(self):
            return self.number
    

    and:

    #spaces/admin.py
    from ex.spaces.models import Building, Room
    from django.contrib import admin
    
    class RoomAdmin(admin.ModelAdmin):
        pass
    
    class RoomInline(admin.TabularInline):
        model = Room
        extra = 2
    
    class BuildingAdmin(admin.ModelAdmin):
        inlines=[RoomInline]
    
    admin.site.register(Building, BuildingAdmin)
    admin.site.register(Room)
    

    The inline will display only rooms in the current building (which is what I want). The problem, though, is that for the inside_room drop down, it displays all of the rooms in the Rooms table (including those in other buildings).

    In the inline of rooms, I need to limit the inside_room choices to only rooms which are in the current building (the building record currently being altered by the main BuildingAdmin form).

    I can't figure out a way to do it with either a limit_choices_to in the model, nor can I figure out how exactly to override the admin's inline formset properly (I feel like I should be somehow create a custom inline form, pass the building_id of the main form to the custom inline, then limit the queryset for the field's choices based on that--but I just can't wrap my head around how to do it).

    Maybe this is too complex for the admin site, but it seems like something that would be generally useful...

  • Peter G
    Peter G about 12 years
    This right here saved me a lot of hassle. I needed to filter choices, but by a session variable. This answer let me do it with 5 lines of code. Thank you.
  • Justin
    Justin about 12 years
    Thank you for this! Its the first version that worked for my application and its nice and clear too.
  • powlo
    powlo almost 12 years
    Thanks a million! An alternative is to assign kwargs['queryset'] before calling super as per docs: docs.djangoproject.com/en/dev/ref/contrib/admin/…
  • Dave
    Dave over 11 years
    Thanks, works great! Much cleaner. (And btw, you left some logging statements in your code that don't go anywhere)
  • fangsterr
    fangsterr almost 11 years
    This code saved me TONS of time as well. Thanks a lot for posting this
  • raratiru
    raratiru almost 10 years
    Could you please adapt the solution to the models existing in the question?
  • Chris Cogdon
    Chris Cogdon about 8 years
    This doesn't help as the query that runs in the limit_choices_to has no reference to the "parent class". I.e, if a model A has a foreign-key to B, and also to C, and C has a foreign-key to B, and we want to ensure an A only refers to a C that refers to the same B as A does, the query needs to know about A->B, which it doesn't.
  • Miguel Ike
    Miguel Ike about 8 years
    THIS! I was looking for something like this for my problem. Took me days to find this.
  • Daniil Mashkin
    Daniil Mashkin about 6 years
    It can be usefull with top answer combination, see stackoverflow.com/a/50298577/2207154
  • Daniil Mashkin
    Daniil Mashkin about 6 years
    But user still can select wrong Room in popup. See stackoverflow.com/a/50298577/2207154 for solution
  • Twitch
    Twitch over 4 years
    This worked for me in django 2.2 without needing to use field.widget.rel.limit_choices_to = {'building_id': building.id}
  • Sezgin İbiş
    Sezgin İbiş over 3 years
    Sometimes I create sites where forms are used extensively. In that case make sure ..! It is mandatory to use the admin section to manipulate the tables. Other than that, I agree with what you said.