Charles Smith
Charles Smith

Reputation: 3289

Creating Django Wagtail Sidebar with Index Page Child Links

I'm building a site using Django Wagtail and am having trouble figuring out how to add a sidebar menu that will list all the child pages of the parent index page. For example, I have a standard_index_page.html that I created a parent page with in Admin, then I added child pages of that using standard_page.html template.

In my standard_index_page.html template, I have the following code

{% standard_index_listing calling_page=self %}

and it displays all the child pages with links, but I would also like to display a list of all the child links on the child pages as well.

I hope this is making sense and someone can lend a hand. Thank you.

Upvotes: 2

Views: 2052

Answers (1)

Ian Price
Ian Price

Reputation: 7616

In essence you traverse the tree structure of your page hierarchy that is provided to Wagtail by Django-Treebeard.

Many front-end frameworks do not allow for multiple levels of menus as some consider it outside of best practices. However, with a library such as SmartMenus you can display this structure with a little elbow grease.

For my needs, there was no easy solution to this. So, while I want to share an example of how I went about this, it may be missing explanation. If you have any questions, I'd be happy to answer them.

I struggled with this for awhile and while there may be easier methods to traverse the tree, I built the following method as my needs expanded. It allows us to traverse all live pages in our site, check when the current page is being rendered in the menu, and allows for fine-grained control over rendering.

Here's what we're going to do:

  • Create a few template tags that will get the site root of the current site, loop through direct children of the site root, and loop through any lower levels of children, while looping through children of the current menuitem when discovered at each level.

In your base template, this means we need to:

  • {% load demo_tags %} to import our custom template tags
  • Call {% top_menu calling_page=self %} to get and render all direct children of the site root. These are items that would be shown across a standard menu bar.
  • Call {% top_menu_children parent=menuitem %} within the template rendered by {% top_menu %} to get and render all second- and lower-level children pages. This encompasses all menu items to be shown when hovering on the parents menu item.

Here's the custom demo_tags.py file I created to traverse all levels of the page hierarchy. The beauty of this is that it does not require any custom context data to be supplied; it works out of the box with Wagtail!

@register.assignment_tag(takes_context=True)
def get_site_root(context):
    ''' 
    Returns a core.Page, not the implementation-specific model used
    so object-comparison to self will return false as objects would differ  
    '''
    return context['request'].site.root_page


def has_menu_children(page):
    '''
    Returns boolean of whether children pages exist to the page supplied
    '''   
    return page.get_children().live().in_menu().exists()    


@register.inclusion_tag('info_site/tags/top_menu.html', takes_context=True)
def top_menu(context, parent, calling_page=None):
    '''
    Retrieves the top menu items - the immediate children of the parent page
    The has_menu_children method is necessary in many cases. For example, a bootstrap menu requires
    a dropdown class to be applied to a parent
    '''
    root = get_site_root(context)
    try:
        is_root_page = (root.id == calling_page.id)
    except:
        is_root_page = False

    menuitems = parent.get_children().filter(
        live=True,
        show_in_menus=True
    ).order_by('title')

    for menuitem in menuitems:
        menuitem.show_dropdown = has_menu_children(menuitem)

    return {
        'calling_page': calling_page,
        'menuitems': menuitems,
        'is_root_page':is_root_page,
        # required by the pageurl tag that we want to use within this template
        'request': context['request'],
    }


@register.inclusion_tag('my_site/tags/top_menu_children.html', takes_context=True)
def top_menu_children(context, parent, sub=False, level=0):
    ''' Retrieves the children of the top menu items for the drop downs '''
    menuitems_children = parent.get_children().order_by('title')
    menuitems_children = menuitems_children.live().in_menu()

    for menuitem in menuitems_children:
        menuitem.show_dropdown = has_menu_children(menuitem)

    levelstr= "".join('a' for i in range(level)) # for indentation
    level += 1

    return {
        'parent': parent,
        'menuitems_children': menuitems_children,
        'sub': sub,
        'level':level,
        'levelstr':levelstr,
        # required by the pageurl tag that we want to use within this template
        'request': context['request'],
    }

In essence, there are three levels of pages rendered:

  • The site root is called by {% get_site_root %}
  • First-level children are called by {% top_menu %}
  • Second- and lower-level children are called by {% top_menu_children %}, which is called any time a page shown in the menu has children while rendering this tag.

In order to do this, we need to create the templates to be rendered by our top_menu and top_menu_children template tags.

Please note - these all are built for Bootstrap 3's navbar class and customized for my needs. Just customize these for your needs. The whole menu building process is called by {% top_menu_children %}, so place this tag in your base template where you want the menus rendered. Change top_menu.html to reflect the overall structure of the menu and how to render each menuitem. Change children_items.html to reflect how you want children of all top-menu items, at any depth, rendered.

my_site/tags/top_menu.html

{% load demo_tags wagtailcore_tags static %}
{% get_site_root as site_root %}

{# FOR TOP-LEVEL CHILDREN OF SITE ROOT; In a nav or sidebar, these are the menu items we'd generally show before hovering. #}

<div class="container">
    <div class="collapse navbar-collapse" id="navbar-collapse-3">
        <ul class="nav navbar-nav navbar-left">
            {% for menuitem in menuitems %}
                <li class="{% if menuitem.active %}active{% endif %}">
                    {% if menuitem.show_dropdown %}
                        <a href="{{ menuitem.url }}">{{ menuitem.title }}
                            <span class="hidden-lg hidden-md hidden-sm visible-xs-inline">
                                <span class="glyphicon glyphicon-chevron-right"></span>
                            </span>
                        </a>
                        {% top_menu_children parent=menuitem %}
                    {% else %}
                        <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
                    {% endif %}
                </li>
            {% endfor %}
        </ul>
    </div>
</div>

my_site/tags/children_items.html

{% load demo_tags wagtailcore_tags %}

{# For second- and lower-level decendents of site root; These are items not shown prior to hovering on their parent menuitem, hence the separate templates (and template tags) #}

<ul class="dropdown-menu">
    {% for child in menuitems_children %}
        {% if child.show_dropdown %}
            <li>
                <a href="{% pageurl child %}">
                    {% for i in levelstr %}&nbsp&nbsp{% endfor %}
                    {{ child.title }}
                    <span class="glyphicon glyphicon-chevron-right"></span>
                </a>
                {# On the next line, we're calling the same template tag we're rendering. We only do this when there are child pages of the menu item being rendered. #}
                {% top_menu_children parent=child sub=True level=level %}
                {# ^^^^ SmartMenus is made to render menus with as many levels as we like. Bootstrap considers this outside of best practices and, with version 3, has deprecated the ability to do so. Best practices are made to be broken, right :] #}
            </li>
        {% else %}
            <li>
                <a href="{% pageurl child %}">
                    <!-- Allows for indentation based on depth of page in the site structure -->
                    {% for i in levelstr %}&nbsp&nbsp{% endfor %}
                    {{ child.title }}
                </a>
            </li>
        {% endif %}
    {% endfor %}
</ul>

Now, in your base level template (let's assume you are using one; if not, get to it :) ) you can traverse the menu while keeping clutter cleared away to the templates used by your inclusion_tags.

my_site/base.html

<ul class="nav navbar-nav navbar-left">
    {% for menuitem in menuitems %}
        <li class="{% if menuitem.active %}active{% endif %}">
            {% if menuitem.show_dropdown %}
                <a href="{{ menuitem.url }}">{{ menuitem.title }}
                    <span class="hidden-lg hidden-md hidden-sm visible-xs-inline">
                        <span class="glyphicon glyphicon-chevron-right"></span>
                    </span>
                </a>
                {% top_menu_children parent=menuitem %}
            {% else %}
                <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
            {% endif %}
        </li>
    {% endfor %}
</ul>

I wrote a blog post about this - check it out for more details. Or, head over to Thermaline.com to see it in action, though I think there's not multiple levels of depth right now. IF THERE WERE, they'd be rendered automatically :)

Now, this example is for a navbar, but it could easily be adapted for a sidebar.

All you need to do is:

  • Include demo_tags in your base template
  • Call {% top_menu %} where you wish to render your menus.
  • Customize top_menu.html and children_items.html to render the first and then all subsequent levels of pages.

Shout out to Tivix for their post on two-level menus that was a great starting point for me!

Upvotes: 9

Related Questions