[Django]-Django difficulty using ModelAdmin.queryset with ModelAdmin.list_filter to limit the filter itrems displayed

3👍

Is there any particular reason you expect the related fields in list_filter to care about the queryset on the admin class? Such filters do not generally remove values that don’t have a related object present.

If you don’t want that behavior, you can write a single filter class that limits the choices however you like as a subset of django.contrib.admin.filter.RelatedFieldListFilter and use that instead, either overriding its choices method (the queryset as you defined it on the model admin will be available as cl.root_query_set within that method) or overriding its __init__ method to create self.lookup_choices differently – perhaps based on the request. I see no reason you should have to keep redefining the class – one definition should work with as many related fields as you like.

Here’s a simple one that should only include items in the filter if the admin’s filtered queryset has at least one object for the filter value:

class RelatedFieldRestrictedListFilter(RelatedFieldListFilter):

    def choices(self, cl):
        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
        yield {
            'selected': self.lookup_val is None and not self.lookup_val_isnull,
            'query_string': cl.get_query_string({},
                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
            'display': _('All'),
        }
        for pk_val, val in self.lookup_choices:
             if cl.root_query_set.filter(**{self.lookup_kwarg: pk_val}).exists():
                 yield {
                    'selected': self.lookup_val == smart_unicode(pk_val),
                    'query_string': cl.get_query_string({
                        self.lookup_kwarg: pk_val,
                    }, [self.lookup_kwarg_isnull]),
                    'display': val,
                    }
        if (isinstance(self.field, models.related.RelatedObject)
                and self.field.field.null or hasattr(self.field, 'rel')
                    and self.field.null):
            yield {
                'selected': bool(self.lookup_val_isnull),
                'query_string': cl.get_query_string({
                    self.lookup_kwarg_isnull: 'True',
                }, [self.lookup_kwarg]),
                'display': EMPTY_CHANGELIST_VALUE,
            }

This does a separate query against the DB for each possible value in the list_filter field, so it’s a bit inefficient – if that turns into a problem you should customize __init__ instead.

Followup:
Ok, so the correct filter choices depend on the request session. That means you need to establish them in the __init__ method of a subclass. Your example shows a related field, so I’ll use RelatedFieldListFilter again – I’m honestly not sure the concept of a restricted queryset makes sense for any other kind of filter. To do that, the lazy method (shorter to write, less efficient) would be to call the superclass’s __init__, then change self.lookup_choices. The less lazy method would be to entirely override __init__.

The lazy method is something like this:

from django.utils.encoding import smart_unicode

class RelatedFieldRestrictedListFilter(RelatedFieldListFilter):

    def __init__(self, field, request, params, model, model_admin, field_path):
        super(RelatedFieldRestrictedListFilter, self).__init__(field, request, params, model, model_admin, field_path)
        if 'site' in request.session:
            self.lookup_choices = [(instance.pk, smart_unicode(instance) for instance in model.objects.filter(site=request.session['site'])]
        else:
            # something else here

The less lazy method would involve copying in the base code from the superclass’s __init__ method and replacing self.lookup_choices = field.get_choices(include_blank=False) line with the above.

Note that I’m allowing for the possibility that the session may not have a site – you should think about what you want to happen if that’s the case. And maybe not bother changing lookup_choices if the user is a superuser.

Leave a comment