Extending the Toolbar

New in version 3.0.

You can add and remove toolbar items. This allows you to integrate django CMS’s frontend editing mode into your application, and provide your users with a streamlined editing experience.

For the toolbar API reference, please refer to The Toolbar.

Important

Overlay and sideframe

Then django CMS sideframe has been replaced with an overlay mechanism. The API still refers to the sideframe, because it is invoked in the same way, and what has changed is merely the behaviour in the user’s browser.

In other words, sideframe and the overlay refer to different versions of the same thing.

Registering

There are two ways to control what gets shown in the toolbar.

One is the CMS_TOOLBARS. This gives you full control over which classes are loaded, but requires that you specify them all manually.

The other is to provide cms_toolbars.py files in your apps, which will be automatically loaded as long CMS_TOOLBARS is not set (or is set to None).

If you use the automated way, your cms_toolbars.py file should contain classes that extend cms.toolbar_base.CMSToolbar and are registered using register(). The register function can be used as a decorator.

These classes have four attributes: * toolbar (the toolbar object) * request (the current request) * is_current_app (a flag indicating whether the current request is handled by the same app as the function is in) * app_path (the name of the app used for the current request)

These classes must implement a populate or post_template_populate function. An optional request_hook function is available for you to overwrite as well.

  • The populate functions will only be called if the current user is a staff user.
  • The populate function will be called before the template and plugins are rendered.
  • The post_template_populate function will be called after the template is rendered.
  • The request_hook function is called before the view and may return a response. This way you can issue redirects from a toolbar if needed

These classes can define an optional supported_apps attribute, specifying which applications the toolbar will work with. This is useful when the toolbar is defined in a different application from the views it’s related to.

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

A simple example, registering a class that does nothing:

  1. from cms.toolbar_pool import toolbar_pool
  2. from cms.toolbar_base import CMSToolbar
  3. @toolbar_pool.register
  4. class NoopModifier(CMSToolbar):
  5. def populate(self):
  6. pass
  7. def post_template_populate(self):
  8. pass
  9. def request_hook(self):
  10. pass

Note

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

Warning

As the toolbar passed to post_template_populate has been already populated with items from other applications, it might contain different items when processed by populate.

Tip

You can change the toolbar or add items inside a plugin render method (context['request'].toolbar) or inside a view (request.toolbar)

Adding items

Items can be added through the various APIs exposed by the toolbar and its items.

To add a cms.toolbar.items.Menu to the toolbar, use cms.toolbar.toolbar.CMSToolbar.get_or_create_menu().

Then, to add a link to your changelist that will open in the sideframe, use the cms.toolbar.items.ToolbarMixin.add_sideframe_item() method on the menu object returned.

When adding items, all arguments other than the name or identifier should be given as keyword arguments. This will help ensure that your custom toolbar items survive upgrades.

Following our Extending the Toolbar, let’s add the poll app to the toolbar:

  1. from django.core.urlresolvers import reverse
  2. from django.utils.translation import ugettext_lazy as _
  3. from cms.toolbar_pool import toolbar_pool
  4. from cms.toolbar_base import CMSToolbar
  5. @toolbar_pool.register
  6. class PollToolbar(CMSToolbar):
  7. def populate(self):
  8. if self.is_current_app:
  9. menu = self.toolbar.get_or_create_menu('poll-app', _('Polls'))
  10. url = reverse('admin:polls_poll_changelist')
  11. menu.add_sideframe_item(_('Poll overview'), url=url)

However, there’s already a menu added by the CMS which provides access to various admin views, so you might want to add your menu as a sub menu there. To do this, you can use positional insertion coupled with the fact that cms.toolbar.toolbar.CMSToolbar.get_or_create_menu() will return already existing menus:

  1. from django.core.urlresolvers import reverse
  2. from django.utils.translation import ugettext_lazy as _
  3. from cms.toolbar_pool import toolbar_pool
  4. from cms.toolbar.items import Break
  5. from cms.cms_toolbars import ADMIN_MENU_IDENTIFIER, ADMINISTRATION_BREAK
  6. from cms.toolbar_base import CMSToolbar
  7. @toolbar_pool.register
  8. class PollToolbar(CMSToolbar):
  9. def populate(self):
  10. admin_menu = self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER, _('Site'))
  11. position = admin_menu.find_first(Break, identifier=ADMINISTRATION_BREAK)
  12. menu = admin_menu.get_or_create_menu('poll-menu', _('Polls'), position=position)
  13. url = reverse('admin:polls_poll_changelist')
  14. menu.add_sideframe_item(_('Poll overview'), url=url)
  15. admin_menu.add_break('poll-break', position=menu)

If you wish to simply detect the presence of a menu without actually creating it, you can use get_menu(), which will return the menu if it is present, or, if not, will return None.

Modifying an existing toolbar

If you need to modify an existing toolbar (say to change the supported_apps attribute) you can do this by extending the original one, and modifying the appropriate attribute.

If CMS_TOOLBARS is used to register the toolbars, add your own toolbar instead of the original one, otherwise unregister the original and register your own:

  1. from cms.toolbar_pool import toolbar_pool
  2. from third.party.app.cms.toolbar_base import FooToolbar
  3. @toolbar_pool.register
  4. class BarToolbar(FooToolbar):
  5. supported_apps = ('third.party.app', 'your.app')
  6. toolbar_pool.unregister(FooToolbar)

Adding Items Alphabetically

Sometimes it is desirable to add sub-menus from different applications alphabetically. This can be challenging due to the non-obvious manner in which your apps will be loaded into Django and is further complicated when you add new applications over time.

