我可以在 django admin 中制作 list_filter 以仅显示引用的 ForeignKeys 吗?

Posted

技术标签:

【中文标题】我可以在 django admin 中制作 list_filter 以仅显示引用的 ForeignKeys 吗?【英文标题】:Can I make list_filter in django admin to only show referenced ForeignKeys? 【发布时间】:2012-08-26 07:05:38 【问题描述】:

我有一个 django 应用程序,它有两个这样的模型:

class MyModel(models.Model):
    name = models.CharField()
    country = models.ForeignKey('Country')

class Country(models.Model):
    code2 = models.CharField(max_length=2, primary_key=True)
    name = models.CharField()

MyModel 的管理类如下所示:

class MyModelAdmin(admin.ModelAdmin):
    list_display = ('name', 'country',)
    list_filter = ('country',)
admin.site.register(models.MyModel, MyModelAdmin)

Country 表包含约 250 个国家/地区。一些MyModel 实例实际上引用了少数几个国家/地区。

问题在于 django admin 中的列表过滤器在过滤器面板中列出了所有国家/地区。在这种情况下,列出所有国家(而不仅仅是实例引用的国家)几乎违背了使用列表过滤器的目的。

是否有一些仅将MyModel 引用的国家/地区显示为列表过滤器中的选项? (我使用 Django 1.3。)

【问题讨论】:

【参考方案1】:

从 Django 1.8 开始,有一个内置的RelatedOnlyFieldListFilter,您可以使用它来显示相关国家/地区。

class MyModelAdmin(admin.ModelAdmin):
    list_display = ('name', 'country',)
    list_filter = (
        ('country', admin.RelatedOnlyFieldListFilter),
    )

对于 Django 1.4-1.7,list_filter 允许您使用 SimpleListFilter 的子类。应该可以创建一个简单的列表过滤器来列出您想要的值。

如果您无法从 Django 1.3 升级,则需要使用内部的、未记录的 FilterSpec api。 Stack Overflow 问题Custom Filter in Django Admin 应该为您指明正确的方向。

【讨论】:

感谢您的回复。计划在不久的将来迁移到 Django 1.4,因此我将推迟对该问题的任何修复。 @andi 谢谢,我已经用新信息更新了答案【参考方案2】:

我知道问题是关于 Django 1.3 的,但是你提到很快升级到 1.4。 对于像我这样正在寻找 1.4 解决方案但发现此条目的人来说,我决定展示使用 SimpleListFilter(可用 Django 1.4)的完整示例,以仅显示引用的(相关的、使用的)外键值

from django.contrib.admin import SimpleListFilter

# admin.py
class CountryFilter(SimpleListFilter):
    title = 'country' # or use _('country') for translated title
    parameter_name = 'country'

    def lookups(self, request, model_admin):
        countries = set([c.country for c in model_admin.model.objects.all()])
        return [(c.id, c.name) for c in countries]
        # You can also use hardcoded model name like "Country" instead of 
        # "model_admin.model" if this is not direct foreign key filter

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(country__id__exact=self.value())
        else:
            return queryset

# Example setup and usage

# models.py
from django.db import models

class Country(models.Model):
    name = models.CharField(max_length=64)

class City(models.Model):
    name = models.CharField(max_length=64)
    country = models.ForeignKey(Country)

# admin.py
from django.contrib.admin import ModelAdmin

class CityAdmin(ModelAdmin):
    list_filter = (CountryFilter,)

admin.site.register(City, CityAdmin)

在示例中,您可以看到两个模型 - 城市和国家。城市有国家的外键。如果您使用常规 list_filter = ('country',) 您将拥有选择器中的所有国家/地区。然而,这个 sn-p 只过滤相关国家 - 与城市至少有一种关系的国家。

来自here的原创想法。非常感谢作者。改进了类名,以便更清晰地使用 model_admin.model 而不是硬编码的模型名。

在 Django Snippets 中也有示例: http://djangosnippets.org/snippets/2885/

【讨论】:

这个例子与我正在寻找的非常接近,但有一个例外:我试图通过 UserProfile 模型上名为 user_type 的字段为 User 对象添加一个 list_filter。所以我定义了一个类,UserTypeFilter(SimpleListFilter): 但我不知道你在queryset函数的return queryset.filter(=self.value())中放了什么。 我的问题的答案在这里:***.com/questions/19187027/… 如果您需要这个出色答案的通用可重复使用版本,请在下面查看我的答案:***.com/a/29501136/304209 在您的查找方法中,您正在遍历每个城市,这是非常低效的。而不是将查询集转换为集合,您应该只返回如下内容:model_admin.model.objects.order_by('country__id').values_list('country__id', 'country__name').distinct()【参考方案3】:

我会像这样在 darklow 的代码中更改查找:

def lookups(self, request, model_admin):
    users = User.objects.filter(id__in = model_admin.model.objects.all().values_list('user_id', flat = True).distinct())
    return [(user.id, unicode(user)) for user in users]

