2

Does anyone have or know of a recipe (sample code and/or instructions) on setting up many-to-many relationships between different Page models? If I have PersonPage and SitePage models, how do I connect the pages (a person can work at multiple sites and a site can have multiple people working there)?

Here's what I've found related to, but not directly on, this topic—

  • Wagtail docs: from a search for "many-to-many" the only hit is in the section on the taggit module (Recipes page).

  • Wagtail docs: the only reference to the ParentalManyToManyField is a demo of how it can be used to create M2Ms between pages and categories (Tutorial)

  • This 2015 post on M2M relationships in Wagtail (it's referenced in an SO 'answer' to basically the same question I'm asking here). Although it doesn't discuss page-page relationships the approach presented might be adapted to work. My modified imitation failed with various errors depending on how I tried to set up the InlinePanel call — but the sample code from the post fails in just the same ways, so either it wasn't tested or it's been made obsolete in 2.x.

    class PersonPage(Page):
        pass
    
    PersonPage.content_panels =  [
        InlinePanel('ps_links', label='PS Links'), 
    ]   
    
    class PersonSitePageLink():
        spage = models.ForeignKey('SitePage', on_delete=models.SET_NULL, related_name='sites')
        ppage = ParentalKey('PersonPage', related_name='ps_links', on_delete=models.SET_NULL,)
        panels = [
            FieldPanel('spage')
        ]

    class SitePage(Page):
        pass
  • This technique works fine for relating a Page model to itself, but expanding it to encompass two distinct models creates two parallel but unconnected sets of relationships (you can pick arbitrary Bug pages to link to any Plant page, or vice versa, but the Plants you picked don't show when you edit Bugs). I see why in the code, I think, but I don't see how to make a single M2M connection between the two pages.
    class PlantPage(Page):
        related_bugs = ParentalManyToManyField('BugPage', blank=True)
        content_panels = Page.content_panels + [
            FieldPanel('related_bugs'),
        ]   

    class BugPage(Page):
        related_plants = ParentalManyToManyField('PlantPage', blank=True)
        content_panels = Page.content_panels + [
            FieldPanel('related_plants'),
        ]   
  • This one also only talks about intra-page model (rather than inter-page model) M2Ms. (It is pre-ParentalManyToManyField and in fact only available from the Wayback Machine.)
Joan Eliot
  • 267
  • 1
  • 8

1 Answers1

4

I hope this helps, I took inspiration from this article about moving from ParentalManyToManyField to a central model that 'links' each page from this AccordBox article.

It turns out that InlinePanel does not fully support ParentalManyToManyField, hence the issues you were running into.

I was able to implement a refined approach to your option one above and it should solve your problem.

A reminder that all Page models already extend ClusterableModel so there is no need to add that to any models you create.

Overview

  • Create a new 'relation' that extends models.Model which will be the relation between these two page models.
  • Each field within this new model will be the two page types via the model-cluster ParentalKey each with a logical related_name set that is the OTHER side of the relationship.
  • No need to set panels on this model as we will declare the panels individually via the panels kwarg to InlinePanel - see the InlinePanel docs.
  • Finally, each individual Page's content_panels has an InlinePanel added that refers to the central relation model indirectly via that model's related_name, adding the other side reference to PageChooserPanel.

Example Code


from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel

class PersonPageSitePageRelation(models.Model):
    person = ParentalKey('app.PersonPage', on_delete=models.CASCADE, related_name='sites')
    site = ParentalKey('app.SitePage', on_delete=models.CASCADE, related_name='people')
    # Optional: some additional fields (e.g. 'note') for this relation
    # Important: NOT setting any `panels` here, will be set individually for each 'direction'

    class Meta:
        unique_together = ('person', 'site')


class PersonPage(Page):
    # ... fields (note: `sites` does NOT need to be declared as a field)
    
    # Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `sites`, but the panels available will be the PersonPageSitePageRelation's field `site`
    content_panels = Page.content_panels + [
        # ... other FieldPanel etc
        InlinePanel('sites', label='Related Sites', [PageChooserPanel('site')]),
    ]


class SitePage(Page):
    # ... fields (note: `people` does NOT need to be declared as a field)

    # Now we add an `InlinePanel` that will connect to the parental connection to PersonPageSitePageRelation via the related name `people`, but the panels available will be the PersonPageSitePageRelation's field `person`
    content_panels = Page.content_panels + [
        # ... other FieldPanel etc
        InlinePanel('people', label='Related People', panels=[PageChooserPanel('person')]),
    ]

Further Reading

LB Ben Johnston
  • 4,751
  • 13
  • 29
  • This answer works! You can start from a page on either side of the relation and choose (add as related) pages of the other model (any number of them). When you edit any of those related pages, the page you started from is now shown as related, as expected. I added PageChooserPanels for both page types to the 'through' model (in the example above, 'PersonPageSitePageRelation') and registered that model as a snippet. That lets you see and edit the related page pairs, but just one pair at a time--not sure that's very helpful. – Joan Eliot Feb 17 '21 at 18:05
  • Still to work out: how to filter the list of pages you see when adding related pages to exclude those you've already added/set as related. – Joan Eliot Feb 17 '21 at 18:07
  • Hey Joan. To filter options available to the PageChooserPanel that way may not be easy. But the unique together constraint should block the user from actually saving the exact same relationship pairs though. – LB Ben Johnston Feb 17 '21 at 22:09
  • Then shall we rest here and rely upon 'unique together,' until at least our many higher-priority to-dos have been done. Thanks for the note. – Joan Eliot Feb 19 '21 at 14:06