Customizing FeinCMS Part 1

FeinCMS is the best django CMS at the moment, this is mostly thanks to its flexibility. In this series of posts we are going to add a few extra features to make it even better.

Overview

The problem with django CMS apps is that they give clients too much control over the system.

I hate saying: "See this select here? Please be careful as you might choose the wrong value and then break the design" so I started customizing FeinCMS. It took me a few hours and I didn't need to change the main code.

I'm going to split the whole post in 4 parts:

  1. Unique templates (in this post)
  2. First-Level-Only templates
  3. No-Children templates
  4. Level of Navigation

Versions used:

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

Unique templates

Initial Steps

We are going to use the built-in page module so please read the official FeinCMS documentation and set up your project before going further.
In addition, you need to create a new django app called pages which will contain our custom code.

Behind the scenes

The FeinCMS Page module is really straightforward, it uses just a few classes:

Besides that, feinCMS defines an additional object called Template used to render a page.

Why

Say that you have two templates, one for the home page and the other one for internal pages, we don't want our home-page template to be used more than once since we can have just one Home Page.

What we need to do in a nutshell

We need to extend the Template object in order to add a new unique option and check that unique templates are not used more than once. Besides that, we want PageAdminForm to exclude unique templates that have already been assigned to other pages whilst we are adding/changing a page.

Implementation

If you take a look at feincms.module.page.models for a moment you can see that it uses a Template object containing information about the template assigned to a page.

So let's create a new Template class which extends the FeinCMS one and adds an extra option called unique.

pages.models:

from feincms.models import Base, Template as FeinCMSTemplate


class Template(FeinCMSTemplate):
    def __init__(
        self, title, path, regions, key=None, preview_image=None, unique=False
    ):
        super(Template, self).__init__(
            title, path, regions, key=key, preview_image=preview_image
        )
        self.unique = unique

In the original version of the FeinCMS Page module you register your template by doing something like this:

Page.register_templates({
    'key': 'base',
    'title': _('Standard template'),
    'path': 'feincms_base.html',
    'regions': (
        ('main', _('Main content area')),
        ('sidebar', _('Sidebar'), 'inherited'),
        ),
    }, {
    'key': '2col',
    'title': _('Template with two columns'),
    'path': 'feincms_2col.html',
    'regions': (
        ('col1', _('Column one')),
        ('col2', _('Column two')),
        ('sidebar', _('Sidebar'), 'inherited'),
        ),
    })

We wouldn't need to change register_templates but the original version uses a wrong Template class and there is no way to tell it to use our implementation instead so we have to literally copy and past the code and make it a function. Hopefully, matthiask will add template_class as extra argument.

Update: We can now use a list of our Template instances instead of a list of dicts whilst registering a template.

pages.models:

from feincms.models import Base, Template as FeinCMSTemplate
from feincms.module.page.models import Page


class Template(FeinCMSTemplate):
    def __init__(
        self, title, path, regions, key=None, preview_image=None, unique=False
    ):
        super(Template, self).__init__(
            title, path, regions, key=key, preview_image=preview_image
        )
        self.unique = unique


Page.register_templates(
    Template(
        key='internalpage',
        title='Internal Page',
        path='pages/internal.html',
        regions=(
            ('main', 'Main Content'),
        )
    ), Template(
        key='homepage',
        title='Home Page',
        path='pages/home_page.html',
        regions=(
            ('home_main', 'Main Content'),
        ),
        unique=True
    )
)

So far so easy.

The next step is to hide unique templates from admin forms when they have already been assigned to other pages, we do this by changing PageAdminForm.

But first of all we have to stop and think about what we are doing.

We are going to write a generic check_template function which checks if a template is valid given a particular set of arguments and raises an exception if it doesn't. Why an Exception? Just because it's flexible. If we want to add a new feature all we have to do is create a new Exception and write a few lines of code for validating a page.

So create a new file called pages.exceptions.py with the definition of a new Exception in it:

pages.exceptions:

class UniqueTemplateException(Exception):
    pass

Now we can finally add our validation.

pages.admin:

from django.contrib import admin
from django.conf import settings as django_settings
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe
from django.forms.util import ErrorList
from django.http import HttpResponse

from feincms.module.page.models import Page, PageAdmin as PageAdminOld
from feincms.module.page.models import PageAdminForm as PageAdminFormOld


from pages.exceptions import UniqueTemplateException


def check_template(model, template, instance=None, parent=None):
    if template.unique and model.objects.filter(
                                template_key=template.key
                            ).exclude(id=instance.id if instance else -1).count():
        raise UniqueTemplateException()


def is_template_valid(model, template, instance=None, parent=None):
    try:
        check_template(model, template, instance=instance, parent=parent)
        return True
    except UniqueTemplateException:
        pass

    return False


class PageAdminForm(PageAdminFormOld):
    def __init__(self, *args, **kwargs):
        super(PageAdminForm, self).__init__(*args, **kwargs)

        instance = kwargs.get('instance')
        parent = kwargs.get('initial', {}).get('parent')
        if not parent and instance:
            parent = instance.parent
        templates = self.get_valid_templates(instance, parent)

        choices = []
        for key, template in templates.items():
            if template.preview_image:
                choices.append(
                    (template.key, mark_safe(
                        u'<img src="%s" alt="%s" /> %s' % (
                            template.preview_image, template.key, template.title
                        )
                    ))
                )
            else:
                choices.append((template.key, template.title))

        self.fields['template_key'].choices = choices
        if choices:
            self.fields['template_key'].default = choices[0][0]

    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']
        return cleaned_data

    def get_valid_templates(self, instance=None, parent=None):
        """
            @return dict: dict containing all the templates valid for this instance
                (excluding unique ones already used etc.)
        """
        templates = self.Meta.model._feincms_templates.copy()

        return dict(
            filter(
                lambda (key, template): is_template_valid(
                    self.Meta.model, template, instance=instance, parent=parent
                ), templates.items()
            )
        )


class PageAdmin(PageAdminOld):
    form = PageAdminForm


# We have to unregister the default configuration, and register ours
admin.site.unregister(Page)
admin.site.register(Page, PageAdmin)

It might seem tricky but it's really straightforward. There are just a few new things:

  • check_template: raises an exception if the template isn't valid given a particular set of params
  • is_template_valid: shortcut. It returns True if the template is valid given a particular set of params
  • PageAdminForm.clean: checks if the template assigned can be used
  • PageAdminForm.get_valid_templates: returns all the valid templates for the current instance we are adding/changing
  • PageAdminForm.__init__: reinitialize the template_key field by excluding unique templates which have already been assigned to other pages

Conclusions

As you can see, with a few lines of code we have added our new unique template feature.

Most importantly, we haven't changed the main code and we can still upgrade feinCMS or implement extra custom validation.

Next

Customizing FeinCMS Part 2: First-Level-Only templates

19 May 2010