Feb 06

Elegant Application Integration with Hooks

Solving the Django multi-site conundrum

In a previous post, I lamented the corner into which I seemed to have developed myself into as I am creating a CMS framework on Django. The essential problem: If pages are created on-the-fly in the Admin, and these pages are dispatched from a catch-all view, how does one go about tying in form processing or application logic to a specific page, without touching the underlying framework? The idea of my framework was to provide a core CMS around which I could add additional functionality without having to mess around with the framework application proper.

I finally have developed an elegant solution to this problem, simply called Hooks. Here's the basic implementation:

class Hook:
hookList = {}

def __init__(self, hookname=''):
self.callables = []
self.forms = {}
self.name = hookname
Hook.hookList[hookname] = self

def register_call(self, fn):
self.callables.append(fn)

def register_form(self, cms_form, form_name):
if form_name not in self.forms:
self.forms[form_name] = cms_form

def call(self, request, context):
newContext={}
redirect=None
#Add the forms to the context, if any are registered. Pass form POST info if this is a POST.
#CMSForm handlers are automatically registered as a callable.
if self.forms:
for form_name, Form in self.forms.iteritems():
if request.method == 'POST':
form = Form(request.POST)
self.register_call(form.handle_form)
else:
form = Form()
newContext[form_name] = form

for fn in self.callables:
logging.debug("Calling: %s" % fn)
(ctx, redirect) = fn(request,context)
if ctx:
newContext.update(ctx)
return (newContext, redirect)

The concept is simple: Create a class, Hook,  which can maintain a registry of named class instances, each of which maintains a registry of callables. Applications register with a specific Hook class instance by name, by adding one or more functions to that hook instance's callables registry with the register_call() method. The Hook class implements a call method which calls the chain of callables one at a time, passing each the current request and context, and receiving context updates and/or page redirects.

Notice that the Hook class maintains it's own static instance registry, hookList{}. In pratice, it is never necessary to instantiate a Hook instance manually. Rather, the get_hook() function is provided. It returns a Hook instance by name, if it exists, and if not, it creates it:


def get_hook(hookname):
if hookname not in Hook.hookList:
hook = Hook(hookname)
return Hook.hookList[hookname]

Optionally, an application may choose to utilize a CMSForm class, and register its forms with the hook with the register_form() method. Such forms have their own integrated form processing method: handle_forms(), which is automatically added to the callables list. For many apps, this is all that will be necessary. the CMSForm class is an abstract class, which is just a regular Form class with a handle_form() method:

class CMSForm(forms.Form):
"""
An abstract and utility class which is the base class for all CMS Forms. CMS Forms
handle their own posts via the handle_form function, which is the only
function that is required.
"""
def handle_form(self, request, context):
return (None, None)

All that remains is to provide a way for Pages to tie into a hook. I chose to implement the hook field in my Template model, since my templates are served from the database, and since application functionality is closely tied to template development:

class Template(models.Model):
name = models.CharField(unique=True, max_length=100)
content = models.TextField()
hook = models.CharField(max_length=20, blank=True)

Now it is just a matter of checking for the existence of a hook, and tying it in to my page_dispatch view:

        if target.template.hook:
hook = get_hook(target.template.hook)
(new_context, redirect_page) = hook.call(request, context)
if new_context:
context.update(new_context)
if redirect_page:
target = Page.objects.get(name=redirect_page, site=request.site)

return HttpResponse(render_content(target, request, context=context))

Notice that a hook callable may return a redirect page. This is used if a POST redirects to a different page.

Now, to tie in an application, add it to INSTALLED_APPS, and put a few lines in the application's __init__.py:

from djangocms.cms_app.hooks import get_hook
from forms import LiturgicalYearForm

lcHook = get_hook('liturgical_calc')
lcHook.register_form(LiturgicalYearForm, 'liturgical_year')

In this example, LiturgicalYearForm is a CMSForm instance:

class LiturgicalYearForm(CMSForm):
year = forms.IntegerField()

def handle_form(self, request, context):
new_context = None
if self.is_valid():
year = self.cleaned_data['year']
ly = LiturgicalYear(year)
new_context = {"ly": ly}
return (new_context, None)

And that's it! If "liturgical_calc" is placed in one of my Template's hook field, any page implementing that Template will gain the functionality of this application.

About

I'm a Lutheran pastor, a CTO, a father, amateur photographer, programmer, Irish music fan, and all around geek, but I only have one blog. So, you will find here a mix of theology, photography, geek speak, family news, and whatever else strikes my fancy. If you get confused, there are now categories…

Subscribe

Categories

Elsewhere

Recent Posts

Archive

BlogRoll