在Django Admin中过滤空值/非空值

28 投票
8 回答
16876 浏览
提问于 2025-04-17 03:53

我有一个简单的Django模型,如下所示:

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

在这个模型的ModelAdmin中,我想知道如何让它可以根据referrer是否为空来进行过滤。默认情况下,把referrer加到list_filter里会出现一个下拉框,里面列出了每一个人记录,这可能有成千上万条,导致页面根本加载不出来。即使加载出来了,我也无法按照我想要的条件进行过滤。

也就是说,我该如何修改这个设置,让下拉框只显示“全部”、“空”和“非空”这几个选项呢?

我看到一些帖子说可以通过自定义FilterSpec子类来实现类似的功能,但没有一个帖子详细说明如何使用它们。我看到的那些似乎适用于所有模型的所有字段,这我可不想要。而且,关于FilterSpec的文档几乎没有,这让我很紧张,因为我不想花很多时间在一些可能在下一个版本中就消失的内部类上写自定义代码。

8 个回答

16

由于Django 1.4对过滤器做了一些改动,我想把我刚刚花时间修改的代码分享给大家,这样可以节省一些时间,让大家在使用Django 1.4 rc1时不再遇到同样的问题。

我有一个模型,其中有一个名为“started”的时间字段(TimeField),这个字段可以为空(null=True)。我想过滤出空值和非空值,所以这和提问者的问题基本上是一样的。
那么,这里是我找到的解决办法……

在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'

然后在ModelAdmin中直接使用它们:

class SomeModelAdmin(admin.ModelAdmin):
    list_filter =  (StartNullFilterSpec, )
39

在Django 3.1之后,你可以使用EmptyFieldListFilter这个功能:

class MyAdmin(admin.ModelAdmin):
    list_filter =  (
        ("model_field", admin.EmptyFieldListFilter),
    )
2

我最后用了这里的最佳解决方案这个代码片段的结合。

不过,我稍微调整了一下这个代码片段,去掉了字段类型的限制,并添加了在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)
)

撰写回答