Adapters in Django and the Revenge of Zope
Adapters are great.
They're so cool, I even wrote a .NET adapter registry called Adaptation, which I hope will come in useful on a forthcoming top-secret project (of which I have a couple in the works).
Simple - up to now
Up until now I haven't had to do anything particularly complex in Django. It's all been small-scale, fairly limited, and - crucially - not really designed to be reusable. Now I'm starting to develop Django applications that I want to reuse, and am running across age-old problems: how to write apps to be reusable, and how to reuse other Python code that either wasn't really written with reuse in mind by not offering the right extension points.
django.contrib.contenttypes
One part of the puzzle is provided by Django's contenttypes module (django.contrib.contenttypes). In a nutshell, this lets you declare 'fake' foreign keys in your models to other models that you don't yet know about. For example, one of my reusable applications is a small workflow component. I can't know in advance what Django models I'm going to be workflowing; however, the contenttypes' GenericForeignKey field lets me abstract that away. (More detail on this package is for another day; it's woefully underdocumented.)
Actions in Django
The second piece of the puzzle is best illustrated with an example.
I have a bunch of models in an application called 'pm'. These are Task, Milestone, and other project-management related entities. I want to display them on the web, and I want each one to have some context-sensitive tabs - such as 'Overview', 'Detail', etc. Each of these tabs has a title and a URL.
(If you're familiar with the Zope 2 CMF then yes, I'm basically talking about a simple form of actions.)
Now - where do I put these definitions?
I want them to be declarative in Python code, so I don't have a lot of ridiculous "if content type is foo then display these tabs" in template code.
The first place that occurs to me to put them is therefore in the model itself - just have a list of dicts. Models would look something like this:
class Task:
actions = [
{ 'title': 'Overview',
'url': reverse(taskoverview)
}
]
title = models.CharField(max_length=10)
... etc ...
I don't really like this approach.
- These actions are really UI-specific, and don't belong in the data model definition
- We really need the URL to be context-specific - in this case, I probably need some form of Task ID in the 'url' value.
Direct Attribute Setting
Another option could be to simply set attributes on the model classes from outside. So in some module-level web code, have something like this:
from pm.models import Task
Task.actions = [{'title': 'Overview', ... etc ...
This solves the problem of having UI code in the model; however, it still suffers from the same problem of not having an instance to compute variable URLs. Direct attribute setting also feels somewhat 'icky'.
Zope 3 Adapters
Zope 3 is not an application server in the same way as Zope 2 is - it's more like a set of libraries. And this means that you can take parts of Zope 3 and use them in other applications.
In this case, we're going to take zope.component and zope.interface, and use them in our Django application.
Installing the eggs
Installation is easy.
easy_install zope.interface
easy_install zope.component
Defining interfaces
First of all, we define what we expect something that can have actions to provide in terms of an interface:
from zope import interface
from zope import component
class IActionProvider(interface.Interface):
"""
Interface specifying that this object can provide actions.
The actions attribute is an iterable of instances of the
Action class.
"""
actions = interface.Attribute("Actions")
Next, we define a marker interface for our Task, and declare that our task model implements it. We'll also need a similar interface for Django's HttpRequest - the reason for that will become clear shortly:
from pm.models import Task
from django.http import HttpRequest
class ITask(interface.Interface): pass
class IHttpRequest(interface.Interface): pass
interface.classImplements(Task, ITask)
interface.classImplements(HttpRequest, IHttpRequest)
Note that we're not doing this in the models code - this need only happen in our UI code, that requires this actions functionality.
The last piece is to define an adapter. The adapter provides the bridge between a Task instance, and something that provides the 'action' attribute specified in the IActionProvider interface. The adapter looks like this:
from django.core.urlresolvers import reverse
from web.views import project import task_details, task_milestones
class TaskActions(object):
interface.implements(IActionProvider)
component.adapts(ITask, IHttpRequest)
def __init__(self, context, request):
self.context = context
self.request = request
def _getActions(self):
actions = []
request = self.request
actions.append(Action(request, 'Overview', task_detail, self.context.id))
actions.append(Action(request, 'Milestones', task_milestones, self.context.id))
return actions
actions = property(_getActions)
component.provideAdapter(ProjectActions)
Note how the adapter also takes the request. This is to allow us to use the request to generate a URL for the task that we're currently looking at.
The call to component.provideAdapter() makes the adapter available to the component architecture.
This adapter also refers to an Action class. This is a very simple data structure:
class Action(object):
def __init__(self, request, title, view, *args, **kwargs):
self.title = title
self.url = reverse(view, args=args, kwargs=kwargs)
self.selected = request.META['PATH_INFO'] == self.url
Using the Adapter
Finally, we define a template tag to display the actions:
from django.template import Library
register = Library()
@register.inclusion_tag('templatetags/actions.html', takes_context=True)
def actions(context):
obj_name = context.get('obj_name', 'object')
obj = context.get(obj_name)
request = context['request']
provider = component.getMultiAdapter((obj, request), IActionProvider)
return {'actions': provider.actions}
After some boilerplate to pull some items (notable the request) out of the context, the key two lines are the two final lines in the function. This performs an adapter lookup to obtain the adapter that we defined, and finally accesses the actions attribute that actually computes the actions for the context object. This is then plugged into the context for the template.
The template itself simply iterates over the Action instances:
<ul class="object-actions">
{% for action in actions %}
<li>
<a href="{{action.url}}"
{%if action.selected%}
class="selected"
{%endif%}>
{{action.title}}
</a>
</li>
{% endfor %}
</ul>
That seems a lot of bother
Well - it is overkill for a single model. However - adding support for actions to any model (or any other Python class) is now as simple as defining an appropriate adapter providing IActionsProvider for that new type - and it will plug cleanly in to this infrastructure.
Judicious use of interfaces and adapters can make incorporating third-party code (or even your own code) extremely simple. In particular, it avoids the need to modify the code you're integrating.
Further Reading
I've glossed over a lot of details here about the ins and outs of the Zope 3 component architecture. If you want to find out more, read the README.txt files in zope.component and zope.interface (which are comprehensive, if somewhat terse) or alternatively buy Philipp von Weitershausen's excellent Web Component Development with Zope 3. Make sure you pick up the second edition.
Comments
1 Robert Gravina says...
Hi! After coming across this post I've gotten excited about the possibility of making my Django apps more reusable. I've looked into Zope 3 and Grok a little before settling on Django (mainly for the admin interface and the wealth of applications out there - I MUCH prefer the design of Grok and Zope3)...
I've tried to apply this to my Django models, to say they implement some interface e.g.
from zope.interface import implements from django.db import models
class Article(models.Model): implements(IArticle) title = models.CharField(max_length=100, help_text="The title of the article.")
But then Django keeps trying to introspect the model and dying...
Have you been able to use zope.interface on your models?
Thanks,
Robert
Posted at 6:01 a.m. on April 22, 2008
2 Dan Fairs says...
As you mention, Django does a lot of introspection on the model classes. I've not tried the exact syntax you describe, only using the classImplements style outside the class. Next time I'm working on this app, I'll give it a go.
Posted at 3:25 p.m. on April 24, 2008
3 Robert Gravina says...
Somehow I managed to miss interface.classImplements in the docs... anyway, I'm keen to use Zope3 CA in my Django app where it makes sense, so it's be great to hear about any more experiences you have had.
If I learn/create anything useful, I'll post here again.
Robert
Posted at 10:37 a.m. on May 5, 2008
4 Chris Johnson says...
Hey - I am curious what your resulting app was...sounds like you worked on a project management app (which is what we are about to undertake in Django also!). I would love to see anything that resulted...we are specifically looking to do agile/scrum development project management.
Thanks in advance,
Chris -- www.ifpeople.net
Posted at 2:06 a.m. on July 4, 2008