How to extend Page & Title models

You can extend the cms.models.Page and cms.models.Title models with your own fields (e.g. adding an icon for every page) by using the extension models: cms.extensions.PageExtension and cms.extensions.TitleExtension, respectively.

Title vs Page extensions

The difference between a page extension and a title extension is related to the difference between the cms.models.Page and cms.models.Title models.

  • PageExtension: use to add fields that should have the same values for the different language versions of a page - for example, an icon.

  • TitleExtension: use to add fields that should have language-specific values for different language versions of a page - for example, keywords.

Implement a basic extension

Three basic steps are required:

  • add the extension model

  • add the extension admin

  • add a toolbar menu item for the extension

Page model extension example

The model

To add a field to the Page model, create a class that inherits from cms.extensions.PageExtension. Your class should live in one of your applications’ models.py (or module).

Note

Since PageExtension (and TitleExtension) inherit from django.db.models.Model, you are free to add any field you want but make sure you don’t use a unique constraint on any of your added fields because uniqueness prevents the copy mechanism of the extension from working correctly. This means that you can’t use one-to-one relations on the extension model.

Finally, you’ll need to register the model using extension_pool.

Here’s a simple example which adds an icon field to the page:

  1. from django.db import models
  2. from cms.extensions import PageExtension
  3. from cms.extensions.extension_pool import extension_pool
  4. class IconExtension(PageExtension):
  5. image = models.ImageField(upload_to='icons')
  6. extension_pool.register(IconExtension)

Of course, you will need to make and run a migration for this new model.

The admin

To make your extension editable, you must first create an admin class that sub-classes cms.extensions.PageExtensionAdmin. This admin handles page permissions.

Note

If you want to use your own admin class, make sure to exclude the live versions of the extensions by using filter(extended_object__publisher_is_draft=True) on the queryset.

Continuing with the example model above, here’s a simple corresponding PageExtensionAdmin class:

  1. from django.contrib import admin
  2. from cms.extensions import PageExtensionAdmin
  3. from .models import IconExtension
  4. class IconExtensionAdmin(PageExtensionAdmin):
  5. pass
  6. admin.site.register(IconExtension, IconExtensionAdmin)

Since PageExtensionAdmin inherits from ModelAdmin, you’ll be able to use the normal set of Django ModelAdmin properties appropriate to your needs.

Note

Note that the field that holds the relationship between the extension and a CMS Page is non-editable, so it does not appear directly in the Page admin views. This may be addressed in a future update, but in the meantime the toolbar provides access to it.

The toolbar item

You’ll also want to make your model editable from the cms toolbar in order to associate each instance of the extension model with a page.

To add toolbar items for your extension create a file named cms_toolbars.py in one of your apps, and add the relevant menu entries for the extension on each page.

Here’s a simple version for our example. This example adds a node to the existing Page menu, called Page icon. When selected, it will open a modal dialog in which the Page icon field can be edited.

  1. from cms.toolbar_pool import toolbar_pool
  2. from cms.extensions.toolbar import ExtensionToolbar
  3. from django.utils.translation import gettext_lazy as _
  4. from .models import IconExtension
  5. @toolbar_pool.register
  6. class IconExtensionToolbar(ExtensionToolbar):
  7. # defines the model for the current toolbar
  8. model = IconExtension
  9. def populate(self):
  10. # setup the extension toolbar with permissions and sanity checks
  11. current_page_menu = self._setup_extension_toolbar()
  12. # if it's all ok
  13. if current_page_menu:
  14. # retrieves the instance of the current extension (if any) and the toolbar item URL
  15. page_extension, url = self.get_page_extension_admin()
  16. if url:
  17. # adds a toolbar item in position 0 (at the top of the menu)
  18. current_page_menu.add_modal_item(_('Page Icon'), url=url,
  19. disabled=not self.toolbar.edit_mode_active, position=0)

Title model extension example

In this example, we’ll create a Rating extension field, that can be applied to each Title, in other words, to each language version of each Page.

Note

Please refer to the more detailed discussion above of the Page model extension example, and in particular to the special notes.

The model

  1. from django.db import models
  2. from cms.extensions import TitleExtension
  3. from cms.extensions.extension_pool import extension_pool
  4. class RatingExtension(TitleExtension):
  5. rating = models.IntegerField()
  6. extension_pool.register(RatingExtension)

The admin

  1. from django.contrib import admin
  2. from cms.extensions import TitleExtensionAdmin
  3. from .models import RatingExtension
  4. class RatingExtensionAdmin(TitleExtensionAdmin):
  5. pass
  6. admin.site.register(RatingExtension, RatingExtensionAdmin)

The toolbar item

In this example, we need to loop over the titles for the page, and populate the menu with those.

  1. from cms.toolbar_pool import toolbar_pool
  2. from cms.extensions.toolbar import ExtensionToolbar
  3. from django.utils.translation import gettext_lazy as _
  4. from .models import RatingExtension
  5. from cms.utils import get_language_list # needed to get the page's languages
  6. @toolbar_pool.register
  7. class RatingExtensionToolbar(ExtensionToolbar):
  8. # defines the model for the current toolbar
  9. model = RatingExtension
  10. def populate(self):
  11. # setup the extension toolbar with permissions and sanity checks
  12. current_page_menu = self._setup_extension_toolbar()
  13. # if it's all ok
  14. if current_page_menu and self.toolbar.edit_mode_active:
  15. # create a sub menu labelled "Ratings" at position 1 in the menu
  16. sub_menu = self._get_sub_menu(
  17. current_page_menu, 'submenu_label', 'Ratings', position=1
  18. )
  19. # retrieves the instances of the current title extension (if any)
  20. # and the toolbar item URL
  21. urls = self.get_title_extension_admin()
  22. # we now also need to get the titleset (i.e. different language titles)
  23. # for this page
  24. page = self._get_page()
  25. titleset = page.title_set.filter(language__in=get_language_list(page.node.site_id))
  26. # create a 3-tuple of (title_extension, url, title)
  27. nodes = [(title_extension, url, title.title) for (
  28. (title_extension, url), title) in zip(urls, titleset)
  29. ]
  30. # cycle through the list of nodes
  31. for title_extension, url, title in nodes:
  32. # adds toolbar items
  33. sub_menu.add_modal_item(
  34. 'Rate %s' % title, url=url, disabled=not self.toolbar.edit_mode_active
  35. )

