How to customise navigation menus

In this document we discuss three different way of customising the navigation menus of django CMS sites.

  1. Menus: Statically extend the menu entries

  2. Attach Menus: Attach your menu to a page.

  3. Navigation Modifiers: Modify the whole menu tree

Menus

Create a cms_menus.py in your application, with the following:

  1. from menus.base import Menu, NavigationNode
  2. from menus.menu_pool import menu_pool
  3. from django.utils.translation import gettext_lazy as _
  4. class TestMenu(Menu):
  5. def get_nodes(self, request):
  6. nodes = []
  7. n = NavigationNode(_('sample root page'), "/", 1)
  8. n2 = NavigationNode(_('sample settings page'), "/bye/", 2)
  9. n3 = NavigationNode(_('sample account page'), "/hello/", 3)
  10. n4 = NavigationNode(_('sample my profile page'), "/hello/world/", 4, 3)
  11. nodes.append(n)
  12. nodes.append(n2)
  13. nodes.append(n3)
  14. nodes.append(n4)
  15. return nodes
  16. menu_pool.register_menu(TestMenu)

Note

Up to version 3.1 this module was named menu.py. Please update your existing modules to the new naming convention. Support for the old name will be removed in version 3.5.

If you refresh a page you should now see the menu entries above. The get_nodes function should return a list of NavigationNode instances. A menus.base.NavigationNode takes the following arguments:

title

Text for the menu node

url

URL for the menu node link

id

A unique id for this menu

parent_id=None

If this is a child of another node, supply the id of the parent here.

parent_namespace=None

If the parent node is not from this menu you can give it the parent namespace. The namespace is the name of the class. In the above example that would be: TestMenu

attr=None

A dictionary of additional attributes you may want to use in a modifier or in the template

visible=True

Whether or not this menu item should be visible

Additionally, each menus.base.NavigationNode provides a number of methods which are detailed in the NavigationNode API references.

Customise menus at runtime

To adapt your menus according to request dependent conditions (say: anonymous/logged in user), you can use Navigation Modifiers or you can make use of existing ones.

For example it’s possible to add {'visible_for_anonymous': False}/{'visible_for_authenticated': False} attributes recognised by the django CMS core AuthVisibility modifier.

Complete example:

  1. class UserMenu(Menu):
  2. def get_nodes(self, request):
  3. return [
  4. NavigationNode(_("Profile"), reverse(profile), 1, attr={'visible_for_anonymous': False}),
  5. NavigationNode(_("Log in"), reverse(login), 3, attr={'visible_for_authenticated': False}),
  6. NavigationNode(_("Sign up"), reverse(logout), 4, attr={'visible_for_authenticated': False}),
  7. NavigationNode(_("Log out"), reverse(logout), 2, attr={'visible_for_anonymous': False}),
  8. ]

Attach Menus

Classes that extend from menus.base.Menu always get attached to the root. But if you want the menu to be attached to a CMS Page you can do that as well.

Instead of extending from Menu you need to extend from cms.menu_bases.CMSAttachMenu and you need to define a name.

We will do that with the example from above:

  1. from menus.base import NavigationNode
  2. from menus.menu_pool import menu_pool
  3. from django.utils.translation import gettext_lazy as _
  4. from cms.menu_bases import CMSAttachMenu
  5. class TestMenu(CMSAttachMenu):
  6. name = _("test menu")
  7. def get_nodes(self, request):
  8. nodes = []
  9. n = NavigationNode(_('sample root page'), "/", 1)
  10. n2 = NavigationNode(_('sample settings page'), "/bye/", 2)
  11. n3 = NavigationNode(_('sample account page'), "/hello/", 3)
  12. n4 = NavigationNode(_('sample my profile page'), "/hello/world/", 4, 3)
  13. nodes.append(n)
  14. nodes.append(n2)
  15. nodes.append(n3)
  16. nodes.append(n4)
  17. return nodes
  18. menu_pool.register_menu(TestMenu)

Now you can link this Menu to a page in the Advanced tab of the page settings under attached menu.

Navigation Modifiers

Navigation Modifiers give your application access to navigation menus.

