Search code examples
wagtailwagtail-admin

Filter the pages available to link to another page


I'm really new to Wagtail. I've been trying to find a way to filter a the values in a chooser (PageChooserPanel). I'm building a story site where authors can create non-linear stories. I followed a blog model to build this and expanded on it. I've gotten to the point where authors can link up pages through an orderable. The problem is the orderable shows pages of other stories. Is there a way to filter out the unrelated pages. I appreciate any help on this. Thank you in advance!

Here's my code:

class StoryPath(models.Model):
    route = models.ForeignKey('story.StoryPage', on_delete=models.SET_NULL, null=True, related_name='next_path', verbose_name='Story Path')
    
    panels = [
        PageChooserPanel('route', page_type='story.StoryPage'),
        FieldPanel('correct'),
    ]

    class Meta:
        abstract = True

class StoryPathOrderable(Orderable, StoryPath):
    page = ParentalKey('story.StoryPage', on_delete=models.CASCADE, related_name='story_paths')

class StoryPage(Page):
    template = 'story/story_page.html'
    body = RichTextField()
    
    content_panels = [
        FieldPanel('title', heading='Section Title', classname='collapsible'),
        FieldPanel('body', classname='collapsible'),
        MultiFieldPanel(
            [
                InlinePanel('story_paths'),
            ],
            heading = 'Story Paths',
            classname = 'collapsible'
        )
    ]
    parent_page_type =['story.Story']
    subpage_types = []

    def __str__(self):
        return '%s' % (self.title)

class Story(Page):
    subtitle = models.CharField(max_length=250, null=True, blank=True)
    
    content_panels = Page.content_panels + [
        FieldPanel('subtitle'),
    ]

    subpage_types = ['story.StoryPage']

    def __str__(self):
        return '%s' % (self.title)

EDIT: Here's the template I'm using:

{% extends "base.html" %}
{% load static wagtailcore_tags %}

{% block body_class %}{{self.title}}{% endblock %}

{% block extra_css %}{% endblock extra_css %}

{% block content %}
    <div class="d-flex justify-content-center flex-column">
        <div class="fs-3">{{page.title}}</div>
        <div>{{page.body|richtext}}</div>
    </div>
{% endblock content %}

Solution

  • This is not easy to do with the PageChooserPanel, the one that opens a modal with a search interface, however you an achieve this goal much easier if you are happy for the field to just show a drop down of 'sibling' pages.

    A bit of an overview of how this all works;

    • When you use InlinePanel('story_paths'), this leverages a few parts of Wagtail, Django and also Django Modelcluster to set up a dynamic formset.
    • The InlinePanel allows you to create sub 'forms' for multiple additions of similar objects, in this case the objects that are created inline are the StoryPathOrderable instances.
    • When you use InlinePanel it will look for a panels attribute on the inner models being created/managed inline.
    • On your inner model you have set up PageChooserPanel('route', page_type='story.StoryPage'),.
    • The PageChooserPanel is really great for a lot of use cases but somewhat hard to customise for edge cases (as it creates a field that triggers a modal that has its own search/listing interface). It is possible to modify the results of this modals using Wagtail Hooks - see `construct_page_chooser_queryset', however this is global and does not have any ability to know 'which' page or field is requesting the linked page.
    • However, we do not have to use PageChooserPanel, you can use a basic FieldPanel which provides a dropdown of pages available across the whole application, from there we can customise this field's query much easier.
    • If you want more control over this and want to preserve the modal interface you can look at using Wagtail Generic Chooser.

    Example

    • Below we will create a custom FieldPanel that modifies the behaviour of its on_form_bound, this gets called when the form is being built up for the editor once the form is available.
    • From here we can find the field for this page listing field and modify its queryset to the desired result.
    • page = self.page will work when you are NOT using an InlinePanel as the instance will be the currently edited page.
    • However, for InlinePanel, we need to consider the case of the 'initial' form that gets prepared as a template so you can add items dynamically.
    • To handle both InlinePanel and basic field usage we can grab the current request that is bound to the instance of the custom Panel and infer the page from there.
    • Once we have access to the right Page and the field, we can modify the queryset to suit our needs, Wagtail extends the ability of querysets to add child_of and sibling_of.

    some-app/models.py

    from wagtail.admin.edit_handlers import FieldPanel
    
    
    
    class SiblingOnlyPageFieldPanel(FieldPanel):
        def on_form_bound(self):
            super().on_form_bound()
    
            field = self.form[self.field_name].field
    
            # when creating a new page, check for the parent page ID & refine field queryset
            parent_page_id = self.request.resolver_match.kwargs.get("parent_page_id", None)
            if parent_page_id:
                parent_page = Page.objects.filter(pk=parent_page_id).first()
                field.queryset = field.queryset.child_of(parent_page)
                return
    
            # when editing a page, get the ID of the page currently being edited
            page_id = self.request.resolver_match.kwargs.get("page_id", None)
            if not page_id:
                return
    
            page = Page.objects.filter(pk=page_id).first()
            if not page:
                return
    
            field = self.form[self.field_name].field
            field.queryset = field.queryset.sibling_of(page)
    
    
    
    class StoryPath(models.Model):
        route = models.ForeignKey('story.StoryPage', on_delete=models.SET_NULL, null=True, related_name='next_path', verbose_name='Story Path')
        
        panels = [
            SibingOnlyPageFieldPanel('route'), # replaced PageChooserPanel & removed `page_type` as that will no longer work for a normal FieldPanel
            FieldPanel('correct'),
        ]
    
        class Meta:
            abstract = True