PylonsHQ.

Layout: Fixed-width

QuickWiki with Chameleon

Why

Let's say you love TAL, hate Mako, or just want to see how you can trade out a template system.

First Things First

Before You Start

This recipe assumes you have set up Pylons and have the QuickWiki project set up using Mako.

If you haven't done so, either

  1. Follow the QuickWiki tutorial; or
  2. Grab the QuickWiki egg.

    easy_install QuickWiki

Get Chameleon

Grab the Chameleon egg.

easy-install -U Chameleon

Chameleon or chameleon.zpt?

Chameleon is one package that used to be chameleon.core and chameleon.zpt. Per Malthe, the new chameleon.core and chameleon.zpt releases are just shims around Chameleon.

Update the Pylons environment configuration

In Quickwiki/quickwiki/config, update environment.py such that you

  1. import TemplateLoader instead of TemplateLookup
  2. remove handle_mako_error
  3. replace the Mako template config with chameleon template config:

    """Pylons environment configuration"""
    import os
    
    from chameleon.zpt.loader import TemplateLoader 
    from pylons import config
    from sqlalchemy import engine_from_config
    
    import quickwiki.lib.app_globals as app_globals
    import quickwiki.lib.helpers
    from quickwiki.config.routing import make_map
    from quickwiki.model import init_model
    
    def load_environment(global_conf, app_conf):
        """Configure the Pylons environment via the ``pylons.config``
        object
        """
        # Pylons paths
        root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        paths = dict(root=root,
                     controllers=os.path.join(root, 'controllers'),
                     static_files=os.path.join(root, 'public'),
                     templates=[os.path.join(root, 'templates')])
    
        # Initialize config with the basic options
        config.init_app(global_conf, app_conf, package='quickwiki', paths=paths)
    
        config['routes.map'] = make_map()
        config['pylons.app_globals'] = app_globals.Globals()
        config['pylons.h'] = quickwiki.lib.helpers
    
        # Create the chameleon TemplateLoader
        config['pylons.app_globals'].pt_loader = TemplateLoader(
                paths['templates'], auto_reload=True) 
    
        # Setup the SQLAlchemy database engine
        engine = engine_from_config(config, 'sqlalchemy.')
        init_model(engine)
    
        # CONFIGURATION OPTIONS HERE (note: all config options will override
        # any Pylons config options)

Build the "pt.py" Library

In Quickwiki/quickwiki/lib, create pt.py:

from pylons.templating import pylons_globals
from pylons.templating import cached_template

def render_pt(template_name, cache_key=None, cache_type=None,
        cache_expire=None):
    """Render a page template with z3c.pt."""

    def render_template():
        globs = pylons_globals()
        template = globs["app_globals"].pt_loader.load(template_name)
        return template(**globs)

    return cached_template(template_name, render_template, cache_key=cache_key,
            cache_type=cache_type, cache_expire=cache_expire)

def render_text(template_name, cache_key=None, cache_type=None,
        cache_expire=None):
    """Render a text template with z3c.pt."""

    def render_template():
        globs = pylons_globals()
        template = globs["app_globals"].pt_loader.load(template_name,
                format="text")
        return template(**globs)

    return cached_template(template_name, render_template, cache_key=cache_key,
            cache_type=cache_type, cache_expire=cache_expire)

Where Did This Come From?

The code of pt.py is based on a discussion on Google Groups {pylons-discuss).
Major props to wiggy and the other folk on that thread!

Update the base Controller API

In Quickwiki/quickwiki/lib, update base.py such that you import the (just-created) render_pt function as render:

"""The base Controller API

Provides the BaseController class for subclassing.
"""
from pylons.controllers import WSGIController
from quickwiki.lib.pt import render_pt as render

from quickwiki.model import meta

class BaseController(WSGIController):

    def __call__(self, environ, start_response):
        """Invoke the Controller"""
        # WSGIController.__call__ dispatches to the Controller method
        # the request is routed to. This routing information is
        # available in environ['pylons.routes_dict']
        try:
            return WSGIController.__call__(self, environ, start_response)
        finally:
            meta.Session.remove()

Replace the Templates

  1. In Quickwiki/quickwiki/templates, you'll find base.mako. Replace it with base.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    
    
    <metal:base define-macro="base">
    <html>
      <head>
        <title>QuickWiki</title>
        ${h.stylesheet_link('/quick.css')}
      </head>
    
      <body>
        <pre>using Chameleon</pre>
        <div class="content">
          <h1 class="main"
              tal:content="c.title" />
    
          <div id="flash">
            <span class="message"
                  tal:repeat="flash h.flash.pop_messages()"
                  tal:content="flash" />
          </div>
    
          <metal:body define-slot="body" />
    
          <p class="footer"
             tal:define="action request.environ['pylons.routes_dict']['action']">
            Return to the ${h.link_to('FrontPage', url('/'))}
            <tal:editLink condition="action not in ('index', 'edit')">
              | ${h.link_to('Edit ' + c.title, url('edit_page', title=c.title))}
            </tal:editLink>
            <tal:titleListLink condition="action not in ('index')">
              | ${h.link_to('Title List', url('pages'))}
            </tal:titleListLink>
          </p>
        </div>
      </body>
    </html>
    </metal:base>
    

    What's With The <pre>?

    Okay, strictly speaking, the <pre>using Chameleon</pre> is not necessary. But, if you have kept the original QuickWiki, it's a nice visual cue to let you know which one you're running.

  2. In Quickwiki/quickwiki/templates/pages, you'll find edit.mako. Replace it with edit.pt:

    1
    2
    3
    4
    5
    6
    7
    8
    <metal:base use-macro="c.base">
      <metal:body fill-slot="body">
        ${h.secure_form(url('save_page', title=c.title))}
          ${h.textarea(name='content', rows=7, cols=40, content=c.content)} <br />
          ${h.submit(value='Save changes', name='commit')}
        ${h.end_form()}
      </metal:body>
    </metal:base>
    

  3. In Quickwiki/quickwiki/templates/pages, you'll find index.mako. Replace it with index.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <metal:base use-macro="c.base">
      <metal:body fill-slot="body">
        ${h.secure_form(url('delete_page'))}
    
        <ul id="titles">
          <li tal:repeat="title c.titles">
            ${h.link_to(title, url('show_page', title=title))} -
            ${h.checkbox('title', title)}
          </li>
        </ul>
    
        ${h.submit('delete', 'Delete')}
    
        ${h.end_form()}
    
      </metal:body>
    </metal:base>
    

  4. In Quickwiki/quickwiki/templates/pages, you'll find new.mako. Replace it with new.pt:

    1
    2
    3
    4
    5
    6
    7
    <metal:base use-macro="c.base">
      <metal:body fill-slot="body">
        <p>This page doesn't exist yet.
          <a href="${url('edit_page', title=c.title)}">Create the page</a>.
        </p>
      </metal:body>
    </metal:base>
    

  5. In Quickwiki/quickwiki/templates/pages, you'll find show.mako. Replace it with show.pt:

    1
    2
    3
    4
    5
    <metal:base use-macro="c.base">
      <metal:body fill-slot="body">
        ${h.literal(c.content)}
      </metal:body>
    </metal:base>
    

Update the Controller

In Quickwiki/quickwiki/controllers, update pages.py such that you

  1. define c.base in _before_() and
  2. render the .pt files instead of .mako files:

    import logging
    from cgi import escape
    
    from pylons import request, response, session, tmpl_context as c
    from pylons.controllers.util import abort, redirect_to
    from pylons.decorators.secure import authenticate_form
    
    from quickwiki.lib.base import BaseController, render
    from quickwiki.lib.helpers import flash
    from quickwiki.model import Page, wikiwords
    from quickwiki.model.meta import Session
    
    log = logging.getLogger(__name__)
    
    class PagesController(BaseController):
    
        def __before__(self):
            self.page_q = Session.query(Page)
            # chameleon.zpt variants need c.base defined
            from chameleon.zpt.template import PageTemplateFile
            c.base = PageTemplateFile("quickwiki/templates/base.pt").macros['base']
    
        def show(self, title):
            page = self.page_q.filter_by(title=title).first()
            if page:
                c.content = page.get_wiki_content()
                return render('pages/show.pt')
            elif wikiwords.match(title):
                return render('pages/new.pt')
            abort(404)
    
        def edit(self, title):
            page = self.page_q.filter_by(title=title).first()
            if page:
                c.content = page.content
            return render('pages/edit.pt')
    
        @authenticate_form
        def save(self, title):
            page = self.page_q.filter_by(title=title).first()
            if not page:
                page = Page(title)
            # In a real application, you should validate and sanitize
            # submitted data throughly! escape is a minimal example here
            page.content = escape(request.POST.getone('content'))
            Session.add(page)
            Session.commit()
            flash('Successfully saved %s!' % title)
            redirect_to('show_page', title=title)
    
        def index(self):
            c.titles = [page.title for page in self.page_q.all()]
            return render('pages/index.pt')
    
        @authenticate_form
        def delete(self):
            titles = request.POST.getall('title')
            pages = self.page_q.filter(Page.title.in_(titles))
            for page in pages:
                Session.delete(page)
            Session.commit()
            # flash only after a successful commit
            for title in titles:
                flash('Deleted %s.' % title)
            redirect_to('pages')

    Watch Those Paths

    A careful reader will notice that the render calls used to start with a forward slash. For example, make sure you change return render('/pages/show.mako') to return render('pages/show.pt')

Notes

I got this working on a Mac (OS 10.5.8), having built out my Pylons environment with the go-pylons.py script, as explained in Getting Started. I used chameleon.zpt 1.1.1.

However, just because I got this working, it doesn't mean I wrote this recipe correctly—apologies if I omitted any steps!

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

Powered by Pylons - Contact Administrators