这对数据库来说要好得多;)

【讨论】:

【参考方案4】:

从 Django 1.8 开始有:admin.RelatedOnlyFieldListFilter

示例用法为:

class BookAdmin(admin.ModelAdmin):
    list_filter = (
        ('author', admin.RelatedOnlyFieldListFilter),
    )

【讨论】:

1.8 已经到来,这个功能现在可用 @Rrrrrrrrrk 更新了我的答案,明确了当前状态;) 由于某种原因,这似乎不在 geodjango 管理员中,并且从常规管理员中提取它似乎不起作用【参考方案5】:

@andi,感谢您告知 Django 1.8 将具有此功能的事实。

我查看了它是如何实现的,并基于适用于 Django 1.7 的创建版本。这是比我之前的答案更好的实现,因为现在您可以将此过滤器与任何外键字段重用。仅在 Django 1.7 中测试,不确定它是否适用于早期版本。

这是我的最终解决方案:

from django.contrib.admin import RelatedFieldListFilter

class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
    def __init__(self, field, request, params, model, model_admin, field_path):
        super(RelatedOnlyFieldListFilter, self).__init__(
            field, request, params, model, model_admin, field_path)
        qs = field.related_field.model.objects.filter(
            id__in=model_admin.get_queryset(request).values_list(
                field.name, flat=True).distinct())
        self.lookup_choices = [(each.id, unicode(each)) for each in qs]

用法:

class MyAdmin(admin.ModelAdmin):
    list_filter = (
        ('user', RelatedOnlyFieldListFilter),
        ('category', RelatedOnlyFieldListFilter),
        # ...
    )

【讨论】:

不幸的是,在 Django 1.4 中不起作用 :( 'ForeignKey' object has no attribute 'related_field' 在 Django 1.6 上运行良好:D【参考方案6】:

伟大的@darklow 答案的通用可重复使用版本:

def make_RelatedOnlyFieldListFilter(attr_name, filter_title):

    class RelatedOnlyFieldListFilter(admin.SimpleListFilter):
        """Filter that shows only referenced options, i.e. options having at least a single object."""
        title = filter_title
        parameter_name = attr_name

        def lookups(self, request, model_admin):
            related_objects = set([getattr(obj, attr_name) for obj in model_admin.model.objects.all()])
            return [(related_obj.id, unicode(related_obj)) for related_obj in related_objects]

        def queryset(self, request, queryset):
            if self.value():
                return queryset.filter(**'%s__id__exact' % attr_name: self.value())
            else:
                return queryset

    return RelatedOnlyFieldListFilter

用法:

class CityAdmin(ModelAdmin):
    list_filter = (
        make_RelatedOnlyFieldListFilter("country", "Country with cities"),
    )

【讨论】:

【参考方案7】:

这是我对 Django 1.4 的通用和可重用实现的看法,如果你碰巧卡在那个版本。它的灵感来自built-in version that is now part of Django 1.8 及更高版本。此外,将其适应 1.5-1.7 应该是一项相当小的任务,主要是查询集方法在其中更改了名称。我已将过滤器本身放在我拥有的core 应用程序中,但您显然可以将它放在任何地方。

实施:

# myproject/core/admin/filters.py:

from django.contrib.admin.filters import RelatedFieldListFilter


class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
    def __init__(self, field, request, params, model, model_admin, field_path):
        self.request = request
        self.model_admin = model_admin
        super(RelatedOnlyFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)

    def choices(self, cl):
        limit_choices_to = set(self.model_admin.queryset(self.request).values_list(self.field.name, flat=True))
        self.lookup_choices = [(pk_val, val) for pk_val, val in self.lookup_choices if pk_val in limit_choices_to]
        return super(RelatedOnlyFieldListFilter, self).choices(cl)

用法:

# myapp/admin.py:

from django.contrib import admin
from myproject.core.admin.filters import RelatedOnlyFieldListFilter
from myproject.myapp.models import MyClass


class MyClassAdmin(admin.ModelAdmin):
    list_filter = (
        ('myfield', RelatedOnlyFieldListFilter),
    )

admin.site.register(MyClass, MyClassAdmin)

如果您稍后更新到 Django 1.8,您应该可以只更改此导入:

from myproject.core.admin.filters import RelatedOnlyFieldListFilter

到这里:

from django.contrib.admin.filters import RelatedOnlyFieldListFilter

【讨论】:

以上是关于我可以在 django admin 中制作 list_filter 以仅显示引用的 ForeignKeys 吗?的主要内容,如果未能解决你的问题,请参考以下文章

django-admin:添加额外的行总计

Django admin - 如何获取模板标签中的所有注册模型?

如何从 django admin 添加多个客户地址

在 django 中测试 admin.ModelAdmin

如何在 Django-admin 中添加自定义搜索框?

Django admin 一些有用的设置