按 child 页面的字段过滤 Wagtail 索引页面(user-initiated 查询)/(FieldError at ... cannot resolve keyword)

Filtering a Wagtail index page by fields of the child pages (user-initiated query) / (FieldError at ... cannot resolve keyword)

我的 Wagtail 项目本质上只是一个非常传统的列表页面,用户可以在其中浏览数据库中的项目,然后单击任何感兴趣的项目进入其详细信息页面。但是我如何允许用户根据 child 页面上的字段内容过滤 and/or 对主页上的列表进行排序?这个最普通、最普通的任务让我望而却步。

假设数据库是事物的集合。假设人们认为每个 Thing 重要的是 (a) 它被发现的年份,以及 (b) 可以找到它的国家。用户可能想要浏览所有事物,但她应该能够将列表缩小到 2019 年在立陶宛发现的那些事物。而且她应该能够按年份或按国家/地区排序。只是你的 super-standard 功能,但我找不到任何指导或自己弄明白。

到目前为止,我的模型借鉴了其他人的工作示例:

    class ThingsListingPage(Page):
    
        def things(self):
            ''' return all child pages, to start with '''
            things = self.get_children().specific() 
               # do I need 'specific' above? 
               # Is this altogether the wrong way to fetch child 
               #    pages if I need to filter on their fields?
            return things
    
        def years(self, things): # Don't need things parameter yet
            '''Return a list of years for use in queries'''
            years = ['2020', '2019', '2018',]
            return years
    
        def countries(self, things):
            '''Return a list of countries for use in queries.'''
            countries = ['Angola', 'Brazil', 'Cameroon','Dubai', 'Estonia',]
            return countries
    
        def get_context(self, request):
            context = super(ThingsListingPage, self).get_context(request)
            things = self.things()
    
            # this default sort is all you get for now
            things_to_display = things.order_by('-first_published_at')
    
            # Filters prn
            has_filter = False 
            for filter_name in ['year', 'country',]:
                filter_value = request.GET.get(filter_name)
                if filter_value:
                    if filter_value != "all":
                        kwargs = {'{0}'.format(filter_name): filter_value}
                        things_to_display = things_to_display.filter(
                            **kwargs)
                    has_filter = True
    
            page = request.GET.get('page') # is this for pagination?
    
            context['some_things'] = things_to_display
            context['has_filter'] = has_filter # tested on listings page to select header string
            context['page_check'] = page # pagination thing, I guess
            # Don't forget the data to populate filter choices on the listings page
            context['years'] = self.years(things)
            context['countries'] = self.countries(things)
    
            return context
    
    class ThingDetailPage(Page):
    
            year = models.CharField(
            max_length=4,
            blank=True,
            null=True,
        )
    
        country = models.CharField(
            max_length=50,
            blank=True,
            null=True,
        )
        CONTNT_PANELS = Page.content_panels + [
            FieldPanel('year'),
            FieldPanel('country'),
        ]
        # etc. 

列表(索引)页面的模板,仅显示过滤器控件(还需要排序控件,当然还需要列表本身):

{% extends "base.html" %}
{% block content %}
  <section class="filter controls">
    <form method="get" accept-charset="utf-8" class="filter_form">
      <ul>
        <li>
          <label>Years</label>
          <h6 class="expanded">Sub-categories</h6>
          <ul class="subfilter">
              <li>
                  <input type="radio" name="year" value="all" id="filter_year_all"
                      {% if request.GET.year == "all" %}checked="checked" {% endif %} /><label
                      for="filter_year_all">All Years</label></input>
              </li>
              {% for year in years %}
              <li>
              <input type="radio" name="year" value="{{ year }}" id="filter_year_{{ year }}"
                {% if request.GET.year == year %}checked="checked" {% endif %} /><label
                for="filter_year_{{ year }}">{{ year }}</label></input>
              </li>
              {% endfor %}
            </ul>
        </li>
        <li>
          <label>Countries</label>
          <h6 class="expanded">Sub-categories</h6>
          <ul class="subfilter">
            <li>
              <input type="radio" name="country" value="all" id="filter_country_all"
                  {% if request.GET.country == "all" %}checked="checked" {% endif %} /><label
                  for="filter_country_all">All Countries</label></input>
            </li>
            {% for country in countries %}
            <li>
                <input type="radio" name="country" value="{{ country }}" id="filter_country_{{ country|slugify }}"
                    {% if request.GET.country == country %}checked="checked" {% endif %} /><label
                    for="filter_country_{{ country|slugify }}">{{ country }}</label></input>
            </li>
            {% endfor %}
          </ul>
        </li>
      </ul>
      <input type="submit" value="Apply Filters"/>
    </form>
  </section>
{% endblock %}

