Limit foreign key choices in select in an inline form in admin
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)
Related videos on Youtube
mightyhal
Updated on July 05, 2022Comments
-
mightyhal almost 2 years
The logic is of the model is:
- A
Building
has manyRooms
- A
Room
may be inside anotherRoom
(a closet, for instance--ForeignKey on 'self') - A
Room
can only be inside anotherRoom
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 theinside_room
choices to onlyrooms
which are in the currentbuilding
(the building record currently being altered by the mainBuildingAdmin
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...
- A
-
Peter G about 12 yearsThis 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 about 12 yearsThank you for this! Its the first version that worked for my application and its nice and clear too.
-
powlo almost 12 yearsThanks a million! An alternative is to assign kwargs['queryset'] before calling super as per docs: docs.djangoproject.com/en/dev/ref/contrib/admin/…
-
Dave over 11 yearsThanks, works great! Much cleaner. (And btw, you left some logging statements in your code that don't go anywhere)
-
fangsterr almost 11 yearsThis code saved me TONS of time as well. Thanks a lot for posting this
-
raratiru almost 10 yearsCould you please adapt the solution to the models existing in the question?
-
Chris Cogdon about 8 yearsThis 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 about 8 yearsTHIS! I was looking for something like this for my problem. Took me days to find this.
-
Daniil Mashkin about 6 yearsIt can be usefull with top answer combination, see stackoverflow.com/a/50298577/2207154
-
Daniil Mashkin about 6 yearsBut user still can select wrong
Room
in popup. See stackoverflow.com/a/50298577/2207154 for solution -
Twitch over 4 yearsThis worked for me in django 2.2 without needing to use
field.widget.rel.limit_choices_to = {'building_id': building.id}
-
Sezgin İbiş over 3 yearsSometimes 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.