Limit the choice of a foreign key when choosing in the built-in form in admin

The logic of the model is as follows:

  • A Building Has Many Rooms
  • A Room may be inside another Room (e.g. a closet, e.g. ForeignKey on self)
  • A Room can only be inside another Room in the same building (this is the difficult part)

Here is 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) 

Only numbers in the current building will be displayed in the line (this is what I want). The problem is that when inside_room it displays all the numbers in the Rooms table (including in other buildings).

On the Rooms line, I need to restrict the inside_room selection inside_room only Rooms that are in the current Building (the building record is currently being changed by the main BuildingAdmin form).

I can’t figure out how to do this with limit_choices_to in the model, and I can’t figure out how to correctly override the built-in set of admin forms (I feel I have to somehow create a custom inline form, pass the build_id of the main form to the custom inline, and then limit the set of queries to select fields based on this, but I just can't wrap myself around how to do this).

It may be too complicated for the admin site, but it seems like it is generally useful ...

+60
django limit inline admin foreign-keys
Dec 01 '09 at 6:01
source share
9 answers

The request instance is used as a temporary container for obj. Override the Inline method formfield_for_foreignkey to change the set of requests. 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) 
+87
Nov 21 '10 at 2:45
source share

After reading this post and experimenting a lot, I think I found a fairly definitive answer to this question. Since this is the design pattern that is used, I wrote Mixin for the Django admin to use it.

(dynamically) restricting the set of queries for ForeignKey fields is now as simple as subclassing LimitedAdminMixin and defining the get_filters(obj) method to return the appropriate filters. Alternatively, the filters property can be set to admin if dynamic filtering is not required.

Usage example:

 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 that you usually specify in the filter() method of queries.

+14
Feb 15 '11 at 20:14
source share

There is a limit_choices_to ForeignKey parameter, which allows you to restrict the administrator access to the object.

+10
Jul 12 2018-12-12T00
source share

You can create a pair of custom classes that will then pass a link to the parent instance of 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 the RoomInline class, just add:

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

In your form, you now have access to the init method for self.parent_instance! parent_instance can now be used to select filters 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 
+8
Apr 12 2018-12-12T00:
source share

This question and answer is very similar and works for a regular admin form

Inside the built-in - and where it falls apart ... I just can’t get the main form data to get the foreign key value that I need in my limit (or one of the built-in records to capture the value).

Here is my admin.py. I guess I'm looking for magic to replace ???? c - if I connect a hard-set value (say 1), it works fine and correctly limits the available options in the built-in ...

 #spaces/admin.py from demo.spaces.models import Building, Room from django.contrib import admin from django.forms import ModelForm class RoomInlineForm(ModelForm): def __init__(self, *args, **kwargs): super(RoomInlineForm, self).__init__(*args, **kwargs) self.fields['inside_room'].queryset = Room.objects.filter( building__exact=????) # <------ class RoomInline(admin.TabularInline): form = RoomInlineForm model=Room class BuildingAdmin(admin.ModelAdmin): inlines=[RoomInline] admin.site.register(Building, BuildingAdmin) admin.site.register(Room) 
+4
Dec 02 '09 at 2:30
source share

I found a pretty elegant solution that works well for inline forms.

Applies to my model, where I filter the inside_room field to only return rooms that are in the same building:

 #spaces/admin.py class RoomInlineForm(ModelForm): def __init__(self, *args, **kwargs): super(RoomInlineForm, self).__init__(*args, **kwargs) #On init... if 'instance' in kwargs: building = kwargs['instance'].building else: building_id = tuple(i[0] for i in self.fields['building'].widget.choices)[1] building = Building.objects.get(id=building_id) self.fields['inside_room'].queryset = Room.objects.filter(building__exact=building) 

Basically, if the keyword "instance" is passed to the form, it is an existing entry displayed in the inline string, and so I can just grab the building from the instance. If this is not an instance, it is one of the empty extra lines in the line, and therefore it goes through the hidden fields of the embedded line form, which save the implicit relation back to the main page and grab the id value from it. Then it captures the building object based on this building_id. Finally, now that we have the building, we can set up a set of queries for the drop-down lists to display only the corresponding elements.

More elegant than my original solution, which crashed and burned as a built-in (but it worked - well, if you do not mind saving the form partially to fill in the drop-downs - for individual forms):

 class RoomForm(forms.ModelForm): # For the individual rooms class Meta: mode = Room def __init__(self, *args, **kwargs): # Limits inside_room choices to same building only super(RoomForm, self).__init__(*args, **kwargs) #On init... try: self.fields['inside_room'].queryset = Room.objects.filter( building__exact=self.instance.building) # rooms with the same building as this room except: #and hide this field (why can't I exclude?) self.fields['inside_room']=forms.CharField( #Add room throws DoesNotExist error widget=forms.HiddenInput, required=False, label='Inside Room (save room first)') 

For non-inlines, it worked if a room already existed. If this is not the case, it will cause an error (DoNotExist), so I will catch it and then hide the field (since there was no way from the administrator to restrict it to the desired building, since the entire room number was new and no building was installed yet!) ... as soon as you click "Save", it will save the building and reload it to limit the choice ...

I just need to find a way to cascade foreign key filters from one field to another in a new record - that is, a new record, select the building and automatically limit the selection in the selection box inside the message - until the record is saved. But this is the next day ...

+4
Dec 09 '09 at 2:39
source share

If Daniel, after editing your question, didn’t answer - I don’t think that I will be very helpful ... :-)

I am going to assume that you are trying to adapt the django admin to some logic that would be better implemented as your own group of views, forms, and templates.

I don’t think you can apply this filtering to InlineModelAdmin.

+2
Dec 01 '09 at 16:05
source share

In django 1.6:

  form = SpettacoloForm( instance = spettacolo ) form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all() 
+2
Jul 18 '14 at 15:08
source share

I must admit that I didn’t do exactly what you are trying to do, but I find it complicated enough that you can think about not basing your site on the admin.

I created a site that started with a simple admin interface, but over time I became so customized that it became very difficult to work with administrator restrictions. I would be better off if I were just starting from scratch - more work in the beginning, but much more flexibility and less pain in the end. My thumb rule would be if what you are trying to do is not documented (i.e. includes overriding admin methods, viewing admin in source code, etc.), then you probably better not use admin. Just me two cents. :)

+1
Dec 01 '09 at 17:09
source share



All Articles