mike watkins dot ca : Python Web Application Diary, Part Five

Python Web Application Diary, Part Five

In part four of this series we looked at the spec module from QP, and created some basic Sancho test cases. In this installment we'll write a few lines of code for the web UI of the application, our first real exposure to the web workings of QP.

QP Applications

In part two we created file system hierarchies for both a library and our project application. Since we plan on reusing our journal objects and related UI, those components will live in our library and our application itself will import them.

The fundamental requirements of any QP application are:

  1. The application(s) must live somewhere in the QP search path. Issue qp -h at the command line for details.
  2. Applications must define a module called either slash.py or slash.qpy. Both contain Python code, qpy files are QP template-aware Python modules which we'll talk about more shortly.
  3. The slash module must expose two objects, a SitePublisher which defines the publishing environment and any application specific customizations, as well as a SiteDirectory which defines the "root" directory of the application.

We generated a basic application skeleton using a tool cooked up just for this article series, mkqpapp.py. This created in ~/qp_apps/blog the following hierarchy:

CHANGES
README
__init__.py
bin/
doc/
slash.qpy
test/
ui/
    test/
var/

Ignoring the obvious or already discussed, lets explore files of significance:

  1. slash.qpy as discussed earlier can be thought of as the driver or root application directory of our new application.

  2. __init__.py defines the directory as a package; however should there be any qpy modules in the given directory, __init__.py must include two lines of code:

    from qpy.compile import compile_qpy_files
    compile_qpy_files(__path__[0])
    

    QPY, which was installed in part one of this series, provides unicode and quote-safe Python templating, in a manner quite different than all other Python templating approaches. Diligent use of qpy can reduce or eliminate your application's risk of being exploited by cross site scripting attacks, and SQL injection attacks, if your application uses SQL.

    No magic or import hooks are required to integrate QPY with regular Python code; the two lines in __init__.py ensure that QPY files are compiled as regular Python code. Its a small price to pay for much flexibility as we'll see soon.

Exposing Objects, Methods or Functions on the Web

Lets now open the template slash.qpy module, explore, and make some changes. If you've not already started the template application created in part two, launch it now:

qp blog start

Visit the application at http://localhost:8011/ and you'll see:

It worked!

The blog application lives at /usr/home/yourhomedir/qp_sites/blog.

Lets edit slash.qpy and make some changes. First of all, since we've not discussed https secure http communications at all, yet, lets comment out some of the configuration:

class SitePublisher (DurusPublisher):

    configuration = dict(
        durus_address=('localhost', 7011),
        http_address=('', 8011),
        # as_https_address=('localhost', 9011),
        # https_address=('', 10011),
        )

The QP README contains a recipe for using stunnel for providing https SSL abilities to your QP applications.

SiteDirectory, Our Master Controller

Lets have a look at SiteDirectory, a required class which defines in essence the root of our web application. All paths lead from here, as we'll see now.

class SiteDirectory(Directory):

    def get_exports(self):
        yield ('', 'index', 'Home', 'The Home page of this site.')

    def index [html] (self):
        title = get_site().get_name()
        header(title)
        '<p><strong>It worked!</strong></p>'
        '<p>The <strong>%s</strong> application lives at <br /><tt>' % title
        get_site().get_package_directory()
        '</tt></p>'
        footer()

Two items of note:

get_exports() explicitly defines what methods or functions we are willing to expose to the World Wide Web (or to other applications via REST or other RPC methods). Nothing is shared with the world by default, everything is explicitly enabled, if at all.

get_exports() returns a four element tuple containing:

  1. The current directory 'pathname' to be exposed to the web, a null string if the path is to be '/'.
  2. The name of the Python callable that the pathname refers to. The publisher 'translates' pathnames into callables, making it easy to expose deeply nested hierarchies of functionality.
  3. A short name describing the callable; this is optionally used for menus.
  4. A longer descriptive phrase describing the callable; this is optionally used for anchor titles / tooltips.

Thus get_exports currently defines only a single exposed callable, a null path component (''), which translates to the index of the class.

The SiteDirectory method def index [html] (self): introduces another significant component of the QP web application framework and family, QPY, which provides almost fully transparent source-code transformation of an "html template" into pure Python. QPY delivers other capabilities which deserve some looking at now.

HTML Template Basics - Hello, World

Lets add a new method to our SiteDirectory class and expose it to the web. First the method:

def hello [html] (self):
    '<p>Hello, world</p>'

If you restart your application, and visit: http://localhost:8011/hello:

qp blog restart

You'll be rewarded with the default 404 response (which we can easily replace). Why? Because we forgot to expose hello() to the web:

'hello'?

