Lean enterprise content management powered by Django.
—
Django CMS includes a sophisticated menu framework that automatically generates navigation from page structure while allowing custom menu providers, menu modifications, and flexible template-based rendering.
Foundation classes for creating custom menu providers and modifying existing menus.
class Menu:
"""
Base class for menu providers that generate navigation nodes.
Attributes:
renderer: Menu renderer instance
request: Current HTTP request
"""
def get_nodes(self, request):
"""
Generate menu nodes for navigation.
Args:
request (HttpRequest): Current request
Returns:
list: List of NavigationNode instances
"""
class Modifier:
"""
Base class for menu modifiers that alter navigation structure.
Used to modify menu nodes after generation, such as:
- Adding CSS classes
- Filtering nodes based on permissions
- Marking active/ancestor nodes
- Adding custom attributes
"""
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
"""
Modify navigation nodes.
Args:
request (HttpRequest): Current request
nodes (list): Navigation nodes to modify
namespace (str): Menu namespace
root_id (str): Root node identifier
post_cut (bool): Whether cutting has been applied
breadcrumb (bool): Whether this is breadcrumb rendering
Returns:
list: Modified navigation nodes
"""
class NavigationNode:
"""
Represents a single navigation menu item.
Attributes:
title: Display title for menu item
url: URL for navigation link
id: Unique node identifier
parent_id: Parent node identifier for hierarchy
parent_namespace: Parent node namespace
attr: Dictionary for custom attributes
visible: Whether node should be displayed
selected: Whether node is currently selected
ancestor: Whether node is ancestor of current page
sibling: Whether node is sibling of current page
descendant: Whether node is descendant of current page
"""
def __init__(self, title, url, id, parent_id=None, parent_namespace=None, attr=None, visible=True):
"""Initialize navigation node."""
def get_descendants(self):
"""Get all descendant nodes."""
def get_ancestors(self):
"""Get all ancestor nodes."""
def get_absolute_url(self):
"""Get absolute URL for node."""Global registry for menu providers and menu processing.
class MenuPool:
"""
Menu pool for registering and managing menu providers.
Manages menu discovery, caching, and rendering coordination.
"""
def register_menu(self, menu_class):
"""
Register a menu provider.
Args:
menu_class (Menu): Menu class to register
"""
def unregister_menu(self, menu_class):
"""
Unregister a menu provider.
Args:
menu_class (Menu): Menu class to unregister
"""
def register_modifier(self, modifier_class):
"""
Register a menu modifier.
Args:
modifier_class (Modifier): Modifier class to register
"""
def get_nodes(self, request, namespace=None, root_id=None):
"""
Get navigation nodes from all registered menus.
Args:
request (HttpRequest): Current request
namespace (str, optional): Menu namespace filter
root_id (str, optional): Root node filter
Returns:
list: Combined navigation nodes
"""
# Global menu pool instance
menu_pool = MenuPool()Template tags for rendering navigation menus with extensive customization options.
# Template tag usage (in Django templates)
{% show_menu from_level to_level extra_inactive extra_active template namespace root_id %}
{% show_menu 0 100 100 100 %}
{% show_menu 0 2 0 1 "menu/custom_menu.html" %}
{% show_menu_below_id menu_id from_level to_level extra_inactive extra_active template namespace %}
{% show_menu_below_id "main-nav" 0 100 100 100 %}
{% show_sub_menu levels template namespace %}
{% show_sub_menu 1 %}
{% show_sub_menu 2 "menu/submenu.html" %}
{% show_breadcrumb from_level template namespace %}
{% show_breadcrumb 0 %}
{% show_breadcrumb 1 "menu/breadcrumb.html" %}from menus.base import Menu, NavigationNode
from menus.menu_pool import menu_pool
from django.urls import reverse
class ProductMenu(Menu):
"""Custom menu for product categories."""
def get_nodes(self, request):
"""Generate product category navigation."""
nodes = []
# Main products node
products_node = NavigationNode(
title="Products",
url=reverse("product_list"),
id="products",
attr={'class': 'products-menu'}
)
nodes.append(products_node)
# Category subnodes
from myapp.models import Category
categories = Category.objects.filter(active=True)
for category in categories:
category_node = NavigationNode(
title=category.name,
url=reverse("category_detail", args=[category.slug]),
id=f"category-{category.id}",
parent_id="products",
attr={
'class': 'category-item',
'data-category-id': category.id
}
)
nodes.append(category_node)
return nodes
# Register the custom menu
menu_pool.register_menu(ProductMenu)from menus.base import Modifier
from menus.menu_pool import menu_pool
class ActiveMenuModifier(Modifier):
"""Add CSS classes based on current page."""
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
"""Add active/current classes to menu nodes."""
current_path = request.path
for node in nodes:
# Mark exact matches as current
if node.url == current_path:
node.selected = True
if 'class' in node.attr:
node.attr['class'] += ' current'
else:
node.attr['class'] = 'current'
# Mark ancestors as active
elif current_path.startswith(node.url) and node.url != '/':
node.ancestor = True
if 'class' in node.attr:
node.attr['class'] += ' active'
else:
node.attr['class'] = 'active'
return nodes
class PermissionMenuModifier(Modifier):
"""Filter menu nodes based on user permissions."""
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
"""Remove nodes user doesn't have permission to view."""
if not request.user.is_authenticated:
# Filter out staff-only menu items
nodes = [node for node in nodes
if not node.attr.get('staff_only', False)]
return nodes
# Register modifiers
menu_pool.register_modifier(ActiveMenuModifier)
menu_pool.register_modifier(PermissionMenuModifier){% load menu_tags %}
<!-- Main navigation menu -->
<nav class="main-menu">
{% show_menu 0 2 100 100 "menu/main_navigation.html" %}
</nav>
<!-- Sidebar submenu -->
<aside class="sidebar">
<h3>Section Menu</h3>
{% show_sub_menu 1 "menu/sidebar_menu.html" %}
</aside>
<!-- Breadcrumb navigation -->
<div class="breadcrumb">
{% show_breadcrumb 0 "menu/breadcrumb.html" %}
</div>
<!-- Footer menu -->
<footer>
{% show_menu_below_id "footer" 0 1 0 0 "menu/footer_menu.html" %}
</footer><!-- menu/main_navigation.html -->
{% load menu_tags %}
{% for child in children %}
<li class="nav-item {% if child.selected %}current{% endif %} {% if child.ancestor %}active{% endif %}">
<a href="{{ child.url }}"
class="nav-link"
{% if child.attr.class %}class="{{ child.attr.class }}"{% endif %}>
{{ child.title }}
</a>
{% if child.children %}
<ul class="dropdown-menu">
{% show_menu_below_id child.id 0 1 0 0 "menu/dropdown_item.html" %}
</ul>
{% endif %}
</li>
{% endfor %}<!-- menu/breadcrumb.html -->
{% load menu_tags %}
<ol class="breadcrumb">
{% for ancestor in ancestors %}
<li class="breadcrumb-item">
<a href="{{ ancestor.url }}">{{ ancestor.title }}</a>
</li>
{% endfor %}
{% if current %}
<li class="breadcrumb-item active" aria-current="page">
{{ current.title }}
</li>
{% endif %}
</ol>from menus.menu_pool import menu_pool
from django.http import HttpRequest
# Get menu nodes programmatically
request = HttpRequest()
request.user = some_user
request.path = '/products/category-1/'
# Get all menu nodes
all_nodes = menu_pool.get_nodes(request)
# Get nodes for specific namespace
product_nodes = menu_pool.get_nodes(request, namespace="products")
# Process nodes
for node in all_nodes:
print(f"Node: {node.title} -> {node.url}")
if node.selected:
print(f" Current page: {node.title}")
if node.ancestor:
print(f" Ancestor: {node.title}")
# Get node hierarchy
descendants = node.get_descendants()
ancestors = node.get_ancestors()
print(f" Descendants: {len(descendants)}")
print(f" Ancestors: {len(ancestors)}")# settings.py menu configuration
CMS_MENUS = {
'product_menu': {
'name': 'Product Navigation',
'enabled': True,
'cache_timeout': 3600,
}
}
# Custom menu with caching
class CachedProductMenu(Menu):
"""Product menu with custom caching."""
def get_nodes(self, request):
cache_key = f"product_menu_{request.LANGUAGE_CODE}"
nodes = cache.get(cache_key)
if nodes is None:
nodes = self._generate_nodes(request)
cache.set(cache_key, nodes, timeout=3600)
return nodes
def _generate_nodes(self, request):
"""Generate fresh menu nodes."""
# Implementation here
passclass MenuRenderer:
"""
Menu rendering engine.
Handles menu node processing, template rendering,
and caching coordination.
"""
def render(self, context, nodes, template):
"""Render menu nodes with template."""
class MenuModifierPool:
"""Registry for menu modifiers."""
def register_modifier(self, modifier_class):
"""Register menu modifier."""
def get_modifiers(self):
"""Get all registered modifiers."""Install with Tessl CLI
npx tessl i tessl/pypi-django-cms