Friday, September 15, 2006

TurboGears decorator madness: linkify

One of the neat features in TurboGears is the @jsonify decorator. It uses RuleDispatch to define generic functions to convert data model objects into JSON notation for use in AJAXish applications. For example, TurboGears provides this default converter for the User identity class:

@jsonify.when('isinstance(obj, User)')
def jsonify_user(obj):
result = jsonify_sqlobject( obj )
del result['password']
result["groups"] = [g.group_name for g in obj.groups]
result["permissions"] = [p.permission_name for p in obj.permissions]
return result


The first line lets the default JSONifier rules handle the object; after that, it removes the "password" field for security reasons, and then adds support for fields that the default rules can't handle (like joins). The @jsonify.when decorator handles mapping the default jsonify() function to the type-specific version, so when you want to return a User object converted to JSON, you just return "jsonify(myUser)" and you're done.

This approach can be used for other purposes. For example, in one project, I kept running across is the need to render references to objects as links to view that object. For example, say you have a app that renders the text

Last updated at 12:00 by Joe

with the template snippet:

<p>Last updated at ${thing.last_update_time} by ${thing.update_user.display_name}</p>


Easy and straightforward. But if you want to link "Joe" to Joe's user profile page, then every time you want to do this, you end up writing something like:

<p>Last updated at ${thing.last_update_time} by 
<a href="${'/users/%d' % thing.update_user.id}"
title="User profile for ${thing.update_user.display_name}"
${thing.update_user.display_name}
</a>
</p>


Then hours later you kick yourself because you find one place out of 20 where you made a typo in this monstrosity (see if you can find the one in the example above!).

So I "borrowed" jsonify's approach and created linkify.py for the project:

import dispatch
import model
import types

from elementtree import ElementTree

# Linkify generic methods... modeled after jsonify

@dispatch.generic()
def linkify(obj):
raise NotImplementedError

@linkify.when('isinstance(obj, model.User)')
def linkify_user(user):
link = ElementTree.Element('a',
href='/user/%d' % user.id,
title='User Profile for "%s"' % user.display_name)
link.text = user.display_name
return link


Then, in your controllers.py, you can make this available to templates:

# Add linkify to tg namespace
import linkify
def provide_linkify(vars):
vars['linkify'] = linkify.linkify
turbogears.view.variable_providers.append(provide_linkify)


And now, in your template, you just write:

<p>Last updated at ${thing.last_update_time} by ${tg.linkify(thing.update_user)}</p>


Much, much nicer.

2 comments:

Anonymous said...

I think it would actually better to use a TG widget here. The widget would contain the template (you could use Kid instead of ET Elements), and the logic for the output.

Tim Lesher said...

I agree--normally I would use a widget for something like this.

class UserLinkWidget(Widget):
    template = '<a href="/users/${id}">${name}</a>'
user_link_widget = UserLinkWidget()

The problem is that it becomes cumbersome when you have half a dozen different entities that you want to be able to linkify (say, a User, a Blog, a Message, etc.).
Then you either have to pass in every link widget to every template (and it would end up being a lot), or else expose them all via a variable provider.

Plus "tg.linkify(u)" looks more concise to me than "user_link_widget.display(id=u.id, name=u.display_name)", and doesn't make me remember different code for different object types.

On the other hand, you could do a mix of both methods:

@linkify.when(...)
def linkify_user(obj):
    return user_link_widget.display(id=obj.id, name=obj.display_name)