0

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 %}
LB Ben Johnston
  • 4,751
  • 13
  • 29
ron_e_e
  • 3
  • 2
  • Can you update the question with a bit more detail, ideally the template code you are using or if you haven't gotten that far a text representation of how you are hoping the output data to look like. It appears that a StoryPage should have a value for story_paths and each value within that should have next_page which itself is another StoryPage. – LB Ben Johnston Nov 13 '21 at 07:22
  • I've posted the template code. I'm currently doing my test create/edit on admin though. For now, the admin site would be enough. – ron_e_e Nov 13 '21 at 07:49
  • Ohh ok - is the goal to ensure that the page chooser for a StoryPage only shows other 'sibling' StoryPages as an option? For example if you have Horror Story and Ghost Story each with multiple StoryPage under them a StoryPage that is a child of Horror Story should only allow you to select other StoryPages that are under the Horror Story and not Gost story? – LB Ben Johnston Nov 13 '21 at 12:14
  • Yes, that way authors don't mix up pages other stories. So even if an author has two stories that have several StoryPages, if he's editing a StoryPage of Story1 he can only see its sibling pages in the Orderable. Likewise, if he's working on StoryPage of Story2, he can only see sibling pages of StoryPage of Story2. – ron_e_e Nov 13 '21 at 17:48

1 Answers1

1

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
LB Ben Johnston
  • 4,751
  • 13
  • 29
  • Thank you so much for this explanation! It clarified how Wagtail Orderables work. I also went digging into the Wagtail code to see how it worked. – ron_e_e Nov 15 '21 at 19:11
  • Just an update: I tried this code but, like you said, I needed to get the page field of `StoryPathOrderable`. I was thinking that I could probably do that by getting the instance of the orderable and then getting the page from there. However, since the orderable has not yet been made, it returns `None`. I'm now trying to figure out a way to get the instance of the `StoryPage` to get the page and then it's siblings but the orderable doesn't seem to allow me to access that instance. – ron_e_e Nov 15 '21 at 20:43
  • I tried looking for `self.instance.page` and I got the error `story.models.StoryPathOrderable.page.RelatedObjectDoesNotExist: StoryPathOrderable has no page.` Would there be a way to send the `StoryPage` data into the orderable or get the `StoryPage` instance from within the orderable? – ron_e_e Nov 15 '21 at 20:54
  • @ron_e_e - I have updated the answer to work for InlinePanel usage and also checked a few edge cases such as creation of new pages. Hopefully that helps. If you get stuck just print out `vars(instance)` within the `on_form_bound` to see what you can access, plus you can see the full code of InlinePanel and FieldPanel here - https://github.com/wagtail/wagtail/blob/main/wagtail/admin/edit_handlers.py – LB Ben Johnston Nov 16 '21 at 21:44