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

Python Web Application Diary, Part Seven

User interface handling in QP

In part six of Python Web Application Diary we looked at data persistence using Durus and wrote Sancho unit tests and a skeleton Pyblosxom migration tool. Today we'll extend the basic UI created in part five for our Entry object, and we'll create a base UI for the Journal object too. At that point we'll be able to wire our new JournalDirectory into SiteDirectory (also referenced in part five) - a QP application's "master controller" - which will allow us to start publishing previously created journal entries to the web.

JournalDirectory

I find it useful to map out the URL design for a set of related objects and then start to work on the UI. In part five we did this for EntryUI. Lets get to work on the UI 'container' for our Entries, JournalDirectory. In this first cut we'll implement enough to display a list of recent entries and put stubs in place for Atom and RSS feeds.

class JournalDirectory(Directory):
    """
    This class provides the functionality to display recent entries,
    to navigate to a specific entry, and to present RSS and Atom feeds.

    Our application urls for this UI component will be:

    ../             "index" or recent entries
    ../1234/        calls the EntryUI "index" method
    ../new          create a new Entry
    ../index.rss
    ../index.xml
    """

    def get_exports(self):
        yield ('', 'index', None, None)
        yield ('index.rss', 'rss', 'RSS', 'RSS feed for this journal')
        yield ('index.xml', 'atom', 'Atom', 'Atom feed for this journal')

    def __init__(self, journal):
        require(journal, Journal)
        self.journal = journal

    def index [html] (self):
        title = "%s's journal" % self.journal.get_name()
        header(title)
        '<p>This is the journal of '
        self.journal.get_name()
        '</p>'
        for e in self.journal.get_recent_entries(count=10):
            '<p>%s %s %s</p>' % (e.key, e.get_text().get_format(),
                                 href(str(e.key),e.get_title()))
        footer(title)

    def new [html] (self):
        ' not implemented '

    atom = rss = new

Looking at the code above it you can see that we've provided no way of navigating to individual entries. In index we've generated links which look like this:

<a href="./1234">Some title</a>

What we've not done is provide a way of resolving that part of the URL path. Lets say for example we have a site:

http://mikewatkins.ca/blog

With entries:

http://mikewatkins.ca/blog/1234
http://mikewatkins.ca/blog/3456

How does the QP application resolve these paths?

_q_lookup

Unlike some web application frameworks which use regular expressions to map URL namespaces to objects/methods or functions, QP uses the concept of object publishing via object traversal. We've seen how a UI object that exposes methods can be called; _q_lookup provides us the ability to return "objects". Lets write a _q_lookup method for JournalDirectory that returns an EntryUI object for a specific journal Entry.

def _q_lookup(self, component):
    try:
        key = int(component)
    except ValueError:
        return not_found('%r is not a valid journal entry identifier.' % component)
    try:
        entry = self.journal.get_entry(key)
    except KeyError:
        return not_found('%r was not found.' % key)
    return EntryUI(entry)

At this point we have working JournalDirectory and EntryUI objects. Lets now replace our temporary demonstration code in the applications master controller with a connection to our Durus database and real data.

As you might imagine, with this object / UI pattern it would be easy to add support for multiple journals. Objects and their corresponding UI might look like:

Object              UI
--------------------------------------------
Entry               EntryUI
Journal             JournalDirectory
JournalDatabase     JournalDatabaseDirectory

Wiring up the application

For simplicities sake, we are going to first wire up our JournalDirectory to SiteDirectory which we last talked about in conjunction with the site's primary "driver", slash.qpy, last discussed in part five.

class SiteDirectory(Directory):

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

    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()

    mw_journal = get_publisher().get_root()['mw_journal']
    blog = JournalDirectory(mw_journal)

Or, we could wire up JournalDirectory as the 'root' of our application like this:

class SiteDirectory(JournalDirectory):

    def __init__(self):
        # I've implemented the JournalDatabase object and migrated some
        # data into a Journal.
        mw_journal = get_publisher().get_root()['journals'].get('mw')
        JournalDirectory.__init__(self, mw_journal)

Going with the latter example, at this point we can restart the qp application (qp blog restart) and visit http://localhost:8011/ and we should see a list of available posts displayed which we can navigate to. Clearly we need to do a bunch more work - implement a site look and feel; convert RST, Markdown, and Textile formatted posts into HTML, implement RSS and Atom feeds, but the basics of a journal or weblog application have come together. Fleshing out the display of existing data will be our next step.

We still can't create, edit, or delete journal entries - we'll discuss that in a soon to be forthcoming installment in this series.