在Django管理中,是否可以限制ManyToMany/外键关系的过滤器?

2 投票
2 回答
2362 浏览
提问于 2025-04-16 14:25

标题可能有点拗口,我知道,但我想不出更简洁的说法。下面是我的问题:

我创建了两个代理模型,分别代表“用户类型”,它们都是从django.contrib.auth.User继承而来的。每个模型都有一个自定义的管理器,用来限制查询结果,只显示属于特定组的项目。具体来说,有一个PressUser,指的是属于“Press”组的用户,还有一个StaffUser,指的是属于其他任何组但不是“Press”的用户。

问题是,当我在StaffUsers的模型管理中添加'groups'作为过滤选项时,结果显示的过滤选项包括所有可用的组,甚至包括“Press”,而不是仅仅显示StaffUsers可以访问的组。

我在网上查了一些资料,想出了一个自定义的过滤规格,应该能实现我想要的效果,但问题是,User模型的'groups'属性实际上是从Group模型应用的一个related_name。因此,我无法将我的过滤规格附加到代理模型中的'groups'上。

有没有其他方法可以应用这个过滤规格?或者,有没有更好的方法来过滤默认过滤规格返回的项目?

2 个回答

1

我一开始就告诉你,我自己从来没有做过这个,所以你可以适当参考一下。

我建议你在你的 ModelAdmin 里重写一下 get_changelist 方法,这样可以返回一个自定义的 ChangeList 类,你可以在你的 admin 模块里定义这个类。

你的自定义 ChangeList 类只需要重写一下 get_filters 方法,这样你就可以为 group 字段设置你自己的过滤器。

还有一个可能会引起你兴趣的事情是,关于自定义过滤器的功能请求 票据 中的一些补丁。最新的补丁目前还不支持 Django 1.3rc1,不过 @bendavis78 最近说他正在做一个新的补丁,具体能不能用还得看你用的 Django 版本。

看起来这个功能差一点就能在 1.3 版本中加入,所以我猜它会在 Django 1.4 开始开发时被纳入主线。

3

我自己解决了问题。对于可能遇到类似情况的人,这里是我采取的步骤:

我修改了change_list.html这个模板,手动过滤掉我不想要的项目。不过,这里需要做的改动还不少。

首先,在你的ModelAdmin里添加一个changelist_view方法:

# myproject/account/admin.py

class StaffUserAdmin(models.ModelAdmin):
    ...
    def changelist_view(self, request, extra_context=None):
        groups = Group.objects.exclude(name__in=['Press',]).values_list('name')
        extra_context = {
            'groups': [x[0] for x in groups],
        }
        return super(StaffUserAdmin, self).changelist_view(request,
            extra_context=extra_context)

基本上,我们在这里做的就是把想要使用的过滤后的组列表传递到模板的上下文中。

第二,为你的应用创建一个change_list.html模板。

# myproject/templates/admin/auth/staffuser/change_list.html

{% extends "admin/change_list.html" %}

{% load admin_list %}
{% load i18n %}
{% load account_admin %}

{% block filters %}

    {% if cl.has_filters %}
    <div id="changelist-filter">
        <h2>{% trans 'Filter' %}</h2>
        {% for spec in cl.filter_specs %}
            {% ifequal spec.title 'group' %}
                {% admin_list_group_filter cl spec groups %}
            {% else %}
                {% admin_list_filter cl spec %}
            {% endifequal %}
        {% endfor %}
    </div>
    {% endif %}

{% endblock filters %}

这个步骤需要稍微解释一下。首先,模板标签加载:admin_list是Django默认的模板标签,用于渲染过滤器,admin_list_filteri18n用于trans,而account_admin是我自定义的模板标签(稍后会讨论),admin_list_group_filter

变量spec.title保存着正在过滤的字段的标题。因为我想改变组过滤器的显示方式,所以我检查它是否等于'groups'。如果是的话,我就使用我的自定义模板标签,否则就使用Django的默认模板标签。

第三,我们创建模板标签。我基本上只是复制了Django的默认模板标签,并做了必要的修改。

# myproject/account/templatetags/account_admin.py

from django.template import Library

register = Library()

def admin_list_group_filter(cl, spec, groups):
    return {'title': spec.title, 'choices' : list(spec.choices(cl)), 'groups': groups }
admin_list_group_filter = register.inclusion_tag('admin/auth/group_filter.html')(admin_list_group_filter)

我在这里唯一改变的就是给方法添加了一个新的参数'groups',这样我就可以把之前过滤后的组列表传递进来,还添加了一个新的键到字典中,以便把这个列表传递到模板标签的上下文中。我还把标签使用的模板改成了我们现在要创建的新模板。

第四,为模板标签创建模板。

# myproject/templates/admin/auth/group_filter.html

{% load i18n %}
<h3>{% blocktrans with title as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% for choice in choices %}
    {% if choice.display in groups %}
    <li{% if choice.selected %} class="selected"{% endif %}>
        <a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
    {% endif %}
{% endfor %}
</ul>

这里没有什么大惊喜。我们只是把所有的部分组合在一起。每个choice都是一个字典,里面包含构建过滤链接所需的所有值。具体来说,choice.display保存着将被过滤的实例的实际名称。显然,我设置了一个检查,看看这个值是否在我想要显示的过滤后的组列表中,只有在它存在时才渲染链接。

所以,这个过程有点复杂,但效果非常好。就这样,你得到了一个完全符合你需求的过滤器列表,而不是Django生成的默认过滤器。

撰写回答