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.

Table Of Contents

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:

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>&#x2702;</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">&#x21b1;</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">&#x21b3;</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