May 07 2015

Working With Wagtail: I Want My M2Ms

by Adam Lord

M2M&Ms

In my first Working With Wagtail post, I wrote about using template tags and some built-in Wagtail methods and properties to create dynamic, CMS-driven menus. I also wrote about some shortcomings in the Wagtail docs, and how the wagtaildemo repo makes up for a lot of that by showing instead of telling. (The Wagtail docs are improving, incidentally, and the versions currently in development, including 1.0, show a lot of promise.)

In addition to the lack of traditional Django views, another common source of confusion/frustration among Wagtail users is the inability to use ManyToMany fields to define more complex relationships between models. The lack of support has to do with the django-modelcluster library, which is at the very heart of Wagtail. Modelcluster is what allows you to preview pages in Wagtail before they're published, keep pages in a "draft" state, etc., without making changes to the database. Modelcluster doesn't support M2M relationships, therefore Wagtail models can't use ManyToMany fields. But that's okay.

A traditional Django ManyToMany field is great, easy shortcut. The classic example of a M2M relationship is Authors and Books: an author can write many books, and a book may be written by multiple authors. Another more relevant example might be a slideshow: a slideshow contains multiple slides, but a single slide could be used in multiple slideshows. I'm going to expand upon the slideshow example in a little bit.

The Django representation of a M2M field, as I mentioned before, is just a shortcut. Behind the scenes, Django creates an additional table (a through table) to join the two models together. In Wagtail, to create a M2M relationship between model A and model B, we just have to do this ourselves, by creating a third, intermediate model, that has foreign keys to both A and B. Each instance of this third model represents the relationship between the objects, not the objects themselves. An advantage of creating this third model is the ability to add information about the relationship.

The wagtaildemo has this all over the place, used mostly to create "carousels." Think about a homepage with a carousel of images. The same image might be used in many ways, but on the homepage, we want to use it in a slide that also includes a caption and a url the image should link to.

Essentially, we're working with three models: a HomePage model, an Image model, and a CarouselItem model. This third model is what defines the relationship between the homepage and an image, and it's where we can add the caption and url — properties that are specific to the image's inclusion on the homepage. I'll refer to this as the Relationship Model.

Wagtaildemo also makes extensive use of model inheritance and mixins, which is great, but kind of distracts from my point, so here's an example that's more realistic, but still simplified:

The HomePage model:

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

HomePage.content_panels = [
    FieldPanel('body',),
    InlinePanel(HomePage, 'carousel_items', label="Carousel items")
]

The Image model we're dealing with is just wagtailimages.Image. No need to reinvent that.

Here's the CarouselItem model:

class HomePageCarouselItem():
    image = models.ForeignKey('wagtailimages.Image')
    embed_url = models.URLField("Embed URL", blank=True)
    caption = models.CharField(max_length=255, blank=True)
    page = ParentalKey('HomePage', related_name='carousel_items')

    panels = [
        ImageChooserPanel('image'),
        FieldPanel('embed_url'),
        FieldPanel('caption'),
    ]

You'll notice we didn't define the relationship to  HomePageCarouselItem in  HomePage, nor does HomePageCarouselItem have a FK to HomePage or a panel to define its page. That's because these relationships are handled through the magic of the Wagtail panels. I'm not a fan of magic, but this is how it goes:

The HomePage InlinePanel will let us select one or more of the HomePage's "carousel_items." Where do they come from? You'll notice "carousel_item" is the related_name in HomePageCarouselItem's ParentalKey panel. When you make a  HomePageCarouselItem, you'll choose an image, enter a url, and write a caption. Then when you save this carousel item, its page field's ParentalKey relationship ties it to the HomePage model, and it makes itself available to HomePage instances under the label "carousel_items," through that InlinePanel.

Here's a super abstract, non-functional version, to point out what makes the relevant connections between models:

class ModelA():
    property = foo

    panels = [
        FieldPanel('blah')
        InlinePanel(ModelA, 'relationship_model')
    ]

class ModelB():
    property = bar

class ModelC():
    property = models.ForeignKey(ModelB)
    page = ParentalKey(ModelA, related_name='relationship_model')

    panels = [
        FieldPanel('property')
    ]

As usual, the best way to learn is to try it yourself, building, breaking, and debugging as you go. If you haven't checked out the wagtaildemo yet, I recommend keeping it (and the wagtail codebase itself) handy for reference.

Good luck!

Want more? Head back to the Tivix blog