Customizing FeinCMS Part 4: Level of Navigation

Part 4 of the "Customizing FeinCMS" series. In this post we are going to limit the level of navigation of our website.

Table Of Contents

You can find the code used in this serie on github.

Why

I've developed many websites with strict levels of navigation, most of the time expressed by a single layer of sub pages, in these cases I want my CMS to validate the user actions with an "all you can do is all you can see" strategy.

This makes a lot of sense if you don't like being bothered by your clients asking you questions like: "Why I can't see my pages?".

What we need to do in a nutshell

We are going to extend the clean method of PageAdminForm by checking if the level of navigation is valid and showing a validation error message if not.

In addition, we can improve the user experience by hiding the add_child icon from the change_list where not allowed.

Implementation

First of all, let's add a new value in settings.py expressing the max level of navigation

settings:

FEINCMS_NAVIGATION_LEVEL = 2

and create a couple of functions we will use whilst validating a page.

pages.admin:

def get_max_navigation_level():
    return getattr(django_settings, 'FEINCMS_NAVIGATION_LEVEL', None)

def is_navigation_level_valid(level):
    max_level = get_max_navigation_level()
    if not max_level:
        return True
    return max_level >= level

Secondly we need to add another check in the PageAdminForm.clean method and raise an exception if the user is adding a page at a level not allowed.

PageAdminForm:

    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]

            parent_error = None
            try:
                check_template(
                    self.Meta.model, template, instance=self.instance, parent=parent
                )
            except UniqueTemplateException:
                parent_error = _('Template already used somewhere else')
            except FirstLevelOnlyTemplateException:
                parent_error = _("This template can't be used as a subpage")
            except NoChildrenTemplateException:
                parent_error = _("Parent page can't have subpages")
            else:
                if not is_navigation_level_valid(parent.level+2):
                    parent_error = _(
                        "Only %d levels allowed" % get_max_navigation_level()
                    )

            if parent_error:
                self._errors['parent'] = ErrorList([parent_error])
                del cleaned_data['parent']                
        return cleaned_data

Finally we can hide the add_child icon from the list item if necessary.

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
        valid_navigation = is_navigation_level_valid(page.level+2)

        if not no_children and valid_navigation:
            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 and valid_navigation:
            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
        valid_navigation = is_navigation_level_valid(page.level+2)

        if (no_children or not valid_navigation) and getattr(page, 'feincms_editable', True):
            actions[1] = u'<img src="%spages/img/actions_placeholder.gif">' % django_settings.STATIC_URL
        return actions

Conclusions

This was the last part of the "Customizing FeinCMS" series, we've seen how easy it is to add new features and improve the user experience.

I hope that if tomorrow you receive an e-mail from one of your clients saying that your CMS sucks you'll feel a bit more guilty and you'll start building robust websites immediately.

23 Sept. 2010