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 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...
I found a fairly elegant solution that works well for inline forms.
Applied to my model, where I'm filtering the inside_room field to only return rooms that are in the same building:
Basically, if an 'instance' keyword is passed to the form, it's an existing record showing in the inline, and so I can just grab the building from the instance. If not an instance, it's one of the blank "extra" rows in the inline, and so it goes through the hidden form fields of the inline that store the implicit relation back to the main page, and grabs the id value from that. Then, it grabs the building object based on that building_id. Finally, now having the building, we can set the queryset of the drop downs to only display the relevant items.
More elegant than my original solution, which crashed and burned as inline (but worked--well, if you don't mind saving the form partway to make the drop downs fill in-- for the individual forms):
For non-inlines, it worked if the room already existed. If not, it would throw an error (DoesNotExist), so I'd catch it and then hide the field (since there was no way, from the Admin, to limit it to the right building, since the whole room record was new, and no building was yet set!)...once you hit save, it saves the building and on reload it could limit the choices...
I just need to find a way to cascade the foreign key filters from one field to another in a new record--i.e., new record, select a building, and it automatically limits the choices in the inside_room select box--before the record gets saved. But that's for another day...
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 aget_filters(obj)
method to return the relevant filters. Alternateively, afilters
property can be set on the admin if dynamic filtering is not required.Example usage:
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 thefilter()
method of querysets.This question and answer is very similar, and works for a regular admin form
Inside of an inline--and that's where it falls apart... I just can't get at the main form's data to get the foreign key value I need in my limit (or to one of the inline's records to grab the value).
Here's my admin.py. I guess I'm looking for the magic to replace the ???? with--if I plug in a hardcoded value (say, 1), it works fine and properly limits the the available choices in the inline...
You can create a couple of custom classes that will then pass along a reference to the parent instance to the form.
in class RoomInline just add:
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: