Django管理界面中的默认过滤器

117

我如何将默认的筛选选项从“所有”更改为其它选项?我有一个名为status的字段,它有三个值:activatependingrejected。当在Django管理后台使用list_filter时,默认情况下筛选器设置为“全部”,但我想将其默认设置为“待处理”。

18个回答

129
为了实现这个目标 并且 在您的侧边栏中有一个可用的“全部”链接(即显示所有而不是显示待处理),您需要创建一个自定义列表过滤器,继承自django.contrib.admin.filters.SimpleListFilter并默认按“待处理”进行过滤。以下代码应该可以解决问题:
from datetime import date

from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter

class StatusFilter(SimpleListFilter):
    title = _('Status')

    parameter_name = 'status'

    def lookups(self, request, model_admin):
        return (
            (None, _('Pending')),
            ('activate', _('Activate')),
            ('rejected', _('Rejected')),
            ('all', _('All')),
        )

    def choices(self, cl):
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == lookup,
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.value() in ('activate', 'rejected'):
            return queryset.filter(status=self.value())    
        elif self.value() == None:
            return queryset.filter(status='pending')


class Admin(admin.ModelAdmin): 
    list_filter = [StatusFilter] 

编辑:需要Django 1.4(感谢Simon)


5
这是所有解决方案中最干净的一个,但它的点赞数最少...尽管这应该是当然的,但它需要 Django 1.4。 - Simon
@Greg 你怎么完全删除筛选功能和筛选标签在管理页面上的显示? - user5117926
嗯...https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter - Greg
2
这个解决方案有一个小缺陷。当过滤器为空(实际上使用了“pending”过滤器)时,Django 1.8会错误地确定完整的结果计数,并且如果show_full_result_count为True(默认情况下),不会显示结果计数。 - Alexander Fedotov
3
请注意,如果您未在解决方案中覆盖choices方法,它将令人讨厌地继续在选择列表的顶部添加自己的**全部**选项。 - richard
显示剩余2条评论

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

25
这个解决方案的缺点是,虽然“所有”选项仍然显示在用户界面上,但选择它仍然会应用默认的过滤。 - akaihola
我有同样的问题,但是我能够理解回复...抱歉我刚接触Django...但也许这个方法会起作用。http://blog.dougalmatthews.com/2008/10/filter-the-django-modeladmin-set/ - Asinox
这很好,但我需要在URL中看到获取参数,以便我的过滤器可以捕获它并显示为已选。不久会发布我的解决方案。 - radtek
仅发布一段代码可能无法帮助所有人,因为缺少解释。此外,如果代码无法正常工作,并且没有一些上下文信息,很难找出原因。 - EvilSmurf

22

参考ha22109的答案并进行了修改,以允许通过比较HTTP_REFERERPATH_INFO来选择"全部"

class MyModelAdmin(admin.ModelAdmin):

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

        test = request.META['HTTP_REFERER'].split(request.META['PATH_INFO'])

        if test[-1] and not test[-1].startswith('?'):
            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)

3
由于 HTTP_REFERER 并非始终存在,导致我的代码出现了问题。我使用了 referer = request.META.get('HTTP_REFERER', ''); test = referer.split(request.META['PATH_INFO']) 进行了处理。 - ben author
@Ben 我正在使用你的两行 referer = request.META.get('HTTP_REFERER', '') 和 test = referer.split(request.META['PATH_INFO'])。我对 HTTP_REFERER 不是很了解。如果 HTTP_REFERER 不存在,这些代码能完全解决问题吗? - the_game
@the_game 是的,这个想法是如果你使用方括号尝试访问一个不存在的键,它会抛出KeyError异常,而如果你使用字典的get()方法,你可以指定一个默认值。我指定了一个空字符串作为默认值,这样split()就不会抛出AttributeError异常了。就是这样。 - ben author
@Ben,谢谢,它对我有用。另外,您能回答这个问题吗?我认为这是对这个问题的扩展:http://stackoverflow.com/questions/10410982/default-django-admin-list-filter 。您能请给我一个解决方案吗? - the_game
3
这很有效。但是 has_key() 已被弃用,推荐使用 key in d。虽然我知道你只是从 ha22109 的答案中获取了代码。有一个问题:为什么要使用 request.META['PATH_INFO'],而不是直接使用 request.path_info(更短)? - Nick
@Nick 没有真正的理由,只是因为这样可能会通过仅引用request.META字典来帮助理解代码片段。 - iridescent

22

我知道这个问题现在已经很老了,但它仍然有效。我相信这是做这件事情最正确的方法。实质上,它与Greg的方法基本相同,但以可扩展类的形式进行了表述,以便于轻松重复使用。

from django.contrib.admin import SimpleListFilter
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _

class DefaultListFilter(SimpleListFilter):
    all_value = '_all'

    def default_value(self):
        raise NotImplementedError()

    def queryset(self, request, queryset):
        if self.parameter_name in request.GET and request.GET[self.parameter_name] == self.all_value:
            return queryset

        if self.parameter_name in request.GET:
            return queryset.filter(**{self.parameter_name:request.GET[self.parameter_name]})

        return queryset.filter(**{self.parameter_name:self.default_value()})

    def choices(self, cl):
        yield {
            'selected': self.value() == self.all_value,
            'query_string': cl.get_query_string({self.parameter_name: self.all_value}, []),
            'display': _('All'),
        }
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == force_text(lookup) or (self.value() == None and force_text(self.default_value()) == force_text(lookup)),
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

class StatusFilter(DefaultListFilter):
    title = _('Status ')
    parameter_name = 'status__exact'

    def lookups(self, request, model_admin):
        return ((0,'activate'), (1,'pending'), (2,'rejected'))

    def default_value(self):
        return 1

class MyModelAdmin(admin.ModelAdmin):
    list_filter = (StatusFilter,)

12

这是我的通用解决方案,使用重定向,它只检查是否存在任何GET参数,如果不存在,则使用默认的GET参数重定向。我还设置了一个 list_filter,所以它将拾取并显示默认值。

from django.shortcuts import redirect

class MyModelAdmin(admin.ModelAdmin):   

    ...

    list_filter = ('status', )

    def changelist_view(self, request, extra_context=None):
        referrer = request.META.get('HTTP_REFERER', '')
        get_param = "status__exact=5"
        if len(request.GET) == 0 and '?' not in referrer:
            return redirect("{url}?{get_parms}".format(url=request.path, get_parms=get_param))
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

唯一的注意事项是,当您直接访问带有URL中存在“?”的页面时,没有HTTP_REFERER设置,因此它将使用默认参数并重定向。这对我来说很好,当您通过管理员过滤器进行点击时,它非常有效。

更新:

为了避免这种情况,我最终编写了一个自定义过滤器函数,简化了changelist_view功能。以下是该过滤器:

class MyModelStatusFilter(admin.SimpleListFilter):
    title = _('Status')
    parameter_name = 'status'

    def lookups(self, request, model_admin):  # Available Values / Status Codes etc..
        return (
            (8, _('All')),
            (0, _('Incomplete')),
            (5, _('Pending')),
            (6, _('Selected')),
            (7, _('Accepted')),
        )

    def choices(self, cl):  # Overwrite this method to prevent the default "All"
        from django.utils.encoding import force_text
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == force_text(lookup),
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):  # Run the queryset based on your lookup values
        if self.value() is None:
            return queryset.filter(status=5)
        elif int(self.value()) == 0:
            return queryset.filter(status__lte=4)
        elif int(self.value()) == 8:
            return queryset.all()
        elif int(self.value()) >= 5:
            return queryset.filter(status=self.value())
        return queryset.filter(status=5)

现在的 changelist_view 只有在没有参数的情况下才传递默认参数。这样做的目的是通过不使用 GET 参数来消除通用筛选器查看所有内容的能力。为了查看所有内容,我将状态(status)赋值为8。

class MyModelAdmin(admin.ModelAdmin):   

    ...

    list_filter = ('status', )

    def changelist_view(self, request, extra_context=None):
        if len(request.GET) == 0:
            get_param = "status=5"
            return redirect("{url}?{get_parms}".format(url=request.path, get_parms=get_param))
        return super(MyModelAdmin, self).changelist_view(request, extra_context=extra_context)

我有一个解决我的问题的方法,一个自定义过滤器。我将其作为另一种解决方案呈现。 - radtek
谢谢,我认为重定向是最干净和最简单的解决方案。我也不明白“警告”。无论是通过点击还是使用直接链接(我没有使用自定义过滤器),我总是得到期望的结果。 - Dennis Golomazov

6
def changelist_view( self, request, extra_context = None ):
    default_filter = False
    try:
        ref = request.META['HTTP_REFERER']
        pinfo = request.META['PATH_INFO']
        qstr = ref.split( pinfo )

        if len( qstr ) < 2:
            default_filter = True
    except:
        default_filter = True

    if default_filter:
        q = request.GET.copy()
        q['registered__exact'] = '1'
        request.GET = q
        request.META['QUERY_STRING'] = request.GET.urlencode()

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

6

创建了一个可重用的过滤器子类,灵感来自于这里一些答案(主要是 Greg 的)。

优点:

可重用性 - 可插入任何标准的 ModelAdmin 类中

可扩展性 - 易于为 QuerySet 过滤添加额外/自定义逻辑

易于使用 - 在其最基本形式中,只需要实现一个自定义属性和一个自定义方法(除了 SimpleListFilter 子类化所需的属性之外)

直观的管理界面 - 所有其他筛选器链接都像预期的那样工作

无需重定向 - 无需检查 GET 请求有效负载,与 HTTP_REFERER(或任何其他请求相关内容,在其基本形式中)无关

无视图操纵 - 也没有模板操纵(天佑我们)

代码:

(大多数 import 只是用于类型提示和异常处理)

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,
            }

完整使用示例:

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

希望这对某些人有所帮助,欢迎提供反馈。

我非常喜欢你的解决方案,特别是在设计方面,所以谢谢!然而,对我来说有些问题。当使用你的解决方案时,我似乎无法看到“全部”选项。我正在使用Django 2.2.12。我可以看到被覆盖的“choices”被调用而不是父级,并且我可以看到被覆盖的“lookups”正确地生成了所有选择。有什么提示吗? - Guy
这是我所知道的2023年最佳答案。完美运作,易于适应。谢谢! - matthewn

4
请注意,如果您想始终在在管理界面显示数据之前对其进行预过滤,而不是预先选择过滤器值,则应覆盖 ModelAdmin.queryset() 方法。

这是一个相当干净和快速的解决方案,尽管它仍然可能会引起问题。当在管理员中启用过滤选项时,用户可能会得到看似不正确的结果。如果重写的查询集包含一个 .exclude() 子句,那么被该子句捕获的记录将永远不会被列出,但管理员过滤选项仍将由管理员 UI 明确地显示。 - Tomas Andrle
有其他更正确的答案,投票较低,适用于这种情况,因为OP明确要求他将放置一个过滤器,其中查询集将是错误的解决方案,正如@TomasAndrle上面指出的那样。 - eskhool
感谢 @eskhool 指出这一点,我尝试将我的答案投票降至零,但似乎不允许自己投反对票。 - akaihola

4
您可以简单地使用return queryset.filter() 或者 if self.value() is None 并重写 SimpleListFilter 的方法来实现。
from django.utils.encoding import force_text

def choices(self, changelist):
    for lookup, title in self.lookup_choices:
        yield {
            'selected': force_text(self.value()) == force_text(lookup),
            'query_string': changelist.get_query_string(
                {self.parameter_name: lookup}, []
            ),
            'display': title,
        }

3

使用DjangoChoices、Python >= 2.5和当然是Django >= 1.4,对Greg的答案进行了轻微改进。

from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter

class OrderStatusFilter(SimpleListFilter):
    title = _('Status')

    parameter_name = 'status__exact'
    default_status = OrderStatuses.closed

    def lookups(self, request, model_admin):
        return (('all', _('All')),) + OrderStatuses.choices

    def choices(self, cl):
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == lookup if self.value() else lookup == self.default_status,
                'query_string': cl.get_query_string({self.parameter_name: lookup}, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.value() in OrderStatuses.values:
            return queryset.filter(status=self.value())
        elif self.value() is None:
            return queryset.filter(status=self.default_status)


class Admin(admin.ModelAdmin):
    list_filter = [OrderStatusFilter] 

感谢Greg提供的好解决方案!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接