djangowagtailwagtail-admin

Wagtail panel for self-referential many to many relationships with a through model


I am busy making something like a knowledge graph in wagtail.

CurriculumContentItem is a node on that graph. It has a many-to-many relationship with itself, and the through model has important fields.

I'm struggling to get this to be usable in the admin page. Please see the inline comments:

class ContentItemOrder(models.Model):
    post = models.ForeignKey(
        "CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
    )
    pre = models.ForeignKey(
        "CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
    )
    hard_requirement = models.BooleanField(default=True)

class CurriculumContentItem(Page):
    body = RichTextField(blank=True)

    prerequisites = models.ManyToManyField(
        "CurriculumContentItem",
        related_name="unlocks",
        through="ContentItemOrder",
        symmetrical=False,
    )

    content_panels = Page.content_panels + [
        # FieldPanel("prerequisites") 
        # FieldPanel just lets me select CurriculumContentItems, but I need to access fields in the through model

        # InlinePanel("prerequisites"), 
        # This causes a recursion error

        FieldPanel('body', classname="full collapsible"),
    ]

If I wanted to do this in the normal Django admin I would make use of an inlines to specify prerequisites. Something like:

class ContentItemOrderPostAdmin(admin.TabularInline):
    model = models.ContentItem.prerequisites.through
    fk_name = "post"

class ContentItemOrderPreAdmin(admin.TabularInline):
    model = models.ContentItem.unlocks.through
    fk_name = "pre"

Is there a similar mechanism in Wagtail?

It looks like I need to create a custom Panel for this.


Solution

  • This works:

    1. Make the through model inherit from Orderable
    2. Make use of ParentalKey instead of ForeignKey
    3. Use InlinePanel referring to the related names of the fields in the through models
    from modelcluster.fields import ParentalKey
    from wagtail.core.models import Page, Orderable
    
    
    class ContentItemOrder(Orderable):   ### 1
        post = ParentalKey(   ### 2
            "CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
        )
        pre = ParentalKey(   ### 2
            "CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
        )
        hard_requirement = models.BooleanField(default=True)
    
        panels = [
            PageChooserPanel('pre'),
            PageChooserPanel('post'),
            FieldPanel('hard_requirement'),
        ]
    
    
    class CurriculumContentItem(Page):
        body = RichTextField(blank=True)
    
        prerequisites = models.ManyToManyField(
            "CurriculumContentItem",
            related_name="unlocks",
            through="ContentItemOrder",
            symmetrical=False,
        )
    
        content_panels = Page.content_panels + [
            InlinePanel('pre_ordered_content', label="prerequisites"),  ### 3
            InlinePanel('post_ordered_content', label="unlocks"),       ### 3
    
            FieldPanel('body', classname="full collapsible"),
        ]
    

    I was worried that there would be 2 PageChooser fields available per inline but wagtail is clever (and magical) enough that it just draws the one we need