Customizing FeinCMS Part 3: No-Children templates
Part 3 of the "Customizing FeinCMS" series. In this post we are going to add another great feature: No-Children templates.
You can find the code used in this serie on github.
Why
Sometimes it happens that you don't want your template to have sub pages, once again think of a home page or a contact page.
If your design doesn't support it, you'll have to explain to your clients why they can't see their sub menu after creating sub pages or worse, everything will break and you'll have to blame them for not using your CMS correctly.
Well, when this happens, you have to blame yourself as your system allows them to make these kind of mistakes.
What we need to do in a nutshell
You should now be able to implement this feature yourself as the strategy is exactly the same as the one we used in the previous two posts.
We are going to create an additional exception and catch it whilst validating a page so that we can be sure that our back-end code works as it's supposed to.
Finally, we are going to improve the user experience by hiding actions not allowed and customising error messages.
Implementation
Let's start by adding an extra argument to our custom Template class
pages.models
class Template(FeinCMSTemplate):
def __init__(
self, title, path, regions, key=None, preview_image=None, unique=False,
first_level_only=False, no_children=False
):
super(Template, self).__init__(
title, path, regions, key=key, preview_image=preview_image
)
self.unique = unique
self.first_level_only = first_level_only
self.no_children = no_children
and register out homepage as we usually do
Page.register_templates(Template(
key='homepage',
title='Home Page',
path='pages/home_page.html',
regions=(
('home_main', 'Main Content'),
),
unique=True,
first_level_only=True,
no_children=True
))
We are now ready to extend the validation check by including the new feature.
This step consists of:
- creating a new exception called
NoChildrenTemplateException
- changing
check_template
by raising the new exception if the user is using a no-children page in the wrong way - catching the new exception in
is_template_valid
that, once again, will be used to display valid templates whilst creating/editing a page
is_template_valid
is extremely important as we are not allowed to assign a no-children template to an existing page with sub pages.
pages.exceptions
class NoChildrenTemplateException(Exception):
pass
pages.admin
from pages.exceptions import UniqueTemplateException
from pages.exceptions import FirstLevelOnlyTemplateException
from pages.exceptions import NoChildrenTemplateException
def check_template(model, template, instance=None, parent=None):
def get_parent(parent):
if not parent:
return None
if isinstance(parent, Page):
return parent
return Page.objects.get(id=parent)
if template.unique and model.objects.filter(
template_key=template.key
).exclude(id=instance.id if instance else -1).count():
raise UniqueTemplateException()
parent_page = get_parent(parent)
if template.first_level_only and parent_page:
raise FirstLevelOnlyTemplateException()
if parent_page and model._feincms_templates[parent_page.template_key].no_children:
raise NoChildrenTemplateException()
if instance and template.no_children and instance.children.count():
raise NoChildrenTemplateException()
def is_template_valid(model, template, instance=None, parent=None):
try:
check_template(model, template, instance=instance, parent=parent)
return True
except (
UniqueTemplateException, FirstLevelOnlyTemplateException,
NoChildrenTemplateException
):
pass
return False
class PageAdminForm(PageAdminFormOld):
...
def clean(self):
cleaned_data = super(PageAdminForm, self).clean()
# No need to think further, let the user correct errors first
if self._errors:
return cleaned_data
parent = cleaned_data.get('parent')
if parent:
template_key = cleaned_data['template_key']
template = self.Meta.model._feincms_templates[template_key]
try:
check_template(
self.Meta.model, template, instance=self.instance, parent=parent
)
except UniqueTemplateException:
self._errors['parent'] = ErrorList(
[_('Template already used somewhere else.')]
)
del cleaned_data['parent']
except FirstLevelOnlyTemplateException:
self._errors['parent'] = ErrorList(
[_("This template can't be used as a subpage")]
)
del cleaned_data['parent']
except NoChildrenTemplateException:
self._errors['parent'] = ErrorList(
[_("This parent page can't have subpages")]
)
del cleaned_data['parent']
return cleaned_data
Our back-end validation is now complete, if you try adding sub pages and moving pages around in the wrong way you'll get validation messages.
The next step is to improve the user experience by hiding actions not allowed and making the interface more intuitive.
Let's start with the change_list; create a home page with a no-children template and go to the page list.
You might notice that you can still see the icon used for adding sub pages, this is not ideal as if you click on it you'll see a validation alert. It would be better to hide it from the interface.
Besides, we cannot paste an existing page into a no-children one therefore we need to hide that icon too.
To do so, we have to override the _actions_columns
method in PageAdmin
, check if the page being displayed has a no-children template associated and hide the two icons if necessary.
Here, I'll use a placeholder, which is just a transparent image, to keep the icons aligned.
pages.admin.PageAdmin:
def _actions_column(self, page):
actions = []
actions.append(
u'<a href="%s" title="%s"><img src="%simg/admin/selector-search.gif" alt="%s" /></a>' % (
page.get_absolute_url(), _('View on site'), django_settings.ADMIN_MEDIA_PREFIX,
_('View on site')
)
)
template = self.model._feincms_templates.get(page.template_key)
no_children = template and template.no_children
if not no_children:
actions.append(
u'<a href="add/?parent=%s" title="%s"><img src="%simg/admin/icon_addlink.gif" alt="%s"></a>' % (
page.pk, _('Add child page'), django_settings.ADMIN_MEDIA_PREFIX ,_('Add child page')
)
)
else:
actions.append(u'<img src="%simg/admin/actions_placeholder.gif" alt="%s">' % (
django_settings.ADMIN_MEDIA_PREFIX ,_('No Action')))
actions.append(
u'<a href="#" class="cut%s" onclick="return cut_item(\'%s\', this)" title="%s"><big>✂</big></a>' % (
' cant_have_children' if no_children else "", page.pk, _('Cut')
)
)
actions.append(
u'<a class="paste_target" href="#" onclick="return paste_item(\'%s\', \'left\')" title="%s">↱</a>' % (
page.pk, _('Insert before')
)
)
if not no_children:
actions.append(
u'<a class="paste_target%s" href="#" onclick="return paste_item(\'%s\', \'last-child\')" title="%s">↳</a>' % (
" children" if page.parent else "", page.pk, _('Insert as child')
)
)
return actions
UPDATE 10.2012
In new versions of Django and FeinCMS you should use the code below instead:
def _actions_column(self, page):
actions = super(PageAdmin, self)._actions_column(page)
template = self.model._feincms_templates.get(page.template_key)
no_children = template and template.no_children
if no_children and getattr(page, 'feincms_editable', True):
actions[1] = u'<img src="%spages/img/actions_placeholder.gif">' % django_settings.STATIC_URL
return actions
The last step is to remove the "add child" link from the change_form for no-children pages.
The easiest way is to override the render_item_editor
method and set has_add_permission
to False so that the django template will think that the user doesn't have add permissions and won't display the link.
Although it works great, I don't recommend it as it's confusing and logically wrong.
pages.admin.PageAdmin:
def render_item_editor(self, request, object, context):
template = self.model._feincms_templates.get(object.template_key)
if template.no_children:
context['has_add_permission'] = False
return super(PageAdmin, self).render_item_editor(request, object, context)
The right way is to define another context variable and override the django template by adding a custom check whilst rendering the link.
UPDATE 10.2012
In new versions of Django and FeinCMS overriding render_item_editor is not required any more.
Conclusions
Once again, we added a new feature by changing a few lines of code and we designed a good software architecture so that you can use the same logic to extend feinCMS with more and more features.
Next
Customizing FeinCMS Part 4: Level of Navigation
26 July 2010