A modifier can change the properties of existing nodes or rearrange entire menus.

Example use-cases

A simple example: you have a news application that publishes pages independently of django CMS. However, you would like to integrate the application into the menu structure of your site, so that at appropriate places a News node appears in the navigation menu.

In another example, you might want a particular attribute of your Pages to be available in menu templates. In order to keep menu nodes lightweight (which can be important in a site with thousands of pages) they only contain the minimum attributes required to generate a usable menu.

In both cases, a Navigation Modifier is the solution - in the first case, to add a new node at the appropriate place, and in the second, to add a new attribute - on the attr attribute, rather than directly on the NavigationNode, to help avoid conflicts - to all nodes in the menu.

How it works

Place your modifiers in your application’s cms_menus.py.

To make your modifier available, it then needs to be registered with menus.menu_pool.menu_pool.

Now, when a page is loaded and the menu generated, your modifier will be able to inspect and modify its nodes.

Here is an example of a simple modifier that places each Page’s changed_by attribute in the corresponding NavigationNode:

  1. from menus.base import Modifier
  2. from menus.menu_pool import menu_pool
  3. from cms.models import Page
  4. class MyExampleModifier(Modifier):
  5. """
  6. This modifier makes the changed_by attribute of a page
  7. accessible for the menu system.
  8. """
  9. def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
  10. # only do something when the menu has already been cut
  11. if post_cut:
  12. # only consider nodes that refer to cms pages
  13. # and put them in a dict for efficient access
  14. page_nodes = {n.id: n for n in nodes if n.attr["is_page"]}
  15. # retrieve the attributes of interest from the relevant pages
  16. pages = Page.objects.filter(id__in=page_nodes.keys()).values('id', 'changed_by')
  17. # loop over all relevant pages
  18. for page in pages:
  19. # take the node referring to the page
  20. node = page_nodes[page['id']]
  21. # put the changed_by attribute on the node
  22. node.attr["changed_by"] = page['changed_by']
  23. return nodes
  24. menu_pool.register_modifier(MyExampleModifier)

It has a method modify() that should return a list of NavigationNode instances. modify() should take the following arguments:

request

A Django request instance. You want to modify based on sessions, or user or permissions?

nodes

All the nodes. Normally you want to return them again.

namespace

A Menu Namespace. Only given if somebody requested a menu with only nodes from this namespace.

root_id

Was a menu request based on an ID?

post_cut

Every modifier is called two times. First on the whole tree. After that the tree gets cut to only show the nodes that are shown in the current menu. After the cut the modifiers are called again with the final tree. If this is the case post_cut is True.

breadcrumb

Is this a breadcrumb call rather than a menu call?

Here is an example of a built-in modifier that marks all node levels:

  1. class Level(Modifier):
  2. """
  3. marks all node levels
  4. """
  5. post_cut = True
  6. def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
  7. if breadcrumb:
  8. return nodes
  9. for node in nodes:
  10. if not node.parent:
  11. if post_cut:
  12. node.menu_level = 0
  13. else:
  14. node.level = 0
  15. self.mark_levels(node, post_cut)
  16. return nodes
  17. def mark_levels(self, node, post_cut):
  18. for child in node.children:
  19. if post_cut:
  20. child.menu_level = node.menu_level + 1
  21. else:
  22. child.level = node.level + 1
  23. self.mark_levels(child, post_cut)
  24. menu_pool.register_modifier(Level)

Performance issues in menu modifiers

Navigation modifiers can quickly become a performance bottleneck. Each modifier is called multiple times: For the breadcrumb (breadcrumb=True), for the whole menu tree (post_cut=False), for the menu tree cut to the visible part (post_cut=True) and perhaps for each level of the navigation. Performing inefficient operations inside a navigation modifier can hence lead to big performance issues. Some tips for keeping a modifier implementation fast:

  • Specify when exactly the modifier is necessary (in breadcrumb, before or after cut).

  • Only consider nodes and pages relevant for the modification.

  • Perform as less database queries as possible (i.e. not in a loop).

  • In database queries, fetch exactly the attributes you are interested in.

  • If you have multiple modifications to do, try to apply them in the same method.