Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Embedding Mako into Django

Jerry Stratton, May 17, 2007

This is my second step in generating PDF dynamically in Django via Python. In PDF and Python I wrote about how to use ReportLab to dynamically generate simple PDF documents. As part of getting Django to produce dynamic PDF, I also hoped to be able to embed Python itself into Django’s templates.

The first thing I tried to do was work with Python’s built-in eval and compile functionality, but that was a dismal failure. I could barely get it to work on a single line of code, let alone work with multiple lines of code. Today, however, it occurred to me that there must be tools to embed Python into text (i.e., PHP-like Python), and that if one of them would accept a string of text, I could extend Django’s template language to accept a block of that other template language.

It may seem a little strange to embed Python into a template language that is itself implemented in Python. But because Django is implemented in Python, it is sometimes easier to work with the data it holds by using Python. In this extension, for example, I will need to pass the context that Django sends, which in turn consists of all of the Django models in use on this page.

Myghty and Mako were the first that came up in a Google search, and as far as I can tell only Mako accepts strings easily. Sealing the deal was that the very front page of Mako’s web site told me exactly what I needed to do:

  • from mako.template import Template
  • print Template("hello ${data}!").render(data="world")

It seemed obvious that making a Django block tag to accept Mako templating code would be dead easy, and it was. The hardest part was figuring out how to send the context as parameters. (Turn it into a dict and preface it with ** in the method call.)

Here’s the entire code to create a mako block in Django:

[toggle code]

  • from django import template
  • import mako.template as mako
  • def do_mako(parser, token):
    • nodelist = parser.parse(('endmako',))
    • parser.delete_first_token()
    • return makoNode(nodelist)
  • class makoNode(template.Node):
    • def __init__(self, nodelist):
      • self.nodelist = nodelist
    • def render(self, context):
      • block = self.nodelist.render(context)
      • #turn the context into a dict
      • parameters = {}
      • [parameters.update(parameter) for parameter in context]
      • #render with Mako
      • rendered =  mako.Template(block, format_exceptions=True).render(**parameters)
      • return rendered
  • register = template.Library()
  • register.tag('mako', do_mako)

Save it as something like “musicalfish.py” in a folder called “templatetags” in your app’s folder. (You’ll need to also create a blank __init__.py to turn the templatetags folder into a Python package.) Load it into your Django template as you would any other extension, something like:

  • {% load musicalfish %}

at the top of your template file.

Once you have that loaded, you can do things like:

[toggle code]

  • {% mako %}
  • <dl>
  • %for child in page.children:
    • <dt>${child.title}</dt>
    • <dd>${child.description}</dd>
  • %endfor
  • </dl>
  • {% endmako %}

The “page” variable is part of the context that my Django pages receive; it contains most of the page’s content.

Mako’s syntax is simple. Each of those lines after the % is a standard Python control block. Each item inside ${…} is standard Python. Whatever it returns will be displayed. And if you want to drop completely into Python, you can even use <% and %> to mark it off.

Note that this mako tag actually runs the block through both Django’s parser and Mako’s. That worries me. Although as far as I can tell the languages don’t overlap, they might in the future. But I don’t know how to get the text out of nodelist without rendering it.

Using Mako to generate PDF dynamically in Django

The first issue I ran into is that, as far as I can tell, you aren’t allowed to print inside of a Python block. Mako doesn’t collect printed output and append it to the returned text. Any text that you want to display you’ll need to collect (say, in a list) and then display it using ${}.

This can cause problems with methods that expect to handle their own I/O, such as ReportLab Toolkit. Reportlab doesn’t do any output except write to a file-like object. However, it’s fairly easy to construct a simple object that can be written to as a file using StringIO.

The bigger problem is ascii encoding. Mako seems to expect that everything coming through is text of some sort (or, more likely, I just don’t understand how to handle encoding). This doesn’t seem to be a problem with PDF, but I wonder if it will allow image creation. That’s an issue for another day, however. I don’t need it now.

What I want now is to know that I can create PDF dynamically in a Django template. Here’s a simple example of creating a PDF page via the mako tag:

  • {% mako %}
  • <%
  • from reportlab.platypus import Paragraph, SimpleDocTemplate
  • from reportlab.lib.styles import getSampleStyleSheet
  • from reportlab.lib.pagesizes import letter
  • import StringIO
  • destination = StringIO.StringIO()
  • style = getSampleStyleSheet()
  • pdf = SimpleDocTemplate(destination, pagesize=letter)
  • posts = []
  • posts.append(Paragraph(page.title, style["Heading1"]))
  • posts.append(Paragraph(page.description, style["Normal"]))
  • pdf.build(posts)
  • pdfpage = destination.getvalue().decode("ascii", "ignore")
  • %>
  • ${pdfpage}
  • {% endmako %}

Since it’s generating PDF, that is the entire template. Obviously, I also have to have Django return the correct mimetype, application/pdf. For my site, I have a database of templates. Each page knows what template it uses, and each template record includes the mimetype it should return. So what I end up doing is something like this in views.py:

[toggle code]

  • def makeDetail(page):
    • pcontext = commonContext(page)
    • template, mimetype = page.template()
    • r = render_to_response(template, pcontext)
    • r.headers['Content-Type'] = mimetype + "; charset=utf-8"
    • return r

Which means that I end up with this header:

  • Content-Type: application/pdf; charset=utf-8

Next up, I need to learn how to create multiple columns without having to do the measurements myself.

Update: I forgot to include the lines that import the functionality necessary to register the tag.

  1. <- PDF and Python
  2. Multi-column PDFs ->