Pezholio
Pezholio

Reputation: 2694

Wagtail filter child pages by a ForeignKey

I'm using Wagtail, and I want to filter a selection of child pages by a Foreign Key. I've tried the following and I get the error django.core.exceptions.FieldError: Cannot resolve keyword 'use_case' into field when I try children = self.get_children().specific().filter(use_case__slug=slug):

class AiLabResourceMixin(models.Model):
  parent_page_types = ['AiLabResourceIndexPage']
  use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
  content_panels = ArticlePage.content_panels + [
    FieldPanel('use_case', widget=forms.Select())
  ]

  class Meta:
    abstract = True

class AiLabCaseStudy(AiLabResourceMixin, ArticlePage):
  pass

class AiLabBlogPost(AiLabResourceMixin, ArticlePage):
  pass

class AiLabExternalLink(AiLabResourceMixin, ArticlePage):
  pass

class AiLabResourceIndexPage(RoutablePageMixin, BasePage):
  parent_page_types = ['AiLabHomePage']
  subpage_types = ['AiLabCaseStudy', 'AiLabBlogPost', 'AiLabExternalLink']
  max_count = 1

  @route(r'^$')
  def all_resources(self, request):
    children = self.get_children().specific()

    return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
      'page': self,
      'children': children,
    })

  @route(r'^([a-z0-9]+(?:-[a-z0-9]+)*)/$')
  def filter_by_use_case(self, request, slug):
    children = self.get_children().specific().filter(use_case__slug=slug)

    return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
      'page': self,
      'children': children,
    })

I've seen this answer, but this assumes I only have one type of page I want to filter. Using something like AiLabCaseStudy.objects.filter(use_case__slug=slug) works, but this only returns AiLabCaseStudys, not AiLabBlogPosts or AiLabExternalLinks.

Any ideas?

Upvotes: 1

Views: 1029

Answers (2)

gasman
gasman

Reputation: 25292

At the database level, there is no efficient way to run the filter against all page types at once. Since AiLabResourceMixin is defined as abstract = True, this class has no representation of its own within the database - instead, the use_case field is defined separately for each of AiLabCaseStudy, AiLabBlogPost and AiLabExternalLink. As a result, there's no way for Django or Wagtail to turn .filter(use_case__slug=slug) into a SQL query, since use_case refers to three different places in the database.

A couple of possible ways around this:

  • If your data model allows, restructure it to use multi-table inheritance - this looks fairly similar to your current definition, except without the abstract = True:

    class AiLabResourcePage(ArticlePage):
      use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
    
    class AiLabCaseStudy(AiLabResourcePage):
      pass
    
    class AiLabBlogPost(AiLabResourcePage):
      pass
    
    class AiLabExternalLink(AiLabResourcePage):
      pass
    

    AiLabResourcePage will then exist in its own right in the database, and you can query its use_case field with an expression like: AiLabResourcePage.objects.child_of(self).filter(use_case__slug=slug).specific(). There'll be a small performance impact here, since Django has to pull data from one additional table to construct these page objects.

  • Run a preliminary query on each specific page type to retrieve the matching page IDs, before running the final query with specific():

    case_study_ids = list(AiLabCaseStudy.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
    blog_post_ids = list(AiLabBlogPost.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
    external_link_ids = list(AiLabExternalLink.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
    children = Page.objects.filter(id__in=(case_study_ids + blog_post_ids + external_link_ids)).specific()
    

Upvotes: 2

Dan Swain
Dan Swain

Reputation: 3108

Try:

children = self.get_children().filter(use_case__slug=slug).specific()

Upvotes: 0

Related Questions