To aid developers, django-cms exposes a cms.toolbar.items.ToolbarMixin.get_alphabetical_insert_position() method, which, if used consistently, can produce alphabetised sub-menus, even when they come from multiple applications.

An example is shown here for an ‘Offices’ app, which allows handy access to certain admin functions for managing office locations in a project:

  1. from django.core.urlresolvers import reverse
  2. from django.utils.translation import ugettext_lazy as _
  3. from cms.toolbar_base import CMSToolbar
  4. from cms.toolbar_pool import toolbar_pool
  5. from cms.toolbar.items import Break, SubMenu
  6. from cms.cms_toolbars import ADMIN_MENU_IDENTIFIER, ADMINISTRATION_BREAK
  7. @toolbar_pool.register
  8. class OfficesToolbar(CMSToolbar):
  9. def populate(self):
  10. #
  11. # 'Apps' is the spot on the existing djang-cms toolbar admin_menu
  12. # 'where we'll insert all of our applications' menus.
  13. #
  14. admin_menu = self.toolbar.get_or_create_menu(
  15. ADMIN_MENU_IDENTIFIER, _('Apps')
  16. )
  17. #
  18. # Let's check to see where we would insert an 'Offices' menu in the
  19. # admin_menu.
  20. #
  21. position = admin_menu.get_alphabetical_insert_position(
  22. _('Offices'),
  23. SubMenu
  24. )
  25. #
  26. # If zero was returned, then we know we're the first of our
  27. # applications' menus to be inserted into the admin_menu, so, here
  28. # we'll compute that we need to go after the first
  29. # ADMINISTRATION_BREAK and, we'll insert our own break after our
  30. # section.
  31. #
  32. if not position:
  33. # OK, use the ADMINISTRATION_BREAK location + 1
  34. position = admin_menu.find_first(
  35. Break,
  36. identifier=ADMINISTRATION_BREAK
  37. ) + 1
  38. # Insert our own menu-break, at this new position. We'll insert
  39. # all subsequent menus before this, so it will ultimately come
  40. # after all of our applications' menus.
  41. admin_menu.add_break('custom-break', position=position)
  42. # OK, create our office menu here.
  43. office_menu = admin_menu.get_or_create_menu(
  44. 'offices-menu',
  45. _('Offices ...'),
  46. position=position
  47. )
  48. # Let's add some sub-menus to our office menu that help our users
  49. # manage office-related things.
  50. # Take the user to the admin-listing for offices...
  51. url = reverse('admin:offices_office_changelist')
  52. office_menu.add_sideframe_item(_('Offices List'), url=url)
  53. # Display a modal dialogue for creating a new office...
  54. url = reverse('admin:offices_office_add')
  55. office_menu.add_modal_item(_('Add New Office'), url=url)
  56. # Add a break in the sub-menus
  57. office_menu.add_break()
  58. # More sub-menus...
  59. url = reverse('admin:offices_state_changelist')
  60. office_menu.add_sideframe_item(_('States List'), url=url)
  61. url = reverse('admin:offices_state_add')
  62. office_menu.add_modal_item(_('Add New State'), url=url)

Here is the resulting toolbar (with a few other menus sorted alphabetically beside it)

alphabetized-toolbar-app-menus

Adding items through views

Another way to add items to the toolbar is through our own views (polls/views.py). This method can be useful if you need to access certain variables, in our case e.g. the selected poll and its sub-methods:

  1. from django.core.urlresolvers import reverse
  2. from django.shortcuts import get_object_or_404, render
  3. from django.utils.translation import ugettext_lazy as _
  4. from polls.models import Poll
  5. def detail(request, poll_id):
  6. poll = get_object_or_404(Poll, pk=poll_id)
  7. menu = request.toolbar.get_or_create_menu('polls-app', _('Polls'))
  8. menu.add_modal_item(_('Change this Poll'), url=reverse('admin:polls_poll_change', args=[poll_id]))
  9. menu.add_sideframe_item(_('Show History of this Poll'), url=reverse('admin:polls_poll_history', args=[poll_id]))
  10. menu.add_sideframe_item(_('Delete this Poll'), url=reverse('admin:polls_poll_delete', args=[poll_id]))
  11. return render(request, 'polls/detail.html', {'poll': poll})

Detecting URL changes

Sometimes toolbar entries allow you to change the URL of the current object displayed in the website.

For example, suppose you are viewing a blog entry, and the toolbar allows the blog slug or URL to be edited. The toolbar will watch the django.contrib.admin.models.LogEntry model and detect if you create or edit an object in the admin via modal or sideframe view. After the modal or sideframe closes it will redirect to the new URL of the object.

To set this behaviour manually you can set the request.toolbar.set_object() function on which you can set the current object.

Example:

  1. def detail(request, poll_id):
  2. poll = get_object_or_404(Poll, pk=poll_id)
  3. if hasattr(request, 'toolbar'):
  4. request.toolbar.set_object(poll)
  5. return render(request, 'polls/detail.html', {'poll': poll})

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:

  1. class PollToolbar(CMSToolbar):
  2. watch_models = [Poll]
  3. def populate(self):
  4. ...

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: if in draft mode the get_draft_url() is returned (or get_absolute_url() if the former does not exists), if in live mode and the method exists get_public_url() is returned.

Frontend

The toolbar adds a class cms-ready to the html tag when ready. Additionally we add cms-toolbar-expanded when the toolbar is fully expanded. We also add cms-toolbar-expanding and cms-toolbar-collapsing classes while toolbar is animating.

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

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