Django Admin:两个ListFilter,跨越多值关系



我有一个Blog模型和一个Entry模型,按照django文档中的例子。

条目有一个到Blog的外键:一个Blog有几个条目。

我有两个FieldListFilters for Blog:一个用于"条目标题",一个用于"条目发布年份"。

如果在博客列表管理页面中,我过滤了entry__title='Lennon'entry__published_year=2008,那么我将看到所有至少有一个标题为"列侬"的博客。至少有一个是2008年的。它们不一定是相同的条目

然而,那不是我想要的。我想要过滤的是那些条目标题都是"列侬"的博客。是2008年的

例如,我有这样的数据:

tbody><B

正如你指出的,最根本的问题是django通过一系列过滤器来构建查询集,一旦过滤器"在";查询集,它不容易改变,因为每个过滤器建立查询集的Query对象。

然而,这不是不可能的。这个解决方案是通用的,不需要你的模型/领域的知识,但可能只适用于SQL后端,使用非公共api(虽然在我的经验中,这些内部api在django是相当稳定的),它可能会变得时髦,如果你使用其他自定义FieldListFilter。这是我能想到的最好的名字:

from django.contrib.admin import (
FieldListFilter,
AllValuesFieldListFilter,
DateFieldListFilter,
)
def first(iter_):
for item in iter_:
return item
return None

class RelatedANDFieldListFilter(FieldListFilter):
def queryset(self, request, queryset):
# clone queryset to avoid mutating the one passed in
queryset = queryset.all()
qs = super().queryset(request, queryset)
if len(qs.query.where.children) == 0:
# no filters on this queryset yet, so just do the normal thing
return qs
new_lookup = qs.query.where.children[-1]
new_lookup_table = first(
table_name
for table_name, aliases in queryset.query.table_map.items()
if new_lookup.lhs.alias in aliases
)
if new_lookup_table is None:
# this is the first filter on this table, so nothing to do.
return qs
# find the table being joined to for this filter
main_table_lookup = first(
lookup
for lookup in queryset.query.where.children
if lookup.lhs.alias == new_lookup_table
)
assert main_table_lookup is not None
# Rebuild the lookup using the first joined table, instead of the new join to the same
# table but with a different alias in the query.
#
# This results in queries like:
#
#   select * from table
#   inner join other_table on (
#       other_table.field1 == 'a' AND other_table.field2 == 'b'
#   )
#
# instead of queries like:
#
#   select * from table
#   inner join other_table other_table on other_table.field1 == 'a'
#   inner join other_table T1 on T1.field2 == 'b'
#
# which is why this works.
new_lookup_on_main_table_lhs = new_lookup.lhs.relabeled_clone(
{new_lookup.lhs.alias: new_lookup_table}
)
new_lookup_on_main_table = type(new_lookup)(new_lookup_on_main_table_lhs, new_lookup.rhs)
queryset.query.where.add(new_lookup_on_main_table, 'AND')
return queryset

现在您可以创建FieldListFilter子类并将其混合,我刚刚完成了示例中您想要的那些:

class RelatedANDAllValuesFieldListFilter(RelatedANDFieldListFilter, AllValuesFieldListFilter):
pass

class RelatedANDDateFieldListFilter(RelatedANDFieldListFilter, DateFieldListFilter):
pass

@admin.register(Blog)
class BlogAdmin(admin.ModelAdmin):
list_filter = (
("entry__pub_date", RelatedANDDateFieldListFilter),
("entry__title", RelatedANDAllValuesFieldListFilter),
)

解决方案

from django.contrib import admin
from django.contrib.admin.filters import AllValuesFieldListFilter, DateFieldListFilter
from .models import Blog, Entry

class EntryTitleFilter(AllValuesFieldListFilter):
def expected_parameters(self):
return []

class EntryPublishedFilter(DateFieldListFilter):
def expected_parameters(self):
# Combine all of the actual queries into a single 'filter' call
return [
"entry__pub_date__gte",
"entry__pub_date__lt",
"entry__pub_date__isnull",
"entry__title",
"entry__title__isnull",
]
@admin.register(Blog)
class BlogAdmin(admin.ModelAdmin):
list_filter = (
("entry__pub_date", EntryPublishedFilter),
("entry__title", EntryTitleFilter),
)

工作原理

  1. 在底层,当一个过滤器被启动时,django循环遍历查询参数(来自请求),如果它们在过滤器的"期望参数"中,它将它们存储在一个名为self.used_parameters的字典中。
  2. EmptyFieldListFilter外的所有内置列表过滤器都继承FieldListFilterqueryset方法。这个方法只执行queryset.filter(**self.used_parameters)
  3. 因此,通过覆盖expected_parameters方法,我们可以控制每个过滤器应用时发生的事情。在这种情况下,我们在entry-published过滤器中执行所有实际的过滤。

