Search code examples
pythondjangodjango-admindjango-admin-filters

Filtering Django Admin by Null/Is Not Null


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.


Solution

  • 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)
    )