The Third Bear

Just Right.

Letting editors choose a template for a Wagtail page

egj wagtail

Each wagtail Page class has a “template” attribute which tells the system what template (as a string identifier) should be loaded when rendering instances of that Page.  As Serafeim Papastefanos writes in his Wagtail tutorial:

A normal Django template can be used to display each page type. Wagtail either generates automatically (by seperating underscors with capital letters in camelcase for instance BlogIndexPage -> blog_index_page) or you can use the template class attribute. Let’s add a templates foler within the tutorial foler and add another folder named tutorial inside templates and ten add a file named blog_index_page.html to templates with the following content (you must have the following hierarchy wagtailtutorial/tutorial/templates/tutorial/blog_index_page.html):

This lets you specify a custom template per page type.  But what if you want to allow administrators to choose from a set of templates for each page?

The Page.template attribute is only accessed when rendering a page instance, so it’s possible to set up a dynamic template per page instance using a property, for example:

class CustomPage(Page):

    template_string = models.CharField(max_length=255, choices=(
                                         (”myapp/default.html”, “Default Template”), 
                                         (”myapp/three_column.html”, “Three Column Template”,
                                         (”myapp/minimal.html”, “Minimal Template”)))
    @property
    def template(self):
        return self.template_string

Now your editors can dynamically set the chosen template for each page instance.

Of course in this example, you could also just define a “template” CharField directly in the model, instead of using a property.  But using a property allows you to do even more dynamic things like referencing a foreign key to a database-backed template class that admins can edit through the web as a Wagtail Snippet, or inspecting the page object’s attributes and returning a different template based on which ones are filled in, etc.

Interactions with Abstract Models 

One caveat, though.   Wagtail uses some metaclass magic to add the default “{{ app-name }}/{{ page-type }}.html” template attribute to each class on initialization, by inspecting the existing class attributes and only adding the default value if the class itself doesn’t provide a “template” attribute:

class PageBase(models.base.ModelBase):
    “”"Metaclass for Page”"”
    def __init__(cls, name, bases, dct):
        super(PageBase, cls).__init__(name, bases, dct)
        […]
        if ‘template’ not in dct:
            # Define a default template path derived from the app name and model name
            cls.template = “%s/%s.html” % (cls._meta.app_label, camelcase_to_underscore(name))

(That’s in wagtail/wagtailcore/models.py)

If you’re using a class inheritance hierarchy for multiple page types which all share a common abstract base — a pattern that’s encouraged in the wagtaildemo source — then you’ll need to make sure you define your custom “template” property on each concrete subclass, rather than on the abstract base class.  It seems that when you define it on the base class, the “template” doesn’t appear in the dct received by that PageBase.__init__ metaclass method, so wagtail will end up overriding your customization.

In case that’s not clear — don’t do this, it won’t work:

class BasePage(Page):
    template_string = models.CharField(max_length=255)

    class Meta:
      abstract = True

    @property
    def template(self):
        return self.template_string


class BlogPage(BasePage):
    author = models.CharField(max_length=255)


class EventPage(BasePage):
    date = models.DateField()
    location = models.CharField(max_length=255)

Instead you’ll need to do something like this, violating DRY:

class BasePage(Page):
    template_string = models.CharField(max_length=255)

    class Meta:
      abstract = True


class BlogPage(BasePage):

    author = models.CharField(max_length=255)

    @property
    def template(self):
        return self.template_string


class EventPage(BasePage):
    date = models.DateField()
    location = models.CharField(max_length=255)

    @property
    def template(self):
        return self.template_string

There’s probably a way around this with even more metaclass magic, but this is good enough for me.