Filtering Django Admin by Null/Is Not Null
Asked Answered
K

8

28

I have a simple Django model like:

class Person(models.Model):
    referrer = models.ForeignKey('self', null=True)
    ...

In this model's ModelAdmin, how would I allow it to be filtered by whether or not referrer is null? By default, adding referrer to list_filter causes a dropdown to be shown that lists every person record, which may be in the hundreds of thousands, effectively preventing the page from loading. Even if it loads, I still can't filter by the criteria I want.

i.e. How would I modify this so that the dropdown only lists "All", "Null", or "Not Null" choices?

I've seen some posts that claim to accomplish something similar using custom FilterSpec subclasses, but none of them explain how to use them. The few I've seen appear to apply to all fields in all models, which I wouldn't want. Moreover, there's zero documentation for FilterSpec, which makes me nervous, because I don't want to invest in a lot of custom code tied to some transient internal class that might disappear by the next release.

Kirstenkirsteni answered 7/10, 2011 at 19:18 Comment(0)
K
2

I ended up using a mixture of the top solution here, along with this snippet.

However, I had to tweak the snippet slightly, dropping the field type restriction and adding the new field_path, recently added in 1.3.

from django.contrib.admin.filterspecs import FilterSpec
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _

class NullFilterSpec(FilterSpec):
    #fields = (models.CharField, models.IntegerField, models.FileField)

    @classmethod
    def test(cls, field):
        #return field.null and isinstance(field, cls.fields) and not field._choices
        return field.null and not field._choices
    #test = classmethod(test)

    def __init__(self, f, request, params, model, model_admin, field_path=None):
        super(NullFilterSpec, self).__init__(f, request, params, model, model_admin, field_path)
        self.lookup_kwarg = '%s__isnull' % f.name
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)

    def choices(self, cl):
        # bool(v) must be False for IS NOT NULL and True for IS NULL, but can only be a string
        for k, v in ((_('All'), None), (_('Has value'), ''), (_('Omitted'), '1')):
            yield {
                'selected' : self.lookup_val == v,
                'query_string' : cl.get_query_string({self.lookup_kwarg : v}),
                'display' : k
            }

# Here, we insert the new FilterSpec at the first position, to be sure
# it gets picked up before any other
FilterSpec.filter_specs.insert(0,
    # If the field has a `profilecountry_filter` attribute set to True
    # the this FilterSpec will be used
    (lambda f: getattr(f, 'isnull_filter', False), NullFilterSpec)
)
Kirstenkirsteni answered 7/10, 2011 at 19:59 Comment(0)
A
40

After Django 3.1 you can use EmptyFieldListFilter:

class MyAdmin(admin.ModelAdmin):
    list_filter =  (
        ("model_field", admin.EmptyFieldListFilter),
    )
Anya answered 31/8, 2020 at 22:29 Comment(1)
As an alternative, if you are using Django lower versions, create a file(say: admin_filter.py) and add the code of EmptyFieldListFilter into a file and import it into the admin file and use class MyAdmin(admin.ModelAdmin): list_filter = ( ("model_field", admin_filter.EmptyFieldListFilter), )Theta
O
16

Since Django 1.4 brings some changes to filters, I thought I save someone the time I just spent modifying the code from Cerin's accepted answer to work with Django 1.4 rc1.

I have a model that has TimeField(null=True) named "started" and I wanted to filter for null and non-null values, so it's prety much the same problem as OP.
So, here is what worked for me...

Defined (actually included) these in admin.py:

from django.contrib.admin.filters import SimpleListFilter

class NullFilterSpec(SimpleListFilter):
    title = u''

    parameter_name = u''

    def lookups(self, request, model_admin):
        return (
            ('1', _('Has value'), ),
            ('0', _('None'), ),
        )

    def queryset(self, request, queryset):
        kwargs = {
        '%s'%self.parameter_name : None,
        }
        if self.value() == '0':
            return queryset.filter(**kwargs)
        if self.value() == '1':
            return queryset.exclude(**kwargs)
        return queryset



class StartNullFilterSpec(NullFilterSpec):
    title = u'Started'
    parameter_name = u'started'

Than just used them in ModelAdmin:

class SomeModelAdmin(admin.ModelAdmin):
    list_filter =  (StartNullFilterSpec, )
Ozonosphere answered 6/3, 2012 at 22:59 Comment(3)
In 1.4 there's BooleanFieldListFilter which will do this by default. list_filter = (('myfield', BooleanFieldListFilter), 'other_field', 'other_field2'). In non boolean fields it achieves the same effect as null/not null.Marcy
@KyleMacFarlane Doesn't appear to work for a DateTime field thoughHardshell
Using 1.6 it appears not to work for ForeignKeys either.Dynamometer
E
8

I have a simpler version of frnhr's answer, which actually filters on __isnull condition. (Django 1.4+):

from django.contrib.admin import SimpleListFilter

class NullListFilter(SimpleListFilter):
    def lookups(self, request, model_admin):
        return (
            ('1', 'Null', ),
            ('0', '!= Null', ),
        )

    def queryset(self, request, queryset):
        if self.value() in ('0', '1'):
            kwargs = { '{0}__isnull'.format(self.parameter_name) : self.value() == '1' }
            return queryset.filter(**kwargs)
        return queryset

Then also:

class StartNullListFilter(NullListFilter):
    title = u'Started'
    parameter_name = u'started'

