How to extend Page & PageContent models ======================================= You can extend the :class:`cms.models.pagemodel.Page` and :class:`cms.models.contentmodels.PageContent` models with your own fields (e.g. adding an icon for every page) by using the extension models: ``cms.extensions.PageExtension`` and ``cms.extensions.PageContentExtension``, respectively. .. note:: .. versionchanged:: 4.1 In django CMS the ``PageContent`` model used to be called ``Title``. Since django CMS 4.1 a ``TitleExtension`` has become ``PageContentExtension`` PageContent vs Page extensions ------------------------------ The difference between a **page extension** and a **page content extension** is related to the difference between the :class:`cms.models.pagemodel.Page` and :class:`cms.models.contentmodels.PageContent` models. - ``PageExtension``: use to add fields that should have **the same values** for the different language versions of a page - for example, an icon. - ``PageContentExtension``: 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 ``PageContentExtension``) 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: .. code-block:: from django.db import models from cms.extensions import PageExtension from cms.extensions.extension_pool import extension_pool class IconExtension(PageExtension): image = models.ImageField(upload_to='icons') 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. Continuing with the example model above, here's a simple corresponding ``PageExtensionAdmin`` class that should live in the ``admin.py`` file: .. code-block:: from django.contrib import admin from cms.extensions import PageExtensionAdmin from .models import IconExtension class IconExtensionAdmin(PageExtensionAdmin): pass 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. .. code-block:: from cms.toolbar_pool import toolbar_pool from cms.extensions.toolbar import ExtensionToolbar from django.utils.translation import gettext_lazy as _ from .models import IconExtension @toolbar_pool.register class IconExtensionToolbar(ExtensionToolbar): # defines the model for the current toolbar model = IconExtension def populate(self): # setup the extension toolbar with permissions and sanity checks current_page_menu = self._setup_extension_toolbar() # if it's all ok if current_page_menu: # retrieves the instance of the current extension (if any) and the toolbar item URL page_extension, url = self.get_page_extension_admin() if url: # adds a toolbar item in position 0 (at the top of the menu) current_page_menu.add_modal_item(_('Page Icon'), url=url, disabled=not self.toolbar.edit_mode_active, position=0) PageContent model extension example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this example, we'll create a ``Rating`` extension field, that can be applied to each ``PageContent``, 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 +++++++++ .. code-block:: from django.db import models from cms.extensions import PageContentExtension from cms.extensions.extension_pool import extension_pool class RatingExtension(PageContentExtension): rating = models.IntegerField() extension_pool.register(RatingExtension) The admin +++++++++ .. code-block:: from django.contrib import admin from cms.extensions import PageContentExtensionAdmin from .models import RatingExtension class RatingExtensionAdmin(PageContentExtensionAdmin): pass admin.site.register(RatingExtension, RatingExtensionAdmin) The toolbar item ++++++++++++++++ In this example, we need to loop over the page contents for the page, and populate the menu with those. .. code-block:: from cms.toolbar_pool import toolbar_pool from cms.extensions.toolbar import ExtensionToolbar from django.utils.translation import gettext_lazy as _ from .models import RatingExtension from cms.utils import get_language_list # needed to get the page's languages @toolbar_pool.register class RatingExtensionToolbar(ExtensionToolbar): # defines the model for the current toolbar model = RatingExtension def populate(self): # setup the extension toolbar with permissions and sanity checks current_page_menu = self._setup_extension_toolbar() # if it's all ok if current_page_menu and self.toolbar.edit_mode_active: # create a sub menu labelled "Ratings" at position 1 in the menu sub_menu = self._get_sub_menu( current_page_menu, 'submenu_label', 'Ratings', position=1 ) # we now need to get the pagecontent_set (i.e. different language page contents) # for this page page = self._get_page() page_contents = page.pagecontent_set(manager="admin_manager").latest_content(language__in=get_language_list(page.node.site_id)) # create a 3-tuple of (title_extension, url, title) nodes = [ (*self.get_page_content_extension_admin(page_content), page_content.title) for page_content in page_contents ] # cycle through the list of nodes for title_extension, url, title in nodes: # adds toolbar items sub_menu.add_modal_item( 'Rate %s' % title, url=url, disabled=not self.toolbar.edit_mode_active ) 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: .. code-block:: {% load static %} {# rest of template omitted ... #} {% if request.current_page.iconextension %} {% 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. PageContent extensions ++++++++++++++++++++++ In order to retrieve a page content extension within a template, get the ``PageContent`` object using ``request.current_page.get_content_obj``. Using the example above, we could use: .. code-block:: {{ request.current_page.get_content_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 :ref:`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 ``PageContentExtension`` 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`` .. code-block:: from django.db import models from cms.extensions import PageExtension from cms.extensions.extension_pool import extension_pool class MyPageExtension(PageExtension): page_categories = models.ManyToManyField(Category, blank=True) def copy_relations(self, oldinstance, language): for page_category in oldinstance.page_categories.all(): page_category.pk = None page_category.mypageextension = self page_category.save() extension_pool.register(MyPageExtension) Complete toolbar API -------------------- The example above uses the :ref:`simplified_extension_toolbar`. .. _complete_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: .. code-block:: from cms.api import get_page_draft from cms.toolbar_pool import toolbar_pool from cms.toolbar_base import CMSToolbar from cms.utils import get_cms_setting from cms.utils.page_permissions import user_can_change_page from django.urls import reverse, NoReverseMatch from django.utils.translation import gettext_lazy as _ from .models import IconExtension @toolbar_pool.register class IconExtensionToolbar(CMSToolbar): def populate(self): # always use draft if we have a page self.page = get_page_draft(self.request.current_page) if not self.page: # Nothing to do return if user_can_change_page(user=self.request.user, page=self.page): try: icon_extension = IconExtension.objects.get(extended_object_id=self.page.id) except IconExtension.DoesNotExist: icon_extension = None try: if icon_extension: url = reverse('admin:myapp_iconextension_change', args=(icon_extension.pk,)) else: url = reverse('admin:myapp_iconextension_add') + '?extended_object=%s' % self.page.pk except NoReverseMatch: # not in urls pass else: not_edit_mode = not self.toolbar.edit_mode_active current_page_menu = self.toolbar.get_or_create_menu('page') 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_extension_toolbar: 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_page_content_extension_admin(page_content=None)``: for page content extensions, retrieves the correct admin URL for the related toolbar item; returns a tuple of the extension instance (or ``None`` if none exists) and the admin URL for the current page content (if the argument is None or omitted) or the page content object passed. Typically, ``ExtensionToolbar.get_page_content_extension_admin`` is used without the argument to modify the toolbar for the currently visible page content object. .. warning:: The ``ExtensionToolbar.get_title_extension_admin(language=None)`` from django CMS versions before 4.1 still exists but is deprecated.