Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
InlineAdmin mixin limiting the selection of related items according to criteria which can depend on the current parent object being edited.
class LimitedAdminInlineMixin(object):
"""
InlineAdmin mixin limiting the selection of related items according to
criteria which can depend on the current parent object being edited.
A typical use case would be selecting a subset of related items from
other inlines, ie. images, to have some relation to other inlines.
Use as follows::
class MyInline(LimitedAdminInlineMixin, admin.TabularInline):
def get_filters(self, obj):
return (('<field_name>', dict(<filters>)),)
"""
@staticmethod
def limit_inline_choices(formset, field, empty=False, **filters):
"""
This function fetches the queryset with available choices for a given
`field` and filters it based on the criteria specified in filters,
unless `empty=True`. In this case, no choices will be made available.
"""
assert formset.form.base_fields.has_key(field)
qs = formset.form.base_fields[field].queryset
if empty:
logger.debug('Limiting the queryset to none')
formset.form.base_fields[field].queryset = qs.none()
else:
qs = qs.filter(**filters)
logger.debug('Limiting queryset for formset to: %s', qs)
formset.form.base_fields[field].queryset = qs
def get_formset(self, request, obj=None, **kwargs):
"""
Make sure we can only select variations that relate to the current
item.
"""
formset = \
super(LimitedAdminInlineMixin, self).get_formset(request,
obj,
**kwargs)
for (field, filters) in self.get_filters(obj):
if obj:
self.limit_inline_choices(formset, field, **filters)
else:
self.limit_inline_choices(formset, field, empty=True)
return formset
def get_filters(self, obj):
"""
Return filters for the specified fields. Filters should be in the
following format::
(('field_name', {'categories': obj}), ...)
For this to work, we should either override `get_filters` in a
subclass or define a `filters` property with the same syntax as this
one.
"""
return getattr(self, 'filters', ())
@ecabuk

This comment has been minimized.

Copy link

@ecabuk ecabuk commented Apr 21, 2013

Thanks!

@mhulse

This comment has been minimized.

Copy link

@mhulse mhulse commented Apr 18, 2015

This code worked perfectly for existing entries. Unfortunately, I was getting a 'NoneType' object has no attribute 'vehicle' exception when adding a new item (Django 1.7, Python 3). I had to do this:

class QuoteAdminInline(LimitedAdminInlineMixin, admin.TabularInline):

    def get_filters(self, obj):

        return getattr(self, 'filters', ()) if obj is None else (('product', dict(vehicle=obj.vehicle)),)

Not sure if that's the best solution. Maybe it will help others though.

@flebel

This comment has been minimized.

Copy link

@flebel flebel commented Nov 30, 2017

@mhulse you likely got this error because you forgot to implement get_filters in your inline class.

@flebel

This comment has been minimized.

Copy link

@flebel flebel commented Nov 30, 2017

My fork contains some minor changes and was tested on Django 2.0b1 with Python 3.6.
https://gist.github.com/flebel/418d4eac306084259b4a4f1d71e043f2/revisions#diff-debb229a55dd842402fd853a9baacdd1

Only the assert line needed to be updated to support Python 3.

@danmash

This comment has been minimized.

Copy link

@danmash danmash commented May 11, 2018

@flebel I still see all entities in the popup
2018-05-11 20-40-06
Is there a way to open popup with filter?

@mileto94

This comment has been minimized.

Copy link

@mileto94 mileto94 commented Feb 22, 2019

Thanks for the snippet. It helped me a lot.

I needed also to restrict the queryset according to exclude conditions so I've tweaked a bit get_formset as:

    def get_formset(self, request, obj=None, **kwargs):
        """
        Make sure we can only select variations that relate to the current
        item.
        """
        formset = super(LimitedAdminInlineMixin, self).get_formset(
            request, obj, **kwargs)

        qs_attributes = {
            'filter': self.get_filters(obj),
            'exclude': self.get_excludes(obj)
        }
        for attr, options in qs_attributes.items():

            for (field, filters) in options:
                if obj:
                    self.limit_inline_choices(formset, field, qs_attr=attr, **filters)
                else:
                    self.limit_inline_choices(formset, field, empty=True)

        return formset

Where self.get_excludes has the same format as self.filters
Also limit_inline_choices accepts qs_attr='filter' and Line-31 became: qs = getattr(qs, qs_attr)(**filters)

@stefanitsky

This comment has been minimized.

Copy link

@stefanitsky stefanitsky commented Mar 10, 2020

Thanks a lot. BTW, has_key is deprecated (i'm using Django >= 2.2) and if someone is getting error like:

AttributeError: 'collections.OrderedDict' object has no attribute 'has_key'

Just change Line-24:

assert formset.form.base_fields.has_key(field)

For:

assert field in formset.form.base_fields
@dokterbob

This comment has been minimized.

Copy link
Owner Author

@dokterbob dokterbob commented Mar 10, 2020

@stefanitsky

This comment has been minimized.

Copy link

@stefanitsky stefanitsky commented Mar 10, 2020

Great tip! If you are perhaps able to make this a pull request (cllick the edit button on the file in question), I promise that it’ll be immediately accepted! :)

I dont think that is possible, but i created a fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment