Thursday, June 28, 2007

Django Template Block Extraction

I have been building a CMS for my job, in order to empower the Editor to manage all of the web page content. A simple TextField for Textile formatted content was sufficient for the www site, which is only text information. But for the support site, the need to mix managed content with dynamic elements (especially forms) arose quickly.

So how to mix the two?

After discussing implementations with the Editor, we found this syntax agreeable:

h1. Contact Us

p. You can use this form to send us a message.

b(#form).

p. Thank you for you interest.


The magic here is the fake, Textile-like `b` tag, with an ID. This indicates to insert some sort of dynamic element in between the normal Textile content.

Why `b`? On the backend, I realized that I wanted to use the full power of Django templates for any of these dynamic elements. How do you segment off sections of content within a Django template? Blocks.

Now the trick is to render the template as normal, render the `form` block, then replace `b(#form).` with the rendered block.

Here is the view logic:

def render(request, section, template, context_vars={}):
"""
Renders a section with a template. Substitues any
textile-ish `b.` blocks with orphan blocks found in
said template.
"""

# resolve the template and render it to a string
content = render_to_string(template, context_vars)

# add a recursive flatten method to NodeList to make
# getting blocks easy
from django.template import NodeList

def flatten(self):
nodes = []
for node in self:
nodes.append(node)
if hasattr(node, 'nodelist'):
nodes.extend(node.nodelist.flatten())
return nodes

NodeList.flatten = flatten

# load the template
from django.template.loader import get_template
t = get_template(template)

# get all the block nodes, by flattening NodeList
from django.template.loader_tags import BlockNode
nodes = t.nodelist.flatten()

blocknodes = []
for node in nodes:
if node.__class__ is BlockNode:
blocknodes.append(node)

# render blocknodes with context into
# name => content dict
from django.template.context import Context
context = Context(context_vars)

blocks = {}
for blocknode in blocknodes:
blocks[blocknode.name] = blocknode.render(context)

# parse out textile-style block tags b(#block-name)
# and substitute rendered block
p = re.compile(r'(b\(#([^\)]+)\).)')
matches = p.findall(content)
for match in matches:
tag, name = match
try:
content = content.replace(tag, blocks[name])
except:
pass
return HttpResponse(content)


And here is the corresponding template:

{% extends 'layout/one-column.html' %}

{% comment %}
This node is an orphan, and would not be rendered
with a normal render_to_string. It is, however,
possible to render it with the same context, then
replace part of render_to_string and return the
resulting string.
{% endcomment %}

{% block form %}
<form action="" method="post">
<table>
{{ form }}
<tr><td colspan="2" align="right">
<input type="submit" name="submit" value="Submit" />
</td></tr>
</table>
</form>
{% endblock %}

1 comment:

Anonymous said...

Thanks buddy, this helped much!