How to override the queryset giving the filters in list_filter?
Asked Answered
A

5

29

Given the following models

class AnotherModel(models.Model):
    n = models.IntegerField()

class MyModel(models.Model):
    somefield = models.ForeignKey(AnotherModel)

and admin

class MyModelAdmin(admin.ModelAdmin):        
    list_filter = ('somefield',)

how can I filter the instances of AnotherModel to show only those with a given n value in my admin filter?

I need something like:

Filter

By somefield

all

[list of AnotherModel instances with given n]

Athenaathenaeum answered 21/9, 2012 at 0:8 Comment(0)
I
39

See ModelAdmin.queryset and ModelAdmin.formfield_for_foreignkey. From the docs:

The queryset method on a ModelAdmin returns a QuerySet of all model instances that can be edited by the admin site. One use case for overriding this method is to show objects owned by the logged-in user:

class MyModelAdmin(admin.ModelAdmin):
    def queryset(self, request):
        qs = super(MyModelAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(author=request.user)

The formfield_for_foreignkey method on a ModelAdmin allows you to override the default formfield for a foreign keys field. For example, to return a subset of objects for this foreign key field based on the user:

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

This uses the HttpRequest instance to filter the Car foreign key field to only display the cars owned by the User instance.

[update]

Sorry, I failed to read the "filter" part. In Django >= 1.4 you can pass a subclass of django.contrib.admin.SimpleListFilter in the list_filter argument list, which you can use in order to override the lookups and queryset methods.

from datetime import date

from django.contrib import admin
from django.utils.translation import ugettext_lazy as _

class DecadeBornListFilter(admin.SimpleListFilter):
    # Human-readable title which will be displayed in the
    # right admin sidebar just above the filter options.
    title = _('decade born')

    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'decade'

    def lookups(self, request, model_admin):
        """
        Returns a list of tuples. The first element in each
        tuple is the coded value for the option that will
        appear in the URL query. The second element is the
        human-readable name for the option that will appear
        in the right sidebar.
        """
        return (
            ('80s', _('in the eighties')),
            ('90s', _('in the nineties')),
        )

    def queryset(self, request, queryset):
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        # Compare the requested value (either '80s' or '90s')
        # to decide how to filter the queryset.
        if self.value() == '80s':
            return queryset.filter(birthday__gte=date(1980, 1, 1),
                                birthday__lte=date(1989, 12, 31))
        if self.value() == '90s':
            return queryset.filter(birthday__gte=date(1990, 1, 1),
                                birthday__lte=date(1999, 12, 31))

class PersonAdmin(admin.ModelAdmin):
    list_filter = (DecadeBornListFilter,)
Illyria answered 21/9, 2012 at 0:14 Comment(5)
I need to filter the queryset of the filters, not of the model itself. I'll edit my question to make it clearer.Athenaathenaeum
With Django 1.10, you need to override get_queryset() and not queryset().Arrive
@AksharRaaj, are you sure? I was looking at the example in the docs and as of Django 2.1 the method name is still queryset, not ``get_queryset`. Am I missing something?Illyria
Sure about Django 1.10.5Arrive
The docs for 1.10 say otherwise. Mind you, this is a filter, not a class based view.Illyria
B
7

Edit - this method has been pointed out to have issues, see below

You can do it like this:

Let's say you have a model called Animal, which has a ForeignKey field to a model called Species. In a particular admin list, you want to allow only certain species to be shown in the animals filter choices.

First, specify a custom ListFilter called SpeciesFilter in the Animal's ModelAdmin:

class AnimalAdmin(ModelAdmin):
    list_filter = (('species', SpeciesFilter),)

Then define the SpeciesFilter:

from django.contrib.admin.filters import RelatedFieldListFilter

class SpeciesFilter(RelatedFieldListFilter):
    def __init__(self, field, request, *args, **kwargs):
        """Get the species you want to limit it to.
        This could be determined by the request,
        But in this example we'll just specify an
        arbitrary species"""
        species = Species.objects.get(name='Tarantula')

        #Limit the choices on the field
        field.rel.limit_choices_to = {'species': species}

        #Let the RelatedFieldListFilter do its magic 
        super(SpeciesFilter, self).__init__(field, request, *args, **kwargs)

That should do it.

Byrne answered 18/2, 2013 at 15:58 Comment(2)
This can cause unexpected results! (I tried it with Django 1.5.) The filter lookups are getting cached in some way. Therefore RelatedFieldListFilter doesn't work with dynamic lookups (e.g. if they vary between users). All users will get the same filter. You don't have such issues with the documented SimpleListFilter. See this answer on how to do it.Proparoxytone
Thanks yofee for pointing this out - I think it's because the field and field.rel objects will persist between requests, so this can affect places other than the admin. I looked into making copies of those two objects so we can change them just in this place, but it doesn't seem to work well. As yofee says, don't use this method!Byrne
S
3

I found another method similar to @seddonym, but doesn't mess with the caching. It is based on this Django code, but uses undocumented method field_choices, which can be subject to change in the future Django releases. The code for @seddonym's case would be:

from django.contrib.admin.filters import RelatedFieldListFilter

class SpeciesFilter(RelatedFieldListFilter):
    def field_choices(self, field, request, model_admin):
        return field.get_choices(include_blank=False, limit_choices_to={'name': 'Tarantula'})

Or in my case the working code is:

from django.contrib.admin.filters import RelatedFieldListFilter

class UnitFilter(RelatedFieldListFilter):
    def field_choices(self, field, request, model_admin):
        return field.get_choices(include_blank=False, limit_choices_to={'pk__in': request.user.administrated_units.all()})

Sap answered 15/7, 2019 at 9:58 Comment(0)
C
1

I had to create my lookup fields from db table. I created custom filter class as below and displaying only related values to logged in user and filter accordingly:

class ShiftFilter_Org(admin.SimpleListFilter):
title = 'Organisation'
parameter_name = 'org'

def lookups(self, request, model_admin):
"""Get the organisations you want to limit"""
    qs_org = Organisation.objects.filter(users=request.user)
    list_org = []
    for og in qs_org:
        list_org.append(
            (og.id, og.name)
        )
    return (
        sorted(list_org, key=lambda tp:tp[1])
    )

def queryset(self, request, queryset):
    if self.value():
        return queryset.filter(org=self.value())

For more visit Getting the most out of Django Admin filters

Caputto answered 31/5, 2018 at 13:29 Comment(0)
S
0

To use RelatedFieldListFilter like suggested in some answers, you need to pass a tuple to list_filter. Following the classes definition used in the original question, you would have:

class MyModelAdmin(admin.ModelAdmin): 
    class AnotherModelListFilter(admin.RelatedFieldListFilter):
        def field_choices(self, field, request, model_admin):
            ordering = self.field_admin_ordering(field, request, model_admin)
            return field.get_choices(
                include_blank=False,
                ordering=ordering,
                limit_choices_to=dict(n=<THE_VALUE_YOU_WANT>),
            )
    list_filter = (('somefield', AnotherModelListFilter),)
Sewan answered 27/5, 2022 at 15:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.