March 16, 2022

A Django admin list filter for PostgreSQL ArrayFields

Django’s ArrayField is a nifty resource straight out of the Field API, helping you avoid polluting your database with tables that you already know won’t be subject to advanced aggregations. Yet, there is no such thing as a native helper for a trivial implementation of an admin list filter feeding from an ArrayField. Let’s fill the gap!

Say you have a WebPage model with a domains ArrayField. You could use a clean solution and have a list filter for that field, requiring only a display title and the query string parameter name:

class DomainsListFilter(ArrayFieldListFilter):
    """An admin list filter for domains."""

    title = "domain"
    parameter_name = "domains"

To achieve this, we can have the ArrayFieldListFilter class helper inherit from SimpleListFilter. In the lookups method override, we can parse the input received from the domains field as a lexicographically sorted list of non-falsy values for the filter widget and also replace the original queryset method to have the URL lookup value processed in two alternative control branches — allowing for either a proper (non-exact) value or an empty one disabling the filtering altogether.

from django.contrib.admin import SimpleListFilter

class ArrayFieldListFilter(SimpleListFilter):
    """An admin list filter for ArrayFields."""

    def lookups(self, request, model_admin):
        """Return the filtered queryset."""
        queryset_values = model_admin.model.objects.values_list(
            self.parameter_name, flat=True
        )
        values = []
        for sublist in queryset_values:
            if sublist:
                for value in sublist:
                    if value:
                        values.append((value, value))
            else:
                values.append(("null", "-"))
        return sorted(set(values))

    def queryset(self, request, queryset):
        """Return the filtered queryset."""
        lookup_value = self.value()
        if lookup_value:
            lookup_filter = (
                {"{}__isnull".format(self.parameter_name): True}
                if lookup_value == "null"
                else {"{}__contains".format(self.parameter_name):
                [lookup_value]}
            )
            queryset = queryset.filter(**lookup_filter)
        return queryset

Finally, all is left to do is to use the class in the ModelAdmin, like so:

from django.contrib import admin

class WebPageAdmin(admin.ModelAdmin):
    """The web page admin."""

    list_filter = (DomainsListFilter,)

Find here the helper class’ source code.

Copyright © 2024 Niccolò Mineo
Some rights reserved: CC BY-NC 4.0