Using extensions

In templates

To access a page extension in page templates you can simply access the appropriate related_name field that is now available on the Page object.

Page extensions

As per the normal related_name naming mechanism, the appropriate field to access is the same as your PageExtension model name, but lowercased. Assuming your Page Extension model class is IconExtension, the relationship to the page extension model will be available on page.iconextension. From there you can access the extra fields you defined in your extension, so you can use something like:

  1. {% load static %}
  2. {# rest of template omitted ... #}
  3. {% if request.current_page.iconextension %}
  4. <img src="{% static request.current_page.iconextension.image.url %}">
  5. {% endif %}

where request.current_page is the normal way to access the current page that is rendering the template.

It is important to remember that unless the operator has already assigned a page extension to every page, a page may not have the iconextension relationship available, hence the use of the {% if ... %}...{% endif %} above.

Title extensions

In order to retrieve a title extension within a template, get the Title object using request.current_page.get_title_obj. Using the example above, we could use:

  1. {{ request.current_page.get_title_obj.ratingextension.rating }}

With menus

Like most other Page attributes, extensions are not represented in the menu NavigationNodes, and therefore menu templates will not have access to them by default.

In order to make the extension accessible, you’ll need to create a menu modifier (see the example provided) that does this.

Each page extension instance has a one-to-one relationship with its page. Get the extension by using the reverse relation, along the lines of extension = page.yourextensionlowercased, and place this attribute of page on the node - as (for example) node.extension.

In the menu template the icon extension we created above would therefore be available as child.extension.icon.

Handling relations

If your PageExtension or TitleExtension includes a ForeignKey from another model or includes a ManyToManyField, you should also override the method copy_relations(self, oldinstance, language) so that these fields are copied appropriately when the CMS makes a copy of your extension to support versioning, etc.

Here’s an example that uses a ManyToManyField

  1. from django.db import models
  2. from cms.extensions import PageExtension
  3. from cms.extensions.extension_pool import extension_pool
  4. class MyPageExtension(PageExtension):
  5. page_categories = models.ManyToManyField(Category, blank=True)
  6. def copy_relations(self, oldinstance, language):
  7. for page_category in oldinstance.page_categories.all():
  8. page_category.pk = None
  9. page_category.mypageextension = self
  10. page_category.save()
  11. extension_pool.register(MyPageExtension)

Complete toolbar API

The example above uses the Simplified Toolbar API.

If you need complete control over the layout of your extension toolbar items you can still use the low-level API to edit the toolbar according to your needs:

  1. from cms.api import get_page_draft
  2. from cms.toolbar_pool import toolbar_pool
  3. from cms.toolbar_base import CMSToolbar
  4. from cms.utils import get_cms_setting
  5. from cms.utils.page_permissions import user_can_change_page
  6. from django.urls import reverse, NoReverseMatch
  7. from django.utils.translation import gettext_lazy as _
  8. from .models import IconExtension
  9. @toolbar_pool.register
  10. class IconExtensionToolbar(CMSToolbar):
  11. def populate(self):
  12. # always use draft if we have a page
  13. self.page = get_page_draft(self.request.current_page)
  14. if not self.page:
  15. # Nothing to do
  16. return
  17. if user_can_change_page(user=self.request.user, page=self.page):
  18. try:
  19. icon_extension = IconExtension.objects.get(extended_object_id=self.page.id)
  20. except IconExtension.DoesNotExist:
  21. icon_extension = None
  22. try:
  23. if icon_extension:
  24. url = reverse('admin:myapp_iconextension_change', args=(icon_extension.pk,))
  25. else:
  26. url = reverse('admin:myapp_iconextension_add') + '?extended_object=%s' % self.page.pk
  27. except NoReverseMatch:
  28. # not in urls
  29. pass
  30. else:
  31. not_edit_mode = not self.toolbar.edit_mode_active
  32. current_page_menu = self.toolbar.get_or_create_menu('page')
  33. current_page_menu.add_modal_item(_('Page Icon'), url=url, disabled=not_edit_mode)

Now when the operator invokes “Edit this page…” from the toolbar, there will be an additional menu item Page Icon ... (in this case), which can be used to open a modal dialog where the operator can affect the new icon field.

Note that when the extension is saved, the corresponding page is marked as having unpublished changes. To see the new extension values publish the page.

Simplified Toolbar API

The simplified Toolbar API works by deriving your toolbar class from ExtensionToolbar which provides the following API:

  • ExtensionToolbar.get_page_extension_admin(): for page extensions, retrieves the correct admin URL for the related toolbar item; returns the extension instance (or None if none exists) and the admin URL for the toolbar item

  • ExtensionToolbar.get_title_extension_admin(): for title extensions, retrieves the correct admin URL for the related toolbar item; returns a list of the extension instances (or None if none exists) and the admin URLs for each title of the current page