How to extend the Toolbar

The django CMS toolbar provides an API that allows you to add, remove and manipulate toolbar items in your own code. It helps you to integrate django CMS’s frontend editing mode into your application, and provide your users with a streamlined editing experience.

See also

Create a cms_toolbars.py file

In order to interact with the toolbar API, you need to create a CMSToolbar sub-class in your own code, and register it.

This class should be created in your application’s cms_toolbars.py file, where it will be discovered automatically when the Django runserver starts.

You can also use the CMS_TOOLBARS to control which toolbar classes are loaded.

Use the high-level toolbar APIs

You will find a toolbar object in the request in your views, and you may be tempted to do things with it, like:

toolbar = request.toolbar
toolbar.add_modal_button('Do not touch', dangerous_button_url)

- but you should not, in the same way that it is not recommended to poke tweezers into electrical sockets just because you can.

Instead, you should only interact with the toolbar using a CMSToolbar class, and the documented APIs for managing it.

Similarly, although a generic add_item() method is available, we provide higher-level methods for handling specific item types, and it is always recommended that you use these instead.

Define and register a CMSToolbar sub-class

from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool

class MyToolbarClass(CMSToolbar):
    [...]

toolbar_pool.register(MyToolbarClass)

The cms.toolbar_pool.ToolbarPool.register method can also be used as a decorator:

@toolbar_pool.register
class MyToolbarClass(CMSToolbar):
    [...]

Populate the toolbar

Two methods are available to control what will appear in the django CMS toolbar:

  • populate(), which is called before the rest of the page is rendered
  • post_template_populate(), which is called after the page’s template is rendered

The latter method allows you to manage the toolbar based on the contents of the page, such as the state of plugins or placeholders, but unless you need to do this, you should opt for the more simple populate() method.

class MyToolbar(CMSToolbar):

    def populate(self):

        # add items to the toolbar

Now you have to decide exactly what items will appear in your toolbar. These can include:

  • menus
  • buttons and button lists
  • various other toolbar items

Create a toolbar menu

The text link items described above can also be added as nodes to menus in the toolbar.

A menu is an instance of cms.toolbar.items.Menu. In your CMSToolbar sub-class, you can either create a menu, or identify one that already exists (in order to add new items to it, for example), in the populate() or post_template_populate() methods, using get_or_create_menu().

def populate(self):
    menu = self.toolbar.get_or_create_menu(
        key='polls_cms_integration',
        verbose_name='Polls'
        )

The key is unique menu identifier; verbose_name is what will be displayed in the menu. If you know a menu already exists, you can obtain it with get_menu().

Note

It’s recommended to namespace your key with the application name. Otherwise, another application could unexpectedly interfere with your menu.

Once you have your menu, you can add items to it in much the same way that you add them to the toolbar. For example:

def populate(self):
    menu = [...]

    menu.add_sideframe_item(
        name='Poll list',
        url=admin_reverse('polls_poll_changelist')
    )

To add a menu divider

add_break() will place a Break, a visual divider, in a menu list, to allow grouping of items. For example:

menu.add_break(identifier='settings_section')

To add a sub-menu

A sub-menu is a menu that belongs to another Menu:

def populate(self):
    menu = [...]

    submenu = menu.get_or_create_menu(
        key='sub_menu_key',
        verbose_name='My sub-menu'
        )

You can then add items to the sub-menu in the same way as in the examples above. Note that a sub-menu is an instance of SubMenu, and may not itself have further sub-menus.

Finding existing toolbar items

get_or_create_menu() and get_menu()

A number of methods and useful constants exist to get hold of and manipulate existing toolbar items. For example, to find (using get_menu()) and rename the Site menu:

from cms.cms_toolbars import ADMIN_MENU_IDENTIFIER

class ManipulativeToolbar(CMSToolbar):

    def populate(self):

        admin_menu = self.toolbar.get_menu(ADMIN_MENU_IDENTIFIER)

        admin_menu.name = "Site"

get_or_create_menu() will equally well find the same menu, and also has the advantages that:

  • it can update the item’s attributes itself (self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER, 'Site'))
  • if the item doesn’t exist, it will create it rather than raising an error.

find_items() and find_first()

Search for items by their type:

