Default filter in Django admin

Viewed 44396

How can I change the default filter choice from 'ALL'? I have a field named as status which has three values: activate, pending and rejected. When I use list_filter in Django admin, the filter is by default set to 'All' but I want to set it to pending by default.

17 Answers
class MyModelAdmin(admin.ModelAdmin):   

    def changelist_view(self, request, extra_context=None):

        if not request.GET.has_key('decommissioned__exact'):

            q = request.GET.copy()
            q['decommissioned__exact'] = 'N'
            request.GET = q
            request.META['QUERY_STRING'] = request.GET.urlencode()
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

Created a reusable Filter sub-class, inspired by some of the answers here (mostly Greg's).

Advantages:

Reusable - Pluggable in any standard ModelAdmin classes

Extendable - Easy to add additional/custom logic for QuerySet filtering

Easy to use - In its most basic form, only one custom attribute and one custom method need to be implemented (apart from those required for SimpleListFilter subclassing)

Intuitive admin - The "All" filter link is working as expected; as are all the others

No redirects - No need to inspect GET request payload, agnostic of HTTP_REFERER (or any other request related stuff, in its basic form)

No (changelist) view manipulation - And no template manipulations (god forbid)

Code:

(most of the imports are just for type hints and exceptions)

from typing import List, Tuple, Any

from django.contrib.admin.filters import SimpleListFilter
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.views.main import ChangeList
from django.db.models.query import QuerySet
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError


class PreFilteredListFilter(SimpleListFilter):

    # Either set this or override .get_default_value()
    default_value = None

    no_filter_value = 'all'
    no_filter_name = _("All")

    # Human-readable title which will be displayed in the
    # right admin sidebar just above the filter options.
    title = None

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

    def get_default_value(self):
        if self.default_value is not None:
            return self.default_value
        raise NotImplementedError(
            'Either the .default_value attribute needs to be set or '
            'the .get_default_value() method must be overridden to '
            'return a URL query argument for parameter_name.'
        )

    def get_lookups(self) -> List[Tuple[Any, str]]:
        """
        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.
        """
        raise NotImplementedError(
            'The .get_lookups() method must be overridden to '
            'return a list of tuples (value, verbose value).'
        )

    # Overriding parent class:
    def lookups(self, request, model_admin) -> List[Tuple[Any, str]]:
        return [(self.no_filter_value, self.no_filter_name)] + self.get_lookups()

    # Overriding parent class:
    def queryset(self, request, queryset: QuerySet) -> QuerySet:
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        if self.value() is None:
            return self.get_default_queryset(queryset)
        if self.value() == self.no_filter_value:
            return queryset.all()
        return self.get_filtered_queryset(queryset)

    def get_default_queryset(self, queryset: QuerySet) -> QuerySet:
        return queryset.filter(**{self.parameter_name: self.get_default_value()})

    def get_filtered_queryset(self, queryset: QuerySet) -> QuerySet:
        try:
            return queryset.filter(**self.used_parameters)
        except (ValueError, ValidationError) as e:
            # Fields may raise a ValueError or ValidationError when converting
            # the parameters to the correct type.
            raise IncorrectLookupParameters(e)

    # Overriding parent class:
    def choices(self, changelist: ChangeList):
        """
        Overridden to prevent the default "All".
        """
        value = self.value() or force_str(self.get_default_value())
        for lookup, title in self.lookup_choices:
            yield {
                'selected': value == force_str(lookup),
                'query_string': changelist.get_query_string({self.parameter_name: lookup}),
                'display': title,
            }

Full usage example:

from django.contrib import admin
from .models import SomeModelWithStatus


class StatusFilter(PreFilteredListFilter):
    default_value = SomeModelWithStatus.Status.FOO
    title = _('Status')
    parameter_name = 'status'

    def get_lookups(self):
        return SomeModelWithStatus.Status.choices


@admin.register(SomeModelWithStatus)
class SomeModelAdmin(admin.ModelAdmin):
    list_filter = (StatusFilter, )

Hope this helps somebody; feedback always appreciated.

Note that if instead of pre-selecting a filter value you want to always pre-filter the data before showing it in the admin, you should override the ModelAdmin.queryset() method instead.

Here's the Cleanest version I was able to generate of a filter with a redefined 'All' and a Default value that is selected.

If shows me by default the Trips currently happening.

class HappeningTripFilter(admin.SimpleListFilter):
    """
    Filter the Trips Happening in the Past, Future or now.
    """
    default_value = 'now'
    title = 'Happening'
    parameter_name = 'happening'

    def lookups(self, request, model_admin):
        """
        List the Choices available for this filter.
        """
        return (
            ('all', 'All'),
            ('future', 'Not yet started'),
            ('now', 'Happening now'),
            ('past', 'Already finished'),
        )

    def choices(self, changelist):
        """
        Overwrite this method to prevent the default "All".
        """
        value = self.value() or self.default_value
        for lookup, title in self.lookup_choices:
            yield {
                'selected': value == force_text(lookup),
                'query_string': changelist.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        """
        Returns the Queryset depending on the Choice.
        """
        value = self.value() or self.default_value
        now = timezone.now()
        if value == 'future':
            return queryset.filter(start_date_time__gt=now)
        if value == 'now':
            return queryset.filter(start_date_time__lte=now, end_date_time__gte=now)
        if value == 'past':
            return queryset.filter(end_date_time__lt=now)
        return queryset.all()

Using ha22109's answer I wrote a mixin for ModelAdmin class:

from urllib.parse import urlencode
from django.contrib.admin.views.main import SEARCH_VAR
from django.http import HttpRequest, QueryDict


class DefaultFilterMixin:

    default_filters: Sequence[tuple[str, Any]] | dict[str, Any] | None = None

    def get_default_filters(
        self,
        request: HttpRequest,
    ) -> Sequence[tuple[str, Any]] | dict[str, Any] | None:
        return self.default_filters

    def changelist_view(
        self,
        request: HttpRequest,
        extra_context: dict[str, str] | None = None,
    ):
        if request.method == 'GET' and not request.GET:
            if default_filters := self.get_default_filters(request):
                request.GET = QueryDict(
                    f"{urlencode(default_filters)}&{SEARCH_VAR}=",
                    encoding=request.encoding,
                )
                request.META['QUERY_STRING'] = request.GET.urlencode()

        return super().changelist_view(request, extra_context=extra_context)

Simple example:

class MyModelAdmin(DefaultFilterMixin, admin.ModelAdmin):

    default_filters = (("status__exact", "pending"),)
    ...

Or more complex dynamic filter for rangefilter.DateTimeRangeFilter:

class MyModelAdmin(DefaultFilterMixin, admin.ModelAdmin):

        def get_default_filters(
            self,
            request: HttpRequest,
        ) -> Sequence[tuple[str, Any]] | dict[str, Any] | None:
            now = timezone.now()
            date_fmt = '%d.%m.%Y'
            return (
                ('created_at__range__gte_0', now.strftime(date_fmt)),
                ('created_at__range__gte_1', '00:00:00'),
                ('created_at__range__lte_0', (now + timedelta(1)).strftime(date_fmt)),
                ('created_at__range__lte_1', '00:00:00'),
            )

replying to the first answer (from Evgeni Shudzel) with the default mixin... here is a solution with less imports and complications

from urllib.parse import urlencode
from django.contrib.admin.views.main import SEARCH_VAR
from django.http import HttpRequest, QueryDict

class DefaultFilterMixin:
    default_filters: None

    def get_default_filters(self, request: HttpRequest):
        return self.default_filters

    def changelist_view(self, request: HttpRequest, extra_context=None):
        if request.method == 'GET' and not request.GET:
            if default_filters := self.get_default_filters(request):
                request.GET = QueryDict(
                    f"{urlencode(default_filters)}&{SEARCH_VAR}=",
                    encoding=request.encoding,
                )
                request.META['QUERY_STRING'] = request.GET.urlencode()

        return super().changelist_view(request, extra_context=extra_context)

class SampleUsageAdmin(DefaultFilterMixin, admin.ModelAdmin):
    default_filters = (("fulfillments_complete__exact", "0"),)
Related