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
- Follow the QuickWiki tutorial; or
- 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
- import TemplateLoader instead of TemplateLookup
- remove handle_mako_error
- 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). |
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
- 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.
- 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>
- 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>
- 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>
- 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
- define c.base in _before_() and
- 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!