David Mertz, Ph.D.
Selector, Gnosis Software, Inc.
June, 2003
In the previous installment of this series, David looked at some higher-level techniques for writing Web services, including serving dynamic pages using the .rpy extension. In this article, he moves on to look at dynamic Web serving, and how to generate dynamic Web pages using the Woven application for templating pages.
In the last installment of this series, we looked at dynamic
web pages served by Twisted using the .rpy
extension. But
those initial versions of a weblog server were only minimally
dynamic. HTML tags were used to force a page to refresh
periodically, and upon each refresh a bit of calculation was
done to determine the relevant recent hits. But there was no
user configuration aspect to the server.
The first thing we will look at in this installment, therefore, is how to configure user interaction into the same basic dynamic page framework we looked at before. But even before that, let me include a quick reminder of how to launch a Twisted web server for readers who may not have seen the prior installments.
Creating an "pickled application" is usually the best approach, and it can be done purely with command line options. You do not have to do it this way. If you like, you are free to include some extra capabilities inside the basic webserver (such as maintaining persistent data across users and sessions), but it is not necessary to write any custom code. Creating the pickled application looks something like:
mktap web --path ~/twisted/www --port 8080
Launching it consists of:
twistd -f web.tap
That's it. Any HTML or .rpy
files that happen to be in the
~/twisted/www
base directory (or subdirectories) will be
served to clients on port 8080. Actually, you can serve
whatever file type you like, but the .rpy
files will be
treated as special dynamic scripts.
The dynamic page config_refresher.rpy
is a bit longer than
any presented in the prior installment; much of that is simply
the fact that it includes HTML templates in its body rather
than importing them. Let us first look at the setup code
from twisted.web import resource, server from persist import Records from webloglib import log_fields, COLOR from urllib import unquote_plus as uqp fieldnames = """ip timestamp request status bytes referrer agent""".split() field_dict = dict(zip(fieldnames, range(len(fieldnames)))
Other than a few imports that we have seen in prior installments,
I map field names to their positions in the tuple returned by
log_fields()
. Do notice also the use of a custom persist
module that will hold the weblog in memory within the Twisted web
server, so that the whole log file does not need to be read each
time any client requests some records. Next the HTML templates:
TOP = '''<html><head><title>Weblog Refresher</title> <META HTTP-EQUIV="Refresh" CONTENT="30"/></head> <body> <table border="1" width="100%%"> <tr bgcolor="yellow"> <form action="http://gnosis.cx:8080/config_refresher.rpy" method="GET"> <td> IP <input type="checkbox" name="ip" %s/> </td> <td> Timestamp <input type="checkbox" name="timestamp" %s/></td> <td> Request <input type="checkbox" name="request" %s/></td> <td> Status <input type="checkbox" name="status" %s/></td> <td> Bytes <input type="checkbox" name="bytes" %s/></td> <td> Referrer <input type="checkbox" name="referrer" %s/></td> <td> Agent <input type="checkbox" name="agent" %s/></td> <td> <input type="submit" value="Change Fields"></td> </form> </td></tr> <table border="0" cellspacing="0" width="100%%">''' ROW = '<tr bgcolor=" %s">%s</tr>\n' END = '</table></body></html>' COLOR = ['white','lightgray'] END = '''</table></body></html>'''
Setting up an HTML form is not too mysterious, but one little trick in the example is to interpolate the string "checked" into those checkboxes in the HTML that have been checked.
records = registry.getComponent(Records) if not records: records = Records() registry.setComponent(Records, records)
The Twisted registry works as described in the last
installment--it is simply the place where the latest records in
the web log file are held. Finally, we create a Resource
, with
a corresponding .render()
method--this does this actual page
creation:
class Resource(resource.Resource): def render(self, request): showlist = [] for field in request.args.keys(): showlist.append(field_dict[field]) showlist.sort() checked = [""] * len(fieldnames) for n in showlist: checked[n] = 'checked' request.write(TOP % tuple(checked)) odd = 0 for rec in records.getNew(): hit = [field.strip('"') for field in log_fields(rec)] flds='\n'.join(['<td>%s</td>'%hit[n] for n in showlist]) request.write(ROW % (COLOR[odd], uqp(flds).replace('&',' &'))) odd = not odd request.write(END) request.finish() return server.NOT_DONE_YET resource = Resource()
The main new thing in this Resource
is the access to the
request.args
attribute. Generally, this attribute is similar to
the Python cgi
modules FieldStorage
class--it collects any
information passed with the page request, both GET and PUT data.
Twisted's request data is a dictionary of passed values--in our
case we are just interested in which checkboxes fields are passed
in and which not. But it would follow the same pattern if we
wanted to check some values stored in request.args
--perhaps
you might add options to filter based on field values, for
example (and choose this with a text entry or an HTML listbox).
The dynamic pages we have looked at so far have all been
conceptually fairly similar to a CGI approach. Twisted
asynchronous server is faster--and it especially saves time to
avoid the overhead of opening a new process for each script
request. But fastcgi
or mod_python
achieve a similar
speedup. There is nothing all that special about Twisted in
this regard.
One way to move web application development to a higher level
is to use Woven
. In concept, Woven
is somewhat similar to
PHP, ASP (Active Server Pages), or JSP (Java Server Pages).
That is, Woven
XHTML pages are not simply the pages delivered
to browsers, but rather templates or skeletons of pages that
are filled in programmatically. However, the separation
between code and HTML is a bit different in Woven
than in
those page-embedding technologies. You do not write Python
code directly inside a Woven
template. Instead you define a
series of custom XHTML attributes on normal tags that let
external code enhance and manipulate the page in preparation
for delivery to the browser client.
The model
attribute determines the data that is used
for expanding the XHTML element it is attached to. The idea
is that a Model represents the "business logic" of an
application--how the data content of a page is determined.
The view
attribute, in contrast determines the particular
presentation of the generated data. There is also the concept
of a Controller in Woven
which is the code that combines the
Model with the View of a node (i.e. an XHTML element). This
last part is usually handled by a Page
object, which is a
class that can be specialized.
The nomenclature behind Woven
is admittedly a bit difficult;
and unfortunately, the HOWTO documents at the Twisted Matrix
website do almost as much to obscure matters as they do to
illuminate them. It is hard going to figure out exactly how to
use Woven
. I do not yet claim to wholly understand Woven
concepts myself, but Twisted user Alex Levy
(<[email protected]>) was kind enough to help me through
developing the example I present below. Still, there is quite
a bit you can do with Woven
, so it is worth working through.
The first step for a Woven
application is to develop a
template file (or files). These are simply XHTML files with
special attributes, e.g.:
<html> <head> <title>Weblog Viewer</title> <meta HTTP-EQUIV="Refresh" CONTENT="30" /> <style type="text/css"><!-- div.info { background-color: lightblue; padding: 2px dotted; } table th, table td { text-align: left; cellspacing: 0px; cellpadding: 0px; } table.log { border: 0px; width: 100%; } table.log tr.even { background-color: white; } table.log tr.odd { background-color: lightgray; } --></style> </head> <body> <div class="info"> You are displaying the contents of <code model="filename" view="Text">filename</code>. </div> <table border="0" cellspacing="0" width="100%" class="log" model="entries" view="List"> <tr bgcolor="yellow" pattern="listHeader"> <th>Referrer</th><th/> <th>Resource</th> </tr> <tr pattern="listItem" view="alternateColor"> <td model="referrer" view="Text"> Referrer</td> <td>-></td> <td model="request_resource" view="Text"> Resource</td> </tr> <tr pattern="emptyList"> <td colspan="2">There is nothing to display.</td> </tr> </table> </body> </html>
Alex Levy developed this template, and showing better style than I have in my examples, used CSS2 to control the exact presentation of elements. Obviously, the basic layout of the page is the same with or without the style sheet.
Notice that the <table>
element is assigned the view "List",
which is a basic Woven
View, as is "Text"--on the other hand,
"alternateColor" is a customized view that we define in code
below. Some elements have a pattern
attribute which is used
by the controlling View to locate matching children. In
particular a List view is composed of an optional listHeader
,
some listItem
children (one template tag, but expanded
during generation), and an emptyList
child in case the Model
does not locate any data. These patterns are standard
attributes that a List view uses, other views would utilize
other patterns for expansion.
The code for this version of a weblog server creates a custom
Twisted sever. Rather than update based on requests by clients,
we add a repeated callback to the update()
function to the
server's Reactor; this is substantially the same as with the
tlogmaker.py
in the prior installment. Let us look at the
setup code first before we examine the customized Page
resource:
import webloglib as wll import os, sys from urllib import unquote_plus as uqp from twisted.internet import reactor from twisted.web import microdom from twisted.web.woven import page, widgets logfile = '../access-log' LOG = open(logfile) RECS = [] NUM_ROWS = 25 def update(): global RECS RECS.extend(LOG.readlines()) RECS = RECS[-NUM_ROWS*3:] reactor.callLater(5, update) update()
The interesting stuff comes with our customization of the class
twisted.web.woven.page.Page
. Most of what we do is magic, in
the sense that you need to define specially-named attributes and
methods.
class WeblogViewer(page.Page): """A Page used for viewing Apache access logs.""" templateDirectory = '~/twisted/www' templateFile = "WeblogViewer.xhtml" # View factories and updates def wvupdate_alternateColor(self, request, node, data): """Makes our table rows alternate CSS classes""" # microdom.lmx is very handy; another example is located here: # http://twistedmatrix.com/documents/howto/picturepile#auto0 tr = microdom.lmx(node) tr['class'] = ('odd','even')[data['_number']%2] # Model factories def wmfactory_filename(self, request): """Returns the filename of the log being examined.""" return os.path.split(logfile)[1] def wmfactory_entries(self, request): """Return list of dict objects representing log entries""" entries = [] for rec in RECS: hit = [field.strip('"') for field in wll.log_fields(rec)] if hit[wll.status] == '200' and hit[wll.referrer] != '-': # We add _number so our alternateColor view will work. d = {'_number': len(entries), 'ip': hit[wll.ip], 'timestamp': hit[wll.timestamp], 'request': hit[wll.request], 'request_resource': hit[wll.request].split()[1], 'status': hit[wll.status], 'bytes': hit[wll.bytes], 'referrer': uqp(hit[wll.referrer]).\ replace('&',' &'), 'agent': hit[wll.agent], } entries.append(d) return entries[-NUM_ROWS:] resource = WeblogViewer()
There are three categories of things that our custom Page does. The first sets up the template to use with this resource.
The second defines a custom View, using the magic method prefix
wv
(Woven view). All we really do in the custom view is set the
class
attribute to one of two values from the CSS stylesheet,
to make alternating rows of display have a different color. But
you could manipulate the node however you like, using a
DOM-like API.
The third category is interesting. We define two Models using
methods with wmfactory_
prefixed to the name of the Model
itself. Since filename
is displayed in a Text view, it is
best to return a string. And likewise, entries
displayed in
a List view should get a list of entries as return value. But
what about the Models referrer
and request_resource
that
are used in the XHTML template? No custom methods were defined
for these models. However, the listItem
pattern that
surrounds nodes with these Models has a dictionary made
available to it--the entries
dict returned by
.wmfactory_entries()
. And this dictionary, in turn, contains
keys for request_resource
and referrer
; you do not need a
custom method to support a Model, a dictionary with the
necessary key works fine. Since the View for the referrer
node is Text, so said dictionary should contain a string for a
value (but if not, Woven makes good efforts to coerse it).
Creating a custom server based on the custom WeblogViewer.py
resource works as we have seen before. Create a server, then
launch it:
% mktap web --resource-script=WeblogViewer.py --port 8080 % twistd -f web.tap
The introduction has only scratched the surface of Woven. There are whole labyrinths within that package; but I hope the example I presented gives a bit of the feel for the templating system.
In the final installment of this series on Twisted, I will pick up some odds and ends, and take a look at a few of the specialized protocols and servers contained in the Twisted package.
Twisted Matrix comes with quite a bit of documentation, and many examples. Browse around its homepage to glean a greater sense of how Twisted Matrix works, and what has been implemented with it (or wait for the next installments here):
http://twistedmatrix.com
You can read about the Woven templating framework at:
http://twistedmatrix.com/documents/howto/woven
A simple version of a weblog server was presented in the developerWorks tip, Use Simple API for XML as a long-running event processor:
http://www-106.ibm.com/developerworks/xml/library/x-tipasysax.html
David Mertz believes that it is turtles all the way down. David may be reached at [email protected]; his life pored over at http://gnosis.cx/publish/. And buy his book: Text Processing in Python (http://gnosis.cx/TPiP/).