Saturday, July 14, 2012

Custom Renderers with Dynamic Content on TurboGears

One of the most common questions we get on the TurboGears mailing lists is actually a group of questions:
  • How do I make a controller return a customized PDF?
  • How do I display a custom graphic to a user?
  • How do I return an Excel spreadsheet?
This all boils down to the same question: How do I generate dynamic content that is tailored for the user and is returned in a non-HTML format? While I won't answer for each format (because of the fact that every format gets built differently), I will show you how to tell TurboGears about the format you want to send to the user.

The actual process of generating the output can be quite complex. For instance, if you are generating a PDF, you could be using ReportLab. If you're generating an Excel 2003 compatible file, you could be using Python-Excel. The list goes on and on. Because of this, I'm going to focus on a simple example: The vCard format. If you want to have your application send vCards (.vcf files), this example will show you how to do it, from beginning to end.

Surprisingly enough, this cen be almost entirely done by modifying the configuration of your TurboGears application. In my case, I added a class to find (and load) a template file, a class to manage the actual rendering, and then changed the base configuration class to register those two classes. I put everything except the template into my app_cfg.py file.

I'll show the classes, and then break down what each piece of the class does.

The Template Loader

from genshi.template import TemplateLoader, NewTextTemplate

class VCardTemplateLoader(TemplateLoader):
    template_extension = '.vcf'

    def get_dotted_filename(self, filename):
        if not filename.endswith(self.template_extension):
            finder = config['pylons.app_globals'].dotted_filename_finder
            filename = finder.get_dotted_filename(
                    template_name=filename,
                    template_extension=self.template_extension)
        return filename

    def load(self, filename, relative_to=None, cls=None, encoding=None):
        """Actual loader function."""
        return TemplateLoader.load(
                self, self.get_dotted_filename(filename),
                relative_to=relative_to, cls=NewTextTemplate, encoding=encoding)

First, we specify the file extension for the template. The normal file extension is .vcf, so I'm going to use that.

Next, the method get_dotted_filename is provided. This function attempts to find a real operating system filename for a given dotted filename that was passed in. A dotted filename will have dots as the path separators. So, for instance, a file name of "/my/path/file.vcf", in dotted form, will be "my.path.file". This function will be given the name that is provided to the @expose decorator. Since we don't have any need for special behavior here, we just go with the default dotted_filename_finder. If there is a need to change the normal search method for finding the dotted filename, we can do so here.

Finally, the load method: This method's job is to return an object that can be rendered. In this case, it will return an object provided by Genshi that represents a loaded template, ready to be rendered. The return value of this method will be passed into the __init__ method of the Renderer class, up next.

The Renderer

from pylons import templating

class RenderVCard(object):
    """Singleton that can be called as the vcard render function."""

    genshi_functions = {} # auxiliary Genshi functions loaded on demand

    def __init__(self, loader):
        if not self.genshi_functions:
            from genshi import HTML, XML
            self.genshi_functions.update(HTML=HTML, XML=XML)
        self.load_template = loader.load

    def __call__(self, template_name, template_vars, **kwargs):
        """Render the template_vars with the Genshi template."""
        template_vars.update(self.genshi_functions)

        doctype = 'text/x-vcard'
        method='vcf'
        kwargs['doctype'] = doctype
        kwargs['method'] = method

        def render_template():
            template = self.load_template(template_name)
            return template.generate(**template_vars).render(encoding=None)

        return templating.cached_template(
            template_name, render_template,
            **kwargs)


This class is set up as a singleton, allowing any thread to use it without having to reinitialize it all the time. genshi_functions are put in place as a dictionary for the class, allowing us to have only one reference to the HTML and XML functions from Genshi itself.

The __init__ method receives an instance of the loader class (from above). Since this rendered is aware that we are using Genshi, it gets a reference to the Genshi HTML and XML functions, setting them up in the genshi_functions dictionary (if this was not already done). It then saves a reference to the loader.

The __call__ method allows the renderer to be called by TurboGears. Failure to include this method will result in TurboGears being unable to render your custom content type, so make sure to provide it! It will be given a name (the name provided to the @expose decorator in your controller), a dictionary of variables, can receive other (optional) keyword arguments.

In our case, we are only going to care about the method and doctype keywords, and even then only to set them to their correct values.  The template_vars dictionary will be updated to include the Genshi functions, allowing HTML and XML to be referenced in the template.

Finally, we declare a function inside of the __call__ method. This allows us to use variables defined in the main function optionally. The final step, the return statement, will attempt to cache the result. If it cannot, or if it cannot use the cached result, then it will call the nested function (render_template), which can use all the variables defined in this scope. We could, alternately, just return the result of render_template, instead of the cached_template.

The Config

from tg.configuration import AppConfig, config

class MyProjectConfig(AppConfig):
    def setup_vcard_renderer(self):
        loader = VCardTemplateLoader(search_path=self.paths.templates,
                                auto_reload=self.auto_reload_templates)

        self.render_functions.vcard = RenderVCard(loader)


base_config = MyProjectConfig()
base_config.renderers = []
#Set the default renderer
base_config.default_renderer = 'genshi'
# Add vcard rendering (genshi plain text renderer)
base_config.renderers.append('vcard')

Here, we define a function and we must be careful about the name: setup_vcard_renderer will set up a rendered named "vcard", and that will be put at the front of the @expose decorator. So, if we want to write this:

@expose('vcard:myvcard')

We're okay. If we want to write this:

@expose('vcf:myvcard')

Then the setup function must be named "setup_vcf_renderer". We also would have to change the last line of the function to say "self.render_functions.vcf" instead of "self.render_functions.vcard".

Note that the body of the setup_renderer function simply creates a loader, and then sets the value for the renderer function to be a new instance of the renderer class from above.

The Template

That's it for code changes. Since I chose to make this into a Genshi rendered template, I also had to write the template. Here it is:

{% python from datetime import datetime; now = datetime.now(); urls = user.links_to_dict(); phones=user.phones_to_dict() %}BEGIN:VCARD
VERSION:2.1
FN:${user.display_name}
{% if user.title %}TITLE:${user.title}{% end %}
{% for phone in phones %}TEL;${phone.upper()};VOICE;${phones[phone]}{% end %}
ADR;HOME:;;${user.streetaddress.replace('\n','=0D=0A')};${user.city};${user.state_province if user.state_province else ""};${user.postal_code};${user.country if user.country else ""}
LABEL;HOME;ENCODING=QUOTED-PRINTABLE:${user.streetaddress.replace('\n','=0D=0A')}=0D=0A${user.city}, ${user.state_province if user.state_province else ""} ${user.postal_code}=0D=0A${user.country if 
user.country else ""}
{% if user.email_address %}EMAIL;PREF;INTERNET:${user.email_address}{% end %}
{% if 'homepage' in urls %}URL:${urls['homepage']}{% end %}
REV:${'%04d%02d%02dT%02d%02d%02dZ' % (now.year, now.month, now.day, now.hour, now.minute, now.second)}
END:VCARD

And that's it. Very simple template, as you can see. It does rely on functions and attributes that are in the user model, but that's a little outside of what I want to address here.

Conclusion

With minor variations, you can take total control over what you want to do. For instance, you can change the renderer class to one that looks at data in the request (tg.request), and generates output graphs using ImageMagick with .png file extensions. You could build your own XML templating syntax (or, use ReportLab Plus) to make .pdf files (again, changing the renderer class).

The possibilities are endless. You have control, from the way the template is found, to the way it is loaded, to the way it is rendered. If you would like to see this code in one file, it's available in our tutorials repository.

1 comment:

Emily Kovacs said...

Nice blog, TurboGears is very powerful... BTW, I also have a blog and a web directory, would you like to exchange links? let me know on emily.kovacs14@gmail.com