Don't believe that was a 404 response? Lets also run the QP log watcher in another terminal window:

qp -l blog

You'll discover your last log entry looks something like:

2007-06-06 19:45:04 404 .007931 127.0.0.1 - - 3005 http GET /hello - Mozilla/5.0_

By now you know what must be done. Yes, that's right, we need to add an entry to get_exports():

def get_exports(self):
    yield ('', 'index', 'Home', 'The Home page of this site.')
    yield ('hello', 'hello', 'Greetings', 'A page for salutations')

Restart the application -- I map a key in vim to restart the current project I am editing -- and the revisit http://localhost:8011/hello and you'll now see returned:

Hello, world

That's not very exciting I admit. Lets alter hello() and supply the new method with some user-provided input to our hello method. For the purposes of this example, in order to keep things simple at this stage, lets pretend hello is asked to process the results of a form submission. We'll define, outside of our hello() method, a dictionary called form as such:

form = dict(
    title='User supplied title',
    text='Malicious user supplied <script>destroy_you_all()</script>')

Lets update hello:

def hello [html] (self):
    '<p>Hello, world, I'm a malicious user and posted this:</p>'
    '<h2>%(title)s</h2>' % form
    '<p>%(text)s</p>' % form

The result:

Hello, world, I am a malicious user and posted this:

User supplied title

Malicious user supplied <script>destroy_you_all()</script>

View source and you'll see that the content has been fully escaped and quoted:

[snip] Malicious user supplied &lt;script&gt;destroy_you_all()&lt;/script&gt;

This demonstrates one of the core features of QPY - safe quoting of content not explicitly declared as safe beforehand.

With this lesson in mind, its time now to write a basic shell of a user interface for our Entry and Journal objects. The interface code won't go directly into our application but into our library of useful reusable routines - but of course you could simply create the objects and related UI in the blog application hierarchy.

Entry UI

Our last task for this installment is to flesh out the basic UI for the Entry object:

from qp.fill.directory import Directory
from qp.lib.spec import require
from qp.pub.common import header, footer
from parlez.journal import Entry, Journal, JournalDatabase

# this is for demonstration only.
from qp.pub.user import User
test_entry = Entry(User('foo'))
test_entry.set_title('An example journal entry')
test_entry.set_text("To QPY, or not to QPY, <that> is the question & what's for dinner?")

class EntryUI(Directory):
    """
    This class provides the functionality to display a single entry,
    edit a new or existing entry, delete an entry, and to represent
    an entry in different ways including RSS or Atom.

    Our application urls for this UI component will be:

        ../1234/
        ../1234/edit
        ../1234/delete
        ../1234/index.rss
        ../1234/index.xml
    """

    def __init__(self, component=test_entry):
        require(component, Entry)
        self.entry = component

    def get_exports(self):
        yield ('', 'index', None, None)
        yield ('edit', 'edit', 'Edit Entry', None)
        yield ('delete', 'delete', 'Delete Entry', None)
        yield ('index.rss', 'rss', 'RSS', 'RSS feed for this entry')
        yield ('index.xml', 'atom', 'Atom', 'Atom feed for this entry')

    def index [html] (self):
        # python docstrings can't be included in [html] templates
        # this template could also deliver the output of other
        # template engines such as Genshi, Cheetah, Mako, etc.
        # lets stick with QPY for now.
        title = self.entry.get_title()
        header(title)
        '<h1>%s</h1>' % title
        self.entry.get_text()
        footer(title)

    def atom [html] (self):
        'Not Implemented'

    # lets just point all these methods to atom / not implemented for now until
    # we implement each
    edit = delete = rss = atom

If you want to race ahead and see what this looks like from the browser side, update slash.qpy by adding an import, changing get_exports and SiteDirectory as follows (don't forget to restart the application afterwards):

# import your UI object from wherever you put it.
from parlez.ui.journal import EntryUI

Modify get_exports as such:

def get_exports(self):
    yield ('', 'index', 'Home', 'The Home page of this site.')
    yield ('hello', 'hello', 'Greetings', 'A page for salutations')
    yield ('test_entry', 'test_entry', 'EntryUI Test', None)

Modify SiteDirectory by adding an attribute which points to an instance of EntryUI:

class SiteDirectory(Directory):
    # snip

    test_entry = EntryUI()

And then restart the app (qp blog restart) and visit the following URL's:

Next Installment

When we return in part six of this series we will inject some real Entry data into our Durus database and explore how we will work with Durus objects interactively, from our application, and from scripts.

After that we'll turn our application shell into a functioning application by stripping out the quick and dirty demonstration data hacks and completing our "controller" logic for EntryUI and Journal user interface classes.