Working With Wagtail: Menus

Wagtail, a Django CMS built by Torchbox, has been gaining a lot of traction recently. And with good reason, as Sebastian pointed out in his evaluation of the options out there for adding a user- (or more likely, client-) friendly layer for adding and editing content. We’re using Wagtail on several projects, and one of the things we really enjoy about it is its ability to coexist with regular Django. You can give over as much of your site to Wagtail as you want; if you want to use it only for the blog or marketing sections of a project, that’s totally fine. It was built for that.

One thing we don’t like so much is the documentation, which can be read in its entirety in a couple hours. And hey, we get it — you’re developing a project as fast as you can, then you decide to open-source it, and it’s all bug fixes and improvements and who has the time to sit down and write comprehensive docs on all the features you built in?

Fortunately for all of us, Wagtail also made and open-sourced a demo project that shows, rather than tells, many of the cool features, tricks, and best practices that can’t (yet) be found anywhere else. But you have to go digging for that knowledge.

So in the interest of saving time and effort, I decided to write about a few of the things we’ve figured out. First up: menus.

One of the benefits of using a CMS is the ability to set up dynamic areas, like sidebars or menus, that are driven by settings and options in the user-friendly admin. Wagtail comes with a structure in place to make your menus completely dynamic and driven by its innate hierarchical structure. They don’t tell you how to do it in the docs, but it’s in the demo.

Something I’ve had a hard time getting used to in Wagtail is the lack of Django views. It makes sense — in order to do its magic, Wagtail needs to essentially commandeer your whole render() process, so it does the work your views.py would do on its own. Okay, but how do we do something like a dynamic menu that has to appear on every page and doesn’t have its own model (or view, for that matter)? You could use a context processor, but take a look at this part of wagtaildemo’s base.html:

{% block menu %}
    {% get_site_root as site_root %}
    {% top_menu parent=site_root calling_page=self %}
{% endblock %}

Oh. A template tag. Yeah, that would work too, wouldn’t it? Well, what does this template tag do?

In demo_tags.py (here’s the whole file):

# Retrieves the top menu items - the immediate children of the parent page
# The has_menu_children method is necessary because the bootstrap menu requires
# a dropdown class to be applied to a parent
@register.inclusion_tag('demo/tags/top_menu.html', takes_context=True)
def top_menu(context, parent, calling_page=None):
    menuitems = parent.get_children().filter(
        live=True,
        show_in_menus=True
    )
    for menuitem in menuitems:
        menuitem.show_dropdown = has_menu_children(menuitem)
    return {
        'calling_page': calling_page,
        'menuitems': menuitems,
        # required by the pageurl tag that we want to use within this template
        'request': context['request'],
    }

Okay, so in base.html we call this template tag with the site_root as the parent, and the current page as the calling_page (get_site_root is another template tag that returns the core.Page of the whole site, the invisible ur-page at the top of the hierarchy). Inside the tag, we call get_children(), an internal method from Treebeard, on the parent (here, the site_root), and filter all the pages that are live and have show_in_menus checked. Every Wagtail page, in its default Promote panel, has a “Show in Menus” checkbox. Turns out, that’s what this is for.

Show In Menus

We check each menuitem for children with has_menu_children(), another method in demo_tags that checks a page for live, show_in_menu children. Then we pass the menuitems to the template top_menu.html, along with the calling_page, and the request object, for… reasons.

If you look at top_menu.html, you’ll see markup for what amounts to a Bootstrap-style navbar, with this critical section:

<ul class="nav navbar-nav">
    {% for menuitem in menuitems %}
        <li class="{% if menuitem.show_dropdown %}dropdown{% endif %}{% if calling_page.url == menuitem.url %} active{% endif %}">
            {% if menuitem.show_dropdown %}
                <a data-toggle="dropdown" class="dropdown-toggle" href="#">{{ menuitem.title }} <b class="caret"></b></a>
                {% top_menu_children parent=menuitem %}
            {% else %}
                <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
            {% endif %}
        </li>
    {% endfor %}
</ul>

So we make a <li> and <a> for each menuitem (page_url is another template tag that returns the relative url of the given page). If the page has children (show_dropdown), include the dropdown class, and if the calling_page is the menu_item page, give it the “active” class. Smart, right? If the menuitem has children (show_dropdown, again), call another tag, top_menu_children(), which is the same thing as top_menu(), except it doesn’t bother looking for more children (er, grandchildren of calling_page). Its template is also much simpler:

<ul class="dropdown-menu">
    {# Include link to parent because the parent link is a drop down #}
    <li><a href="{% pageurl parent %}">{{ parent.title }}</a></li>
    {% for child in menuitems_children %}
        <li><a href="{% pageurl child %}">{{ child.title }}</a></li>
    {% endfor %}
</ul>

Pretty neat, right? By adding a handful of template tags and a couple fragments, you can implement a dynamic two-level menu that will show whatever pages you choose in the Wagtail admin. But I think the real advantage is by walking through how features like this were implemented in the wagtaildemo, we can learn a lot about how to get the most out of Wagtail in our own projects.

Image by KKoshy

Working With Wagtail: Menus

Leave a Reply

Your email address will not be published. Required fields are marked *