wagtail

Wagtail many-to-many links between different Page models


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—

    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
    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'),
        ]   

Solution

  • 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

    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