Django: CSS-Friendly Dynamic Navigation Menus
2009 Aug 25 - Brian Kloppenborg
One design principle I have read is to keep your navigation menu to a depth of
two or less. Following this principle, the code I will discuss below will
produce a navigation menu that will have a maximum of one pop-out for each
primary link. The key to getting the navigation menu to work is to have the
output be in the form of an HTML unordered list so that CSS can be applied to
the <ul>
and <li>
tags. Django has a filter, entitled
unordered_list
,
that can parse an array of strings (or nested arrays of strings) into an
unordered list. I copied the defaultfilters.py
from the Django source code and
removed everything except the unordered_list
function (and any necessary import
lines) and then rewrote the unordered_list
function to work on a list of
MenuItems
objects. The modifications to the unordered_list
function were few
and basically involved changing the output method from a string to a call to the
yet-to-be-written MenuItem.render()
function. A copy of that file can be found
in my
navigation_filters.py
file. Place this fine in the “navigation/templatetags” directory along with
blank file called __init__.py
so the directory will be treated as a Python
class. Just like the unordered_list
function, navigation_list
takes an array
(or a nested array), but unlike unordered_list
, navigation_list
will only
output from MenuItems objects. Now we write a template that uses the new
navigation_list
filter:
<div id="navmenu">
<ul>
<li>Navigation</li>
{% load navigation_filters %}
{% if nav_menu %}
{{ nav_menu|navigation_list }}
{% else %}
<li>No Navigation Menu</li>
{% endif %}
<li>nbsp;</li>
</ul>
</div>
With the template and filter written, now all that needs to be done is to create
the navigation module. The ideal method of storing the navigation menu would be
in a hierarchical list (see
djangosnippets, and the
django-mptt project), but the status of
the django-mptt
project seemed unnecessarily complicated for this task.
Additionally, a full hierarchical list would encourage the creation of
navigation lists more than two deep, something against design principles.
I therefore created a very simple semi-hierarchical model to store the
navigation menu that also contains the MenuItem.render()
function I mentioned
above:
from django.db import models
# Create your models here.
class MenuItem(models.Model):
ParentID = models.ForeignKey('self')
text = models.CharField(max_length=20)
url = models.CharField(max_length=200)
""" Returns the title of the news item. """
def __unicode__(self):
return self.text
def render(self):
return '<a href = "' + self.url + '">' + self.text + '</a>'</pre>
I enabled the administrative interface for this class and inserted some junk data. I then wrote a context processor to pull the necessary information from the database:
from django.template import Context
from finiteline.navigation.models import MenuItem
def navigation_context(request):
# Define a variable in which we store the output list
output_list = []
# Run a query to get the top-level objects
nav_menu = MenuItem.objects.filter(ParentID=1).order_by('text').exclude(pk=1)
# For each top level object, run a query to get the subitems.
for item in nav_menu:
sub_menu = MenuItem.objects.filter(ParentID=item.id).order_by('text')
output_list.append(item)
if sub_menu.count > 0:
sub_list = []
for subitem in sub_menu:
sub_list.append(subitem)
output_list.append(sub_list)
c = Context({'nav_menu': output_list})
return c</pre>
After including the navigation template mentioned above, I included
“navigation/base.html” in another page and opened it up in a web browser. The
output looks like the image shown to the right. After applying CSS to the <ul>
and <li>
tags, the navigation menu changes considerably and even supports
pop-out navigation!