Tuesday, February 28, 2006

Python: Django Custom Tags

In my last post, I complained that code like the following is redundant:
<tr class="fieldrow">
<th><label for="id_subject">Subject:</label></th>
<td>
{{ form.subject }}<br>
{% if form.subject.errors %}
<div class="error">{{ form.subject.errors|join:", " }}</div>
{% endif %}
</td>
</tr>

<tr class="fieldrow">
<th><label for="id_name">
Poster's Name:
</label></th>
<td>
{{ form.name }}<br>
{% if form.name.errors %}
<div class="error">{{ form.name.errors|join:", " }}</div>
{% endif %}
</td>
</tr>

<tr class="fieldrow">
<th><label for="id_email">
Poster's Email:
</label></th>
<td>
{{ form.email }}<br>
{% if form.email.errors %}
<div class="error">{{ form.email.errors|join:", " }}</div>
{% endif %}
</td>
</tr>

<tr>
<td></td>
<td>
{{ form.body }}<br>
{% if form.body.errors %}
<div class="error">{{ form.body.errors|join:", " }}</div>
{% endif %}
</td>
</tr>
What I really want is:
{% load formutils %}
{% fieldrow form "subject" %}Subject:{% endfieldrow %}
{% fieldrow form "name" %}Poster's Name:{% endfieldrow %}
{% fieldrow form "email" %}Poster's Email:{% endfieldrow %}
{% fieldrow form "body" %}{% endfieldrow %}
This would allow me to create new forms and form fields quickly. Naturally, there could be other custom tags besides just "fieldrow" for different style layouts. This is just the beginning. Well, I started by creating a custom tag:
"""These are custom tags for creating forms more easily."""

from django.core import template
from django.core.template import Context, loader


register = template.Library()


@register.tag
def fieldrow(parser, token):
"""This tag creates a table row with a form field.

Example: {% fieldrow form "name" %}Name:{% endfieldrow %}

form -- This is a formfields.FormWrapper object.
name -- This is the name of the field. It may be a variable or a quoted
string. If it is a quoted string, it may not contain spaces.
Name: -- This is the label for the field.

Note, I'll use <tr class="fieldrow">.

"""
try:
tag_name, form, fieldname = token.contents.split()
except ValueError:
raise template.TemplateSyntaxError(
"%s tag requires two arguments" % token.contents[0])
nodelist = parser.parse(('endfieldrow',))
parser.delete_first_token()
return FieldRow(form, fieldname, nodelist)


class FieldRow(template.Node):

def __init__(self, form, fieldname, nodelist):
self.form = form
self.fieldname = fieldname
self.nodelist = nodelist

def render(self, context):
form = template.resolve_variable(self.form, context)
fieldname = resolve_variable_or_string(self.fieldname, context)
field = form[fieldname]
label = self.nodelist.render(context)
t = loader.get_template('bulletin/fieldrow')
c = Context({"form": form, "fieldname": fieldname,
"field": field, "label": label})
return t.render(c)


def resolve_variable_or_string(s, context):
"""s may be a variable or a quoted string.

If s is a quoted string, unquote it and return it. If s is a variable,
resolve it and return it.

"""
if not s[0] in ("'", '"'):
return template.resolve_variable(s, context)
if s[-1] == s[0]:
s = s[:-1] # Strip trailing quote, if any.
s = s[1:] # Strip starting quote.
return s
This made use of a template:
{% comment %}
This template is used by the fieldrow custom tag to create a table row with
a form field.
{% endcomment %}

<tr class="fieldrow">
<th><label for="id_{{ fieldname }}">
{{ label }}
</label></th>
<td>
{{ field }}<br>
{% if field.errors %}
<div class="error">{{ field.errors|join:", " }}</div>
{% endif %}
</td>
</tr>
Viola! I'm done. It works.

Now, without a doubt, writing that custom tag is a pain. The hard part is that I have to parse the contents of the custom tag, resolve variables, etc. It sure would be nice if the templating system did that stuff for me. I personally think it would be much nicer if I could simply create a block that takes arguments. Consider something like:
{% block fieldrow(form, fieldname, field) %}
<tr class="fieldrow">
<th><label for="id_{{ fieldname }}">
{{ yield }}
</label></th>
<td>
{{ field }}<br>
{% if field.errors %}
<div class="error">{{ field.errors|join:", " }}</div>
{% endif %}
</td>
</tr>
{% endblock %}

{% fieldrow(form, "name", form.name) %}Subject's Name:{% endfieldrow %}
Sure, a template author might not be able to write that "fieldrow" block. The template authors I work with do understand code like this. If nothing else, a programmer could create a separate template containing that block. He'd be able to avoid duplicating code with very little work. Having the templating system handle parsing arguments and resolving variables would make this task much easier than creating a custom tag.

4 comments:

Anonymous said...

Thanks, just stole this.

Joost Meijer said...

Thank you very much for posting this information. The only problem that I encountered was that the import statements are not valid with the development version of Django. And it took me a while to figure out where to place the code. This post gives a thorough explanation of how this should de done.

Thanks Again!

Brian Luft said...

Nice writeup. Similar to what Peter Baumgartner described in his technique for dealing with form fields in a clean way.
I often see new Django users confused about the use of template tags versus views. This is a great illustration of how template tags can save a lot of redundant code in an elegant manner.

Shannon -jj Behrens said...

> Nice writeup.

Thanks.