= Genshi Tutorial = This tutorial is intended to give an introduction on how to use Genshi in your web application, and present common patterns and best practices. It is aimed at developers new to Genshi as well as those who've already used Genshi, but are looking for advice or inspiration on how to improve that usage. == Introduction == In this tutorial we'll create a simple Python web application based on [http://cherrpy.org/ CherryPy 3]. !CherryPy was chosen because it provides a convenient level of abstraction over raw CGI or [http://wsgi.org/wsgi WSGI] development, but is less ambitious than full-stack web frameworks such as [http://pylonshq.com/ Pylons] or [http://www.djangoproject.com/ Django], which tend to come with a preferred templating language, and often show significant bias towards that language. The application is a stripped-down version of sites such as [http://reddit.com/ reddit] or [http://digg.com/ digg]: it lets users submit links to online articles they find interesting, and then lets other users comment on those stories. Just for kicks, we'll call that application '''Geddit''' We'll keep the project as simple as possible, while still showing many of Genshi features and how to best use them: * For persistence, we'll use native Python object serialization (via the `pickle` module), instead of an SQL database and an ORM. * There's no authentication of any kind. Anyone can submit links, anyone can comment. [[PageOutline(2-3, Content, inline)]] == Getting Started == === Prerequisites === First, make sure you have !CherryPy 3.0.x installed, as well as recent versions of [http://formencode.org/ FormEncode] and obviously Genshi. You can download and install those manually, or just use [http://peak.telecommunity.com/DevCenter/EasyInstall easy_install]: {{{ $ easy_install CherryPy $ easy_install FormEncode $ easy_install Genshi }}} === The !CherryPy Application === Next, set up the basic !CherryPy application. 1. Create a directory that should contain the application 2. Inside that directory create a Python package named geddit by doing the following: * Create a `geddit` directory * Create an empty file called `__init__.py` inside the `geddit` directory 3. Inside the `geddit` package directory, create a file called `controller.py` with the following content: {{{ #!python #!/usr/bin/env python import operator, os, pickle, sys import cherrypy class Root(object): def __init__(self, data): self.data = data @cherrypy.expose def index(self): return 'Geddit' def main(filename): data = {} # We'll replace this later # Some global configuration; note that this could be moved into a # configuration file cherrypy.config.update({ 'request.throw_errors': True, 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', 'tools.decode.on': True, 'tools.trailing_slash.on': True, 'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)), }) cherrypy.quickstart(Root(data), '/', { '/media': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'static' } }) if __name__ == '__main__': main(sys.argv[1]) }}} Enter the tutorial directory in the terminal, and run: {{{ $ PYTHONPATH=. python geddit/controller.py geddit.db }}} You should see a log message pointing you to the URL where the application is being served, which is usually http://localhost:8080/. Visiting that page will respond with just the string “Geddit”, as that's what the `index()` method of the `Root` object returns. Note that we've configured !CherryPy to serve static files from the `geddit/static` directory. !CherryPy will complain that that directory does not exist, so create it, but leave it empty for now. We'll add static resources later on in the tutorial. === Basic Template Rendering === So far the code doesn't actually use Genshi, or even any kind of templating. Let's change that. Inside of the `geddit` directory, create a directory called `templates`, and inside that directory create a file called `index.html`, with the following content: {{{ #!genshi
Welcome!
}}} This is basically an almost static XHTML file with some simple variable substitution: the string `$title` will be replaced by a variable of that name that we pass into the template from the controller. There are couple of important things to point out here: * Variables substituted into templates, such as `$title` in our example, can be of any Python data type. Genshi will convert the value to a string and insert the result into the generated output stream. * You generally do not need to worry about XML-escaping such variables. Genshi will automatically take care of that when the template is serialized. We'll look into the details of this process later. * The template will be parsed by Genshi using an XML parser, which means that '''it needs to be well-formed XML'''. If you know HTML but are unfamiliar with XML/XHTML, you will need to read up on the topic. Here are a couple of good references: * [http://www.w3schools.com/xhtml/xhtml_html.asp Differences Between XHTML And HTML] at W3Schools * [http://www.sitepoint.com/article/xhtml-introduction/2 XHTML - An Introduction] at !SitePoint * [http://www.webmonkey.com/00/50/index2a.html XHTML Overview] at Webmonkey * That the template uses XHTML does not mean that your web-application will generate XHTML! You can choose whether you'd rather just generate good old HTML 4.01, because despite all the hype, that's still the format that works best in most browsers (see [http://webkit.org/blog/?p=68 this blog post] over at Surfin' Safari for some background). We now need to change the controller code so that this template is used. First, add the Genshi `TemplateLoader` to the imports at the top of the `geddit/controller.py` file, and instantiate a loader for the `geddit/templates` directory: {{{ #!python import cherrypy from genshi.template import TemplateLoader loader = TemplateLoader( os.path.join(os.path.dirname(__file__), 'templates'), auto_reload=True ) }}} Next, change the implementation of the `index()` method of the `Root` class to look like this: {{{ #!python @cherrypy.expose def index(self): tmpl = loader.load('index.html') return tmpl.generate(title='Geddit').render('html', doctype='html') }}} This asks the template loader for a template named `index.html`, generates the output stream, and finally serializes the output to HTML. When you now reload the page in your browser, you should get back the following HTML response: {{{ #!xmlWelcome!
}}} === The Data Model === To continue, we'll need to first add some Python classes to define the data model the application will use. As mentioned above, we're using a simple pickle file for persistence, so all we need to do here is create a couple of very simply Python classes. [[Image(model.png)]] Inside the `geddit` directory, create a file named `model.py`, with the following content: {{{ #!python from datetime import datetime class Link(object): def __init__(self, username, url, title): self.username = username self.url = url self.title = title self.time = datetime.utcnow() self.id = hex(hash(tuple([username, url, title, self.time])))[2:] self.comments = [] def __repr__(self): return '<%s %r>' % (type(self).__name__, self.title) def add_comment(self, username, content): self.comments.append(Comment(username, content)) class Comment(object): def __init__(self, username, content): self.username = username self.content = content self.time = datetime.utcnow() def __repr__(self): return '<%s>' % (type(self).__name__) }}} You'll need to import those classes in `geddit/controllers.py`, just below the other imports: {{{ #!python from geddit.model import Link, Comment }}} And in the `main()` function, let's add some code to read our data from the pickle file, and write it back: {{{ #!python def main(filename): # load data from the pickle file, or initialize it to an empty list if os.path.exists(filename): fileobj = open(filename, 'rb') try: data = pickle.load(fileobj) finally: fileobj.close() else: data = {} def _save_data(): # save data back to the pickle file fileobj = open(filename, 'wb') try: pickle.dump(data, fileobj) finally: fileobj.close() cherrypy.engine.on_stop_engine_list.append(_save_data) }}} Now let's add some initial content to our “database”. '''Note: You'll need to stop the !CherryPy server to do the following, otherwise your changes will get overwritten'''. In the terminal, from the tutorial directory, launch the interactive Python shell by executing `PYTHONPATH=. python`, and enter the following code: {{{ #!pycon >>> from geddit.model import * >>> link1 = Link(username='joe', url='http://example.org/', title='An example') >>> link1.add_comment(username='jack', content='Bla bla bla') >>> link1.add_comment(username='joe', content='Bla bla bla, bla bla.') >>> link2 = Link(username='annie', url='http://reddit.com/', title='The real thing') >>> import pickle >>> pickle.dump({link1.id: link1, link2.id: link2}, open('geddit.db', 'wb')) }}} You should now have two links in the pickle file, with the first link having two comments. Start the CherryPy server again by running: {{{ $ PYTHONPATH=. python geddit/controller.py geddit.db }}} == Making the Application “Do Stuff” == === Extending the Template === Now let's change the `Root.index()` method in `geddit/controller.py` to pass the links list to the template: {{{ #!python @cherrypy.expose def index(self): links = sorted(self.data.values(), key=operator.attrgetter('time')) tmpl = loader.load('index.html') stream = tmpl.generate(links=links) return stream.render('html', doctype='html') }}} And finally, we'll modify the `index.html` template so that it displays the links in a simple ordered list. While we're at it, let's add a link to submit new items: {{{ #!genshiIn reply to ${comment.username} at ${comment.time.strftime('%x %X')}:
${comment.content}}}} Phew! We should be done with the commenting now. Play around with the application a bit to get a feel for what we've achieved so far. The next section will look into various things that can be done to further improve the application. [[Image(tutorial04.png)]] == Advanced Topics == === Adding an Atom Feed === Every web site needs an RSS or [http://www.atomenabled.org/ Atom] feed these days. So we shall provide one too. Adding Atom feeds to Geddit is fairly straightforward. First, we'll need to add auto-discovery links to the index and detail pages. Inside the `` element of `geddit/templates/index.html`, add: {{{ #!genshi }}} And inside the `` element of `geddit/templates/info.html`, add: {{{ #!genshi }}} Now we need to add the `feed()` method to our `Root` class in `geddit/controller.py`: {{{ #!python @cherrypy.expose @template.output('index.xml', method='xml') def feed(self, id=None): if id: link = self.data.get(id) if not link: raise cherrypy.NotFound() return template.render('info.xml', link=link) else: links = sorted(self.data.values(), key=operator.attrgetter('time')) return template.render(links=links) }}} Note that this method dispatches to different templates depending on whether the `id` parameter was provided. So, for the URL `/feed/`, we'll render the list of links using the template `index.xml`, and for the URL `/feed/{link_id}/`, we'll render a link and the list of related comments using the template `info.xml`. The templates for this are also pretty simple. First, `geddit/templates/index.xml`: {{{ #!genshi