tumblelog detail

17.06.2010
Customizing FeinCMS Part 2: First-Level-Only templates

Part 2 of the "Customizing FeinCMS" series.

In this post we are going to add an important feature: First-Lavel-Only templates.

Table Of Contents

Why

It's common to have templates valid only for first-level pages, an essential feature if you don't want your clients to break the design of your website.

To understand how important it is, think of a home-page or a contact-form; you really don't want a client to use this kind of templates for subpages.

What we need to do in a nutshell

As you have seen in the first part of this series, we added a simple but flexible logic based on template validation. We defined a custom exception for our custom feature and a few functions to validate the page form, we also excluded invalid templates from the form so that the user wouldn't think that he was doing something wrong.

In this post, we are going to use the same design for First-Level-Only templates by adding an additional exception and a few lines of code for validating a page. Afterwards, we will see how to improve the user experience by styling error messages and keeping the CMS in a consistent state.

Implementation

Our custom Template needs an extra argument for first-level templates.

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,
        first_level_only=False
    ):
        super(Template, self).__init__(
            title, path, regions, key=key, preview_image=preview_image
        )
        self.unique = unique
        self.first_level_only = first_level_only

Now we can specify that the homepage is a first-level-only template whilst registering it.

pages.models:

Page.register_templates(Template(
    key='homepage',
    title='Home Page',
    path='pages/home_page.html',
    regions=(
        ('home_main', 'Contenuto Principale'),
    ),
    unique=True,
    first_level_only=True
))

As said earlier, we need to create a new exception for our new feature

pages.exceptions:

class FirstLevelOnlyTemplateException(Exception):
    pass

and update the validation code by checking that first-level pages aren't used as subpages.

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
from pages.exceptions import FirstLevelOnlyTemplateException

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()

    if template.first_level_only and parent:
        raise FirstLevelOnlyTemplateException()

def is_template_valid(model, template, instance=None, parent=None):
    try:
        check_template(model, template, instance=instance, parent=parent)
        return True
    except (
            UniqueTemplateException, FirstLevelOnlyTemplateException
        ):
        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']
            except FirstLevelOnlyTemplateException:
                self._errors['parent'] = ErrorList(
                    [_("This template can't be used as a subpage")]
                )
                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()
            )
        )

Just a few new updates:

  • check_template checks if the First-Level-Only Template can be assigned to the page
  • is_valid_template catches FirstLevelOnlyTemplateException along with UniqueTemplateException
  • PageAdminForm.clean invalidate the form when the First-Level-Only template specified by the user can't be assigned to the page

Now, if you navigate to the change_list you might notice that you can arrange pages by clicking on the Cut icon and Paste them wherever you want. At the moment, you can move First-Level-Only pages around and use them as subpages, this is definitely not acceptable so we need some extra validation.

FeinCMS uses an ajax request for cutting and pasting pages calling PageAdmin._move_node, we are going to extend it by checking if we can really move the page and returning an error if not.

To add this validation, we can call check_template and see if it raises an exception, if so it means that the user is not allowed to move the page and we alert him with an error message.

pages.admin:

class PageAdmin(PageAdminOld):
    form = PageAdminForm

    def _move_node(self, request):
        cut_item = self.model._tree_manager.get(pk=request.POST.get('cut_item'))
        pasted_on = self.model._tree_manager.get(pk=request.POST.get('pasted_on'))
        position = request.POST.get('position')

        if position == 'last-child':
            cut_item_template = self.model._feincms_templates[cut_item.template_key]
            pasted_on_template = self.model._feincms_templates[pasted_on.template_key]

            try:
                check_template(
                    self.model, cut_item_template, instance=cut_item, parent=pasted_on
                )
            except FirstLevelOnlyTemplateException:
                return HttpResponse(unicode(_(u"This page can't be used as subpage.")))
            except:
                return HttpResponse(unicode(_(u"Server Error")))

        return super(PageAdmin, self)._move_node(request)

Finally, we can unregister Page and register it again using our PageAdmin definition.

# We have to unregister it, and then reregister
admin.site.unregister(Page)
admin.site.register(Page, PageAdmin)

Almost done, we now have server side validation for change_form and change_list, you can play around with them and see that it's exactly what we wanted.

The last thing to do is to style error messages in the change_list so that they appear exactly like standard django ones.

media.admin.feincms.page_toolbox.js:

function paste_item(pk, position) {
    if(!cut_item_pk)
        return false;

    $.post('.', {
            '__cmd': 'move_node',
            'position': position,
            'cut_item': cut_item_pk,
            'pasted_on': pk
        }, function(data) {
            if(data == 'OK') {
                window.location.reload();
            } else {
                if (!$('#changelist .errornote').length) {
                    $('<p class="errornote"></p>').hide().prependTo('#changelist');
                }
                $('#changelist .errornote').text(data).fadeIn();

                setTimeout("$('.errornote').fadeOut()", 5000);
            }
        });

    return false;
}

Conclusions

As you can see good software design might help you improve your code whilst adding new features.
It's amazing what you can do with a couple of functions and a few extra lines of code.

We can now create our unique Home Page and don't allow it to be used as subpage.

(django)

4 comments

07.10.2010
Edvinas

Hi,

Thank you for these articles. They have been a tremendous help.

I have a question about the js file, page_toolbox.js, however. I cannot figure out where the above code should live. There is no file named page_toolbox.js in the the feincms media. Is this because the file only existed in a previous version, and if so, where should it be in the newer versions of feincms? Or am I missing something else entirely?

Thank you again, great work.

Edvinas


08.10.2010
Marco Fucci

Hi Edvinas,
lot has been changed since version 1.1.
The validation code should work just fine whilst the UX enhancements aren't valid anymore.
I'll try to update these posts to make my code compatible with the latest version when I have time but in the meantime you might need to start doing it yourself.

Cheers.


19.10.2010
Marco Fucci

Hi Edvinas,
I've tested my code with FeinCMS 1.1.4, which is the latest stable version at the moment, and it works great (you might need a few design tweaks here and there though).

I haven't tried it with the development branch yet but I'll definitely do it when if becomes stable.

In the meantime, if you want to use the features I added you have to use FeinCMS 1.1.4 or wait for a stable release.

Hope this helps,
cheers.


18.05.2011
Clothes

Big thanks





Add your comment Your comment won't be synchronized with external services.

name: your real name

email: won't be published

website: (optional) your website

comment: your comment