On most of the websites that I've built with Django, I have had a desire to be able to manage little elements of the website from the Django administration screen without having to touch my templates. My intent is for the templates to become the presentation vehicle, with anything that matters being built out of the Django databases.
One such thing that I want to keep out of my templates is navigation. Sure, the template has a place for navigation (including an empty <ul>), but the contents of my navigation bars are driven by a dynamic Django application.
The application has but two files: the models and a template tag.
First up is the model file, menu/models.py:
from django.db import models
class Menu(models.Model):
name = models.CharField(maxlength=100)
slug = models.SlugField()
base_url = models.CharField(maxlength=100, blank=True, null=True)
description = models.TextField(blank=True, null=True)
class Admin:
pass
def __unicode__(self):
return "%s" % self.name
def save(self):
"""
Re-order all items at from 10 upwards, at intervals of 10.
This makes it easy to insert new items in the middle of
existing items without having to manually shuffle
them all around.
"""
super(Menu, self).save()
current = 10
for item in MenuItem.objects.filter(menu=self).order_by('order'):
item.order = current
item.save()
current += 10
class MenuItem(models.Model):
menu = models.ForeignKey(Menu)
order = models.IntegerField()
link_url = models.CharField(maxlength=100, help_text='URL or URI to the content, eg /about/ or http://foo.com/')
title = models.CharField(maxlength=100)
login_required = models.BooleanField(blank=True, null=True)
class Admin:
pass
def __unicode__(self):
return "%s %s. %s" % (self.menu.slug, self.order, self.title)
Next is a template tag that builds named or path-based menus - menu/templatetags/menubuilder.py:
from menu.models import Menu, MenuItem
from django import template
register = template.Library()
def build_menu(parser, token):
"""
{% menu menu_name %}
"""
try:
tag_name, menu_name = token.split_contents()
except:
raise template.TemplateSyntaxError, "%r tag requires exactly one argument" % token.contents.split()[0]
return MenuObject(menu_name)
class MenuObject(template.Node):
def __init__(self, menu_name):
self.menu_name = menu_name
def render(self, context):
current_path = template.resolve_variable('request.path', context)
user = template.resolve_variable('request.user', context)
context['menuitems'] = get_items(self.menu_name, current_path, user)
return ''
def build_sub_menu(parser, token):
"""
{% submenu %}
"""
return SubMenuObject()
class SubMenuObject(template.Node):
def __init__(self):
pass
def render(self, context):
current_path = template.resolve_variable('request.path', context)
user = template.resolve_variable('request.user', context)
menu = False
for m in Menu.objects.filter(base_url__isnull=False):
if m.base_url and current_path.startswith(m.base_url):
menu = m
if menu:
context['submenu_items'] = get_items(menu.slug, current_path, user)
context['submenu'] = menu
else:
context['submenu_items'] = context['submenu'] = None
return ''
def get_items(menu, current_path, user):
menuitems = []
for i in MenuItem.objects.filter(menu__slug=menu).order_by('order'):
current = ( i.link_url != '/' and current_path.startswith(i.link_url)) or ( i.link_url == '/' and current_path == '/' )
if not i.login_required or ( i.login_required and user.is_authenticated() ):
menuitems.append({'url': i.link_url, 'title': i.title, 'current': current,})
return menuitems
register.tag('menu', build_menu)
register.tag('submenu', build_sub_menu)
Using this menu system is relatively easy:
-
Filesystem setup
- Create a directory called
menu
in your Python path - Drop
models.py
(above) into themenu
folder - Create a directory called
templatetags
inside themenu
folder - Copy the
menuubilder.py
(above) into thetemplatetags
folder - Create a blank file called
__init__.py
and put a copy in each of yourmenu
andtemplatetags
folders
- Create a directory called
-
Django setup
- Add
menu
to theINSTALLED_APPS
list in yoursettings.py
- Run
./manage.py syncdb
to create the relevant database tables
- Add
-
Data setup (using Django's Admin tools)
- Add a
Menu
object for each menu set you wish to use. Give static menus (eg, those that are the same on each page) a slug such as main, footer or sidebar. For dynamic menus, that display different contents on different pages, add a base URL. A menu with a base URL or /about/ might contain links to your philosophy, your team photos, your history, and a few policies - but only when the user is visiting a page within /about/.
- Add a
-
Template setup
- In your template, wherever you want to use a particular named menu, add this code:
<ul>{% load menubuilder %}{% menu main %}
{% for item in menuitems %}<li><a href="{{ item.url }}" title="{{ item.title|escape }}"{% if item.current %} class='current'{% endif %}>{{ item.title }}</a></li>
{% endfor %}
</ul>
2. Replace **main** with the name of a static menu (eg **footer** or **sidebar** from the above example)
3. For dynamic URL-based menus, add this code:
{% load menubuilder %}{% submenu %}{% if submenu %}
<div id='subnav'>
<h2>{{ submenu.name }}</h2>
<ul>
<li>» <a href="{{ submenu.base_url }}" title="{{ submenu.name|escape }}"{% ifequal submenu.base_url request.path %} class='current'{% endifequal %}>{{ submenu.name }}</a></li>
{% for item in submenu_items %}<li>» <a href="{{ item.url }}" title="{{ item.title|escape }}"{% if item.current %} class='current'{% endif %}>{{ item.title }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
4. If a menu has been set up with a BASE URI that the user is currently seeing, it will be displayed. If no such menu exists, nothing will be displayed.
I use named menus for link bars in header and footers. I use URI-based menus to display a sub-section navigation, for example within the about area on a website or within the products section. A URI-based menu will only be displayed with the user is within that URI.
So there you have it. Database driven menus that let you build easy static or URI-based menus and submenus. If you look carefully there is code to only display links to logged in users if required, and the page that is currently being viewed is tagged with the current
CSS class so it can be easily styled.
If all of that is confusing, stay tuned for my Basic Django-powered website in a box that will pull together a number of elements such as this into a simple to install package you can use to get a website up and running quicksmart - with the fantasmical automated Admin screens to control it all.