def populate(self):

    self.toolbar.find_items(item_type=LinkItem)

will find all LinkItems in the toolbar (but not for example in the menus in the toolbar - it doesn’t search other items in the toolbar for items of their own).

find_items() returns a list of ItemSearchResult objects; find_first() returns the first object in that list. They share similar behaviour so the examples here will use find_items() only.

The item_type argument is always required, but you can refine the search by using their other attributes, for example:

self.toolbar.find_items(Menu, disabled=True))

Note that you can use these two methods to search Menu and SubMenu classes for items too.

Control the position of items in the toolbar

Methods to add menu items to the toolbar take an optional position argument, that can be used to control where the item will be inserted.

By default (position=None) the item will be inserted after existing items in the same level of the hierarchy (a new sub-menu will become the last sub-menu of the menu, a new menu will be become the last menu in the toolbar, and so on).

A position of 0 will insert the item before all the others.

If you already have an object, you can use that as a reference too. For example:

def populate(self):

    link = self.toolbar.add_link_item('Link', url=link_url)
    self.toolbar.add_button('Button', url=button_url, position=link)

will add the new button before the link item.

Finally, you can use a ItemSearchResult as a position:

def populate(self):

    self.toolbar.add_link_item('Link', url=link_url)

    link = self.toolbar.find_first(LinkItem)

    self.toolbar.add_button('Button', url=button_url, position=link)

and since the ItemSearchResult can be cast to an integer, you could even do:

self.toolbar.add_button(‘Button’, url=button_url, position=link+1)

Control how and when the toolbar appears

By default, your CMSToolbar sub-class will be active (i.e. its populate methods will be called) in the toolbar on every page, when the user is_staff. Sometimes however a CMSToolbar sub-class should only populate the toolbar when visiting pages associated with a particular application.

A CMSToolbar sub-class has a useful attribute that can help determine whether a toolbar should be activated. is_current_app is True when the application containing the toolbar class matches the application handling the request.

This allows you to activate it selectively, for example:

def populate(self):

    if not self.is_current_app:
        return

    [...]

If your toolbar class is in another application than the one you want it to be active for, you can list any applications it should support when you create the class:

supported_apps = ['some_app']

supported_apps is a tuple of application dotted paths (e.g: supported_apps = ('whatever.path.app', 'another.path.app').

The attribute app_path will contain the name of the application handling the current request - if app_path is in supported_apps, then is_current_app will be True.

Modifying an existing toolbar

If you need to modify an existing toolbar (say to change an attribute or the behaviour of a method) you can do this by creating a sub-class of it that implements the required changes, and registering that instead of the original.

The original can be unregistered using toolbar_pool.unregister(), as in the example below. Alternatively if you orginally invoked the toolbar class using CMS_TOOLBARS, you will need to modify that to refer to the new one instead.

An example, in which we unregister the original and register our own:

from cms.toolbar_pool import toolbar_pool
from third_party_app.cms_toolbar import ThirdPartyToolbar

@toolbar_pool.register
class MyBarToolbar(ThirdPartyToolbar):
    [...]

toolbar_pool.unregister(ThirdPartyToolbar)

Detecting URL changes to an object

If you want to watch for object creation or editing of models and redirect after they have been added or changed add a watch_models attribute to your toolbar.

Example:

class PollToolbar(CMSToolbar):

    watch_models = [Poll]

    def populate(self):
        ...

After you add this every change to an instance of Poll via sideframe or modal window will trigger a redirect to the URL of the poll instance that was edited, according to the toolbar status:

  • in draft mode the get_draft_url() is returned (or get_absolute_url() if the former does not exist)
  • in live mode, and the method exists, get_public_url() is returned.

Frontend

If you need to interact with the toolbar, or otherwise account for it in your site’s frontend code, it provides CSS and JavaScript hooks for you to use.

It will add various classes to the page’s <html> element:

  • cms-ready, when the toolbar is ready
  • cms-toolbar-expanded, when the toolbar is fully expanded
  • cms-toolbar-expanding and cms-toolbar-collapsing during toolbar animation.

The toolbar also fires a JavaScript event called cms-ready on the document. You can listen to this event using jQuery:

CMS.$(document).on('cms-ready', function () { ... });