and finally:

class SomeModelAdmin(admin.ModelAdmin):
    list_filter =  (StartNullListFilter, )

I personally don't like to trash my admin.py with dozens of classes, so I came up with such a helper function:

def null_filter(field, title_=None):
    class NullListFieldFilter(NullListFilter):
        parameter_name = field
        title = title_ or parameter_name
    return NullListFieldFilter

Which I can later apply as in:

class OtherModelAdmin(admin.ModelAdmin):
    list_filter =  (null_filter('somefield'), null_filter('ugly_field', _('Beautiful Name')), )
Effeminate answered 9/10, 2014 at 3:12 Comment(0)
M
6

there is a simple way:

class RefererFilter(admin.SimpleListFilter):
    title = 'has referer'
    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'referer__isnull'

    def lookups(self, request, model_admin):
        return (
            ('False', 'has referer'),
            ('True', 'has no referer'),
        )

    def queryset(self, request, queryset):
        if self.value() == 'False':
            return queryset.filter(referer__isnull=False)
        if self.value() == 'True':
            return queryset.filter(referer__isnull=True)

Then just used them in ModelAdmin:

class PersonAdmin(admin.ModelAdmin):
    list_filter =  (RefererFilter,) 
Macrocosm answered 4/4, 2017 at 7:17 Comment(0)
G
2

A snippet with a better explanation might be this. Django 1.4 will ship with a simplified filter mechanism.

Gorlicki answered 7/10, 2011 at 19:26 Comment(0)
K
2

I ended up using a mixture of the top solution here, along with this snippet.

However, I had to tweak the snippet slightly, dropping the field type restriction and adding the new field_path, recently added in 1.3.

from django.contrib.admin.filterspecs import FilterSpec
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _

class NullFilterSpec(FilterSpec):
    #fields = (models.CharField, models.IntegerField, models.FileField)

    @classmethod
    def test(cls, field):
        #return field.null and isinstance(field, cls.fields) and not field._choices
        return field.null and not field._choices
    #test = classmethod(test)

    def __init__(self, f, request, params, model, model_admin, field_path=None):
        super(NullFilterSpec, self).__init__(f, request, params, model, model_admin, field_path)
        self.lookup_kwarg = '%s__isnull' % f.name
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)

    def choices(self, cl):
        # bool(v) must be False for IS NOT NULL and True for IS NULL, but can only be a string
        for k, v in ((_('All'), None), (_('Has value'), ''), (_('Omitted'), '1')):
            yield {
                'selected' : self.lookup_val == v,
                'query_string' : cl.get_query_string({self.lookup_kwarg : v}),
                'display' : k
            }

# Here, we insert the new FilterSpec at the first position, to be sure
# it gets picked up before any other
FilterSpec.filter_specs.insert(0,
    # If the field has a `profilecountry_filter` attribute set to True
    # the this FilterSpec will be used
    (lambda f: getattr(f, 'isnull_filter', False), NullFilterSpec)
)
Kirstenkirsteni answered 7/10, 2011 at 19:59 Comment(0)
N
1

There has been a ticket bouncing around for this for 4 years (https://code.djangoproject.com/ticket/5833). It missed the 1.3 milestone, but has reached new feature status and presumably has found it's way into trunk. If you don't mind running off of trunk, you can use it now. The patch is supposedly 1.3 compatible, though, so you can probably get by with just patching your currently installation.

Nijinsky answered 7/10, 2011 at 19:27 Comment(0)
T
0

For, django versions less that 3.1, copy the code of EmptyFieldListFilter below

from django.contrib.admin import FieldListFilter
from django.contrib.admin.options import IncorrectLookupParameters
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.translation import gettext_lazy as _


class EmptyFieldListFilter(FieldListFilter):
    def __init__(self, field, request, params, model, model_admin, field_path):
        if not field.empty_strings_allowed and not field.null:
            raise ImproperlyConfigured(
                "The list filter '%s' cannot be used with field '%s' which "
                "doesn't allow empty strings and nulls." % (
                    self.__class__.__name__,
                    field.name,
                )
            )
        self.lookup_kwarg = '%s__isempty' % field_path
        self.lookup_val = params.get(self.lookup_kwarg)
        super().__init__(field, request, params, model, model_admin, field_path)

    def queryset(self, request, queryset):
        if self.lookup_kwarg not in self.used_parameters:
            return queryset
        if self.lookup_val not in ('0', '1'):
            raise IncorrectLookupParameters

        lookup_condition = models.Q()
        if self.field.empty_strings_allowed:
            lookup_condition |= models.Q(**{self.field_path: ''})
        if self.field.null:
            lookup_condition |= models.Q(**{'%s__isnull' % self.field_path: True})
        if self.lookup_val == '1':
            return queryset.filter(lookup_condition)
        return queryset.exclude(lookup_condition)

    def expected_parameters(self):
        return [self.lookup_kwarg]

    def choices(self, changelist):
        for lookup, title in (
            (None, _('All')),
            ('1', _('Empty')),
            ('0', _('Not empty')),
        ):
            yield {
                'selected': self.lookup_val == lookup,
                'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}),
                'display': title,
            }

You can use this to define null field value filter in admin as

import myfile

class MyAdmin(admin.ModelAdmin):
    list_filter =  (
        ("model_field", myfile.EmptyFieldListFilter),
    )
Theta answered 24/3, 2021 at 7:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.