以上页面模型似乎在 Wagtail 中运行良好。我创建了一个名为“Things”的 ThingsListingPage 页面和一组 child ThingDetailPage 页面,每个页面都有 'year' 和 'country' 数据。页面显示正常:Things 列表页面上的过滤器显示(当前 hard-coded)来自 ThingsListingPage 模型的年份和国家/地区项目。列表页面还列出了 child 命令页面。服务器无投诉。

但是:在制作我的过滤器 selections 并单击提交/应用过滤器按钮后,我在地址栏 (http://localhost:8000/things/ ?year=2019&country=Lithuania) 但是这个错误: /things/ 的 FieldError 无法将关键字 'year' 解析为字段。 (如果我没有 select 年过滤器但对国家/地区进行过滤,我会在 'country' 关键字上得到相同的错误。)


所以: 我应该如何更改 ThingsListingPage 模型,以便我可以过滤 child 页面字段(ThingDetailPage 页面的字段)?或者我应该采取完全不同的方法,一种更好的/everybody-knows-that's-how Wagtail 方法来对页面的 children 进行任意 user-initiated 过滤和排序操作字段?

请注意,在实际项目中,TinyThings、WildThings 等可能有不同的页面类型,因此我正在寻找一种解决方案,即使某些 child ren 没有在过滤器中使用的字段。

我也很感激你对如何完成排序操作的任何指导。

Page.get_children returns 结果作为基本页面实例,其中只有核心字段(如标题)可用 - 这是因为它无法知道 [= 的预期页面类型35=] 在进行查询时(参见 What is the difference between ChildPage.objects.child_of(self) and ParentPage.get_children()?)。添加 .specific() 将 return 结果作为更具体的页面类型,但这对过滤没有帮助,因为它在主查询 [=39= 之后作为 post-processing 步骤工作](包括应用任何过滤器)。

但是,您可以通过重新组织查询表达式来指定页面类型来解决这个问题:

things = ThingDetailPage.objects.child_of(self)

在这里,Django 知道要查询哪个特定的页面模型,因此 ThingDetailPage 的所有字段都可用于过滤。

这确实将您限制为单一页面类型,并且没有完美的解决方法 - 在数据库级别,每个页面类型都由单独的 table 处理,并且不可能有效地查询数据分布在多个 table 上。 (即使 ThingDetailPage 和 TinyThingDetailPage 都定义了 year 字段,它们在数据库中是不同的实体,因此没有一个 'year' 列可以过滤。)但是,您可以使用 multi-table inheritance 重构您的模型以适应这种情况。正如 Wagtail 本身为您提供了一个包含所有页面通用字段的基本页面模型一样,您可以定义 ThingDetailPage 以包含所有事物通用的字段(例如 year),并从中继承子类型:

class TinyThingDetailPage(ThingDetailPage):
    size = models.CharField(...)

您的 TinyThingDetailPages 随后将包含在 ThingDetailPage.objects.child_of(self) 的结果中,尽管它们只会 return 作为 ThingDetailPage 的实例编辑。同样,您可以以最具体的形式将 .specific() 添加到 return,但这不适用于过滤 - 因此您将能够过滤 ThingDetailPage 共有的字段(例如 year) 但不是 size.

@gasman 的回答非常成功。但是,@gasman 的一篇链接帖子建议的替代解决方案同样有效。

上面的@gasman 回答要求通过自己的模型访问子页面对象(请注意,您需要附加 child_of(self) 以便查询 returns 只有那些属于当前页面的页面列表页面:

    def things(self):
        ''' return all child pages, to start with '''
        things = ThingDetailPage.objects.child_of(self)
        return things

通过这种方式查询子页面模型,您就可以在没有任何前缀的情况下访问子页面的字段。因此,我的 models.py (见问题)中的 get_context 方法按原样工作。具体来说,这些行无需任何更改即可工作:

     kwargs = {'{0}'.format(filter_name): filter_value}
     things_to_display = things_to_display.filter(**kwargs)

但是,从列表页面开始并检索其子页面似乎同样有效,查询在我的问题中是这样写的:

    def things(self):
        ''' return all child pages, to start with '''
        things = self.get_children().specific()
        return things

但要使其正常工作,您必须在过滤器前加上子页面模型的名称(必须使用两个下划线将模型名称与字段名称连接起来 ).
过滤器按预期运行,并且在我添加后立即没有错误:

    kwargs = {'{0}'.format('thingdetailpage__' + filter_name): filter_value}
    things_to_display = things_to_display.filter(**kwargs)

根据这些发现,我对@gasman 给出的为什么我的原始代码不起作用的解释表示怀疑:

Adding .specific() will return the results as the more specific page types, but this won't help for filtering, since it works as a post-processing step after the main query has run (including applying any filters).

尽管如此,我添加这个答案只是为了记录一个替代解决方案,即一个似乎也有效的解决方案。我没有理由更喜欢我的问题中使用的方法。子页面字段上的过滤器现在起作用。