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.