The other day, while discussing designs with a client, he came up with the requirement of having author information on each page of his website. It was a good idea, but how could we implement that?
Using a dedicated plugin in each page would be feasible but definitely cumbersome.
To handle such cases a common pattern in the CMS world is to have a very broad definition of what a page is. A Page object is often just a base to create more complex objects (commonly named 'Content type' or 'Classes') by adding attributes and properties, while still remaining a Page object managed by the CMS. In this case we could create an 'AuthorPage' (or whatever) with a link to an Author object. django CMS took a very different approach and stayed true to this choice ever since. Page class is unique, and you're not supposed to mess with it.
The django CMS way
django CMS is a good Django citizen in the first place and tries to do a great job at one thing i.e. being a CMS and managing unstructured content, while leaving tasks such as taking care of structured content, user interaction and so on to specialized Django applications. In other words, if you want a list of news, don't try to organize pages to emulate a list of news: write a couple dozen lines of Python, and you'll end up with a far better news application than a bunch of pages could ever be!
Extending the CMS Page
Nevertheless sometimes the need to add some information to the Page model arise, without really making it other than a Page, classification tags, authorship information, or extended meta tags, just some examples of information that may be needed to be attached to any page. Up until now there wasn't any cleaner way to achieve this, than writing a bunch of hackish code. Since 3.0 this has changed for the better. The Page Extensions API permits adding attributes to Page and Title objects, augmenting the storable information for each that can be later retrieved in the template or everywhere in your Django project. The good thing is, everything is totally transparent and django-ish: PageExtension and TitleExtension (the base classes you're going to use) are just models with a OneToOneField to Page or Title models, and most of the work the API code does is, keeping the state sane when you populate those models or publish pages. Accessing a PageAuthor instance, for example in the template is something like request.current_page.pageauthor.author with no real magic behind the curtains (if you except the Django ORM magic!)
Implementing an extension
Extension are recommended to live in a separate application, especially if they have relations with other models. To implement a Page / Title extension you must create three classes:
- the extension model: it's where the extra information are going to be stored;
- the extension admin: it's the ModelAdmin for the model;
- the extension toolbar items: the extension admin is not “usable” from the main admin dashboard and you must tie it to the toolbar for it to work properly.
Extension model - PageAuthorProperties
The extension model is a model extending from a defined parent that handles the publishing process and other internal working; it must be defined in models.py.
In this way, the resulting model is quite simple and straightforward:
# -*- coding: utf-8 -*- from django.contrib.auth.models import User from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from cms.extensions import PageExtension, extension_pool @python_2_unicode_compatible class PageAuthorProperties(PageExtension): author = models.ForeignKey(User, null=True, blank=True, verbose_name=_(u'Page author'), related_name='page_author') def __str__(self): if self.author: return _('Page author %s') % self.author.get_full_name() else: return _('No author') extension_pool.register(PageAuthorProperties)
Basically the only thing you need are the fields that you want in your page extension; you can add any number of field of any type.
After the model definition you must register your model as a page extension with extension_pool.register.
If you add ManyToMany fields in your model, you are requested to define a copy_relations(self) method to handle copy related objects when publishing the extension; see documentation for examples.
This is implemented in admin.py as always; if you don't need ModelAdmin customisation, registering the model with the generic PageExtensionAdmin will suffice:
# -*- coding: utf-8 -*- from cms.extensions import PageExtensionAdmin from django.contrib import admin from .models import PageAuthorProperties admin.site.register(PageAuthorProperties, PageExtensionAdmin)
Otherwise just create a ModelAdmin by extending PageExtensionAdmin:
# -*- coding: utf-8 -*- from cms.extensions import PageExtensionAdmin from django.contrib import admin from .models import PageAuthorProperties class PageAuthorPropertiesAdmin(PageExtensionAdmin): fields = ('author',) admin.site.register(PageAuthorProperties, PageAuthorPropertiesAdmin)
Toolbar - PageAuthorToolbar
Toolbar is the way django CMS allows you to customise your editing interface.
Implementing a toolbar is a bit a low level thing as you are required to check for the permissions and add the proper toolbar item in the desired position. Toolbar must by defined in cms_toolbar.py file.
This is the complete code for a page extension toolbar:
# -*- coding: utf-8 -*- 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.permissions import has_page_change_permission from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.translation import ugettext_lazy as _ from .models import PageAuthorProperties @toolbar_pool.register class PageAuthorPropertiesToolbar(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 # check global permissions if CMS_PERMISSIONS is active if get_cms_setting('PERMISSION'): has_global_current_page_change_permission = has_page_change_permission(self.request) else: has_global_current_page_change_permission = False # check if user has page edit permission can_change = self.request.current_page and self.request.current_page.has_change_permission(self.request) if has_global_current_page_change_permission or can_change: try: extension = PageAuthorProperties.objects.get(extended_object_id=self.page.id) except PageAuthorProperties.DoesNotExist: extension = None try: if extension: url = reverse('admin:myapp_pageauthorproperties_change', args=(extension.pk,)) else: url = reverse('admin:myapp_pageauthorproperties_add') + '?extended_object=%s' % self.page.pk except NoReverseMatch: # not in urls pass else: not_edit_mode = not self.toolbar.edit_mode current_page_menu = self.toolbar.get_or_create_menu('page') current_page_menu.add_modal_item(_('Page author'), url=url, disabled=not_edit_mode)
When you add an item to the toolbar you have to check whether the toolbar is
in edit mode or not and eventually disable the menu item:
current_page_menu.add_modal_item('page author', url=url, disabled=not self.toolbar.edit_mode, position=position)
Another option is to completely remove the item if the toolbar is in live mode:
if url and self.toolbar.edit_mode: current_page_menu.add_modal_item('page author', url=url, position=position)