Django applylist_filter sequential查询queryset()列表筛选器类的方法。如果returnqueryset不是None然后django赋值queryset = modified_queryset_by_the_filter

我们可以使用这些点。

我们可以为它创建两个自定义类过滤器

第一个EntryTitleFilterqueryset()方法返回的类.

<第二strong>MyDateTimeFilter类,我们在其中访问两个过滤器类的查询参数,然后根据我们的需求应用。

from django.contrib.admin.filters import DateFieldListFilter
class EntryTitleFilter(admin.SimpleListFilter):
title = 'Entry Title'
parameter_name = 'title'
def lookups(self, request, model_admin):
return [(item.title, item.title) for item in Entry.objects.all()]
def queryset(self, request, queryset):
# it returns None so queryset is not modified at this time.
return None

class MyDateFilter(DateFieldListFilter):
def __init__(self, *args, **kwargs):
super(MyDateFilter, self).__init__(*args, **kwargs)
def queryset(self, request, queryset):
# access title query params
title = request.GET.get('title')
if len(self.used_parameters) and title:
# if we have query params for both filter then
start_date = self.used_parameters.get('entry__pub_date__gte')
end_end = self.used_parameters.get('entry__pub_date__lt')
blog_ids = Entry.objects.filter(
pub_date__gte=start_date,
pub_date__lt=end_end,
title__icontains=title
).values('blog')
queryset = queryset.filter(id__in=blog_ids)
elif len(self.used_parameters):
# if only apply date filter
queryset = queryset.filter(**self.used_parameters)
elif title:
# if only apply title filter
blog_ids = Entry.objects.filter(title__icontains=title).values('blog')
queryset = queryset.filter(id__in=blog_ids)
else:
# otherwise
pass
return queryset

@admin.register(Blog)
class BlogAdmin(admin.ModelAdmin):
list_filter = [EntryTitleFilter, ('entry__pub_date', MyDateFilter),]
pass

当重写queryset方法时如何:


from django.db.models import Q
def queryset(self, request, queryset):
title_filter = Q(entry_title='Lennon')
published_filter = Q(entry_published_year=2008)

return queryset.filter(title_filter | published_filter )

您可以使用像&(位与),|(位与)这样的操作符。

让跨越多值关系的过滤器在ChangeList.get_querysetqs.filter(**remaining_lookup_params)中通过在expected_parameters中返回一个空列表[]来处理。

与其他答案不同,这避免了过滤器之间的依赖关系。

过滤器的实现和使用:

from django.contrib import admin
from .models import Blog, Entry

class EntryTitleFieldListFilter(admin.AllValuesFieldListFilter):
def expected_parameters(self):
return []  # Let filters spanning multi-valued relationships be handled in ChangeList.get_queryset: qs.filter(**remaining_lookup_params)

class EntryPublishedFieldListFilter(admin.AllValuesFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
field_path = 'entry__pub_date__year'
self.lookup_kwarg = field_path
self.lookup_kwarg_isnull = '%s__isnull' % field_path
self.lookup_val = params.get(self.lookup_kwarg)
self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull)
queryset = model_admin.get_queryset(request)
self.lookup_choices = queryset.distinct().order_by(self.lookup_kwarg).values_list(self.lookup_kwarg, flat=True)
def expected_parameters(self):
return []  # Let filters spanning multi-valued relationships be handled in ChangeList.get_queryset: qs.filter(**remaining_lookup_params)

@admin.register(Blog)
class BlogAdmin(admin.ModelAdmin):
list_filter = (
('entry__title', EntryTitleFieldListFilter),
('entry__pub_date', EntryPublishedFieldListFilter),
)

ChangeList.get_querysetqs.filter(**remaining_lookup_params)代码参考:

def get_queryset(self, request):
# First, we collect all the declared list filters.
(
self.filter_specs,
self.has_filters,
remaining_lookup_params,
filters_use_distinct,
self.has_active_filters,
) = self.get_filters(request)
# Then, we let every list filter modify the queryset to its liking.
qs = self.root_queryset
for filter_spec in self.filter_specs:
new_qs = filter_spec.queryset(request, qs)
if new_qs is not None:
qs = new_qs
try:
# Finally, we apply the remaining lookup parameters from the query
# string (i.e. those that haven't already been processed by the
# filters).
qs = qs.filter(**remaining_lookup_params)

我想这就是你想要的。

entries = Entry.objects.filter(title='Lennon', published_year=2008)
blogs = Blog.objects.filter(entry__in=entries)

最新更新



  • All rights reserved © 2023 www.xiaobeizi.cn

  • 首页
博客条目标题条目年份
麦卡特尼2008
列侬2009
列侬2008