4.3 The Popview Application

In this section we will develop a simple application that allows a user to log onto a POP server to view the contents of their mailbox. Python provides the poplib module which provides a nice interface to the POP3 protocol.

The complete sample program is contained in the samples/popview1 directory. Use the install.py script to install the sample.

cd samples/popview1
python install.py

First of all let's create the model components of the application. This consists of some classes to simplify access to a user mailbox. Create a module called popviewlib.py and start with the Mbox class.

import string
import poplib

pophost = 'pop'

class Mbox:
    def __init__(self, name, passwd):
        self.mbox = poplib.POP3(pophost)
        self.mbox.user(name)
        self.mbox.pass_(passwd)

    def __getitem__(self, i):
        try:
            return Msg(self.mbox, i + 1)
        except poplib.error_proto:
            raise IndexError

The important feature of our Mbox class is that it implements the Python sequence protocol to retrieve messages. This allows us to iterate over the mailbox using the <al-for> tag in templates. When there is an attempt to retrieve a message number which does not exist in the mailbox, a poplib.error_proto exception will be raised. We transform this exception into an IndexError exception to signal the end of the sequence.

The sequence protocol is just one of the truly excellent Python features which allows your application objects to become first class citizens.

Next we need to implement a Msg class to access the header and body of each message.

class Msg:
    def __init__(self, mbox, msgnum):
        self.mbox = mbox
        self.msgnum = msgnum
        self.read_headers()

    def read_headers(self):
        res = self.mbox.top(self.msgnum, 0)
        hdrs = Headers()
        hdr = None
        for line in res[1]:
            if line and line[0] in string.whitespace:
                hdr = hdr + '\n' + line
            else:
                hdrs.append(hdr)
                hdr = line
        hdrs.append(hdr)
        self.hdrs = hdrs
        return hdrs

    def read_body(self):
        res = self.mbox.retr(self.msgnum)
        lines = res[1]
        for i in range(len(lines)):
            if not lines[i]:
                break
        self.body = string.join(lines[i:], '\n')
        return self.body

Note that we retrieve the message headers in the constructor to ensure that the creation of the Msg object will fail if there is no such message in the mailbox. The Mbox class uses the exception raised by the poplib module to detect when a non-existent message is referenced.

The poplib module returns the message headers as a flat list of text lines. It is up to the poplib user to process those lines and impose a higher level structure upon them. The read_headers() method attaches header continuation lines to the corresponding header line before passing each complete header to a Headers object.

The read_body() method retrieves the message body lines from the POP server and combines them into a single string.

We are going to need to display message headers by name so our Headers class implements the dictionary protocol.

class Headers:
    def __init__(self):
        self.hdrs = {}

    def append(self, header):
        if not header:
            return
        parts = string.split(header, ': ', 1)
        name = string.capitalize(parts[0])
        if len(parts) > 1:
            value = parts[1]
        else:
            value = ''
        curr = self.hdrs.get(name)
        if not curr:
            self.hdrs[name] = value
            return
        if type(curr) is type(''):
            curr = self.hdrs[name] = [curr]
        curr.append(value)

    def __getitem__(self, name):
        return self.hdrs.get(string.capitalize(name), '')

Instead of raising a KeyError for undefined headers, the __getitem__() method returns the empty string. This allows us to test the presence of headers in template files without being exposed to exception handling.

Lets take all of these classes for a spin in the Python interpreter.

>>> import popviewlib
>>> mbox = popviewlib.Mbox('djc', '***')
>>> msg = mbox[0]
>>> msg.hdrs['From']
'Owen Taylor <otaylor@redhat.com>'
>>> print msg.read_body()

Daniel Egger <egger@suse.de> writes:

> Am 05 Aug 2001 12:00:15 -0400 schrieb Alex Larsson:
> 
[snip]

Next we will create the application components (the controller) in popview.py. Albatross has a prepackaged application object which you can use for small applications; the SimpleApp class.

In anything but the most trivial applications it is probably a good idea to draw a site map like figure 4.6 to help visualise the application.

Our application will contain three pages; login, message list, and message detail.

Figure 4.6: Popview Site Map
\includegraphics[]{pagemap}

The SimpleApp class inherits from the PageObjectMixin class which requires that the application define an object for each page in the application.

Albatross stores the current application page identifier in the local namespace of the execution context as __page__. The value is automatically created and placed into the session.

When the current page changes, the Albatross application object calls the page_enter() function/method of the new page object. The page_process() method is called to process a browser request, and finally page_display() is called to generate the application response.

The SimpleApp class creates SimpleAppContext execution context objects. By subclassing SimpleApp and overriding create_context() we can define our own execution context class.

The prologue of the application imports the required application and execution context classes from Albatross and the Request class from the deployment module.

#!/usr/bin/python
from albatross import SimpleApp, SimpleAppContext
from albatross.cgiapp import Request
import poplib
import popviewlib

Next in the file is the class for the login page. Note that we only implement glue (or controller) logic in the page objects. Each time a new page is served we will need to open the mail box and retrieve the relevant data. That means that the username and password will need to be stored in some type of session data storage.

class LoginPage:
    name = 'login'

    def page_process(self, ctx):
        if ctx.req_equals('login'):
            if ctx.locals.username and ctx.locals.passwd:
                try:
                    ctx.open_mbox()
                    ctx.add_session_vars('username', 'passwd')
                except poplib.error_proto:
                    return
                ctx.set_page('list')

    def page_display(self, ctx):
        ctx.run_template('login.html')

The req_equals() method of the execution context looks inside the browser request for a field with the specified name. It returns a TRUE value if such a field exists and it has a value not equal to None. The test above will detect when a user presses the submit button named 'login' on the login.html page.

Next, here is the page object for displaying the list of messages in the mailbox. You will notice one small wart in the request processing; since the 'detail' request is implemented in an <input type="image"> tag, the browser sends x and y coordinates where the mouse was clicked in the image.

class ListPage:
    name = 'list'

    def page_process(self, ctx):
        if ctx.req_equals('detail.x'):
            ctx.set_page('detail')

    def page_display(self, ctx):
        ctx.open_mbox()
        ctx.run_template('list.html')

The ``detail'' page displays the message detail.

class DetailPage:
    name = 'detail'

    def page_process(self, ctx):
        if ctx.req_equals('list'):
            ctx.set_page('list')

    def page_display(self, ctx):
        ctx.open_mbox()
        ctx.read_msg()
        ctx.run_template('detail.html')

And finally we define the application class and instantiate the application object. Note that we have subclassed SimpleApp to create our own application class. This allows us to implement our own application level functionality as required.

class AppContext(SimpleAppContext):
    def open_mbox(self):
        if hasattr(self.locals, 'mbox'):
            return
        self.locals.mbox = popviewlib.Mbox(self.locals.username, self.locals.passwd)

    def read_msg(self):
        if hasattr(self.locals, 'msg'):
            return
        self.locals.msg = self.locals.mbox[int(self.locals.msgnum) - 1]
        self.locals.msg.read_body()

class App(SimpleApp):
    def __init__(self):
        SimpleApp.__init__(self,
                           base_url = 'popview.py',
                           template_path = '.',
                           start_page = 'login',
                           secret = '-=-secret-=-')
        for page_class in (LoginPage, ListPage, DetailPage):
            self.register_page(page_class.name, page_class())

    def create_context(self):
        return AppContext(self)

if __name__ == '__main__':
    app = App()
    app.run(Request())

The base_url argument to the application object constructor will be placed into the action attribute of all forms produced by the <al-form> tag. It will also form the left hand side of all hrefs produced by the <al-a> tag. The template_path argument is a relative path to the directory that contains the application template files. The start_page argument is the name of the application start page. When a browser starts a new session with the application it will be served the application start page.

We have also created our own execution context to provide some application functionality as execution context methods.

With the model and controller components in place we can now move onto the template files that comprise the view components of the application.

First let's look at the login.html page.

<html>
 <head>
  <title>Please log in</title>
 </head>
 <body>
  <h1>Please log in</h1>
  <al-form method="post">
   <table>
    <tr>
     <td>Username</td>
     <td><al-input name="username" size="10" maxlength="10"></td>
    </tr>
    <tr>
     <td>Password</td>
     <td><al-input type="password" name="passwd" size="10" maxlength="10"></td>
     <td><al-input type="submit" name="login" value="Log In"></td>
    </tr>
   </table>
  </al-form>
 </body>
</html>

When you look at the HTML produced by the application you will notice two extra <input> tags have been generated at the bottom of the form. They are displayed below (reformatted to fit on the page).

<input type="hidden" name="__albform__" value="eJzTDJeu3P90rZC6dde04xUhHL
WFjBqhHKXFqUV5ibmphUzeDKFsBYnFxeUphcxANmtOfnpmXiGLN0OpHgB7UBOp
">
<input type="hidden" name="__albstate__" value="eJzT2sr5Jezh942TUrMty6q1j
WsLGUM54uMLEtNT4+MLmUJZc/LTM/MKmYv1AH8XEAY=
">

If we fire up the Python interpreter we can have a look at what these fields contain.

>>> import base64,zlib,cPickle
>>> s = "eJzTDJeu3P90rZC6dde04xUhHL\n" + \
... "WFjBqhHKXFqUV5ibmphUzeDKFsBYnFxeUphcxANmtOfnpmXiGLN0OpHgB7UBOp\n"
>>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:])
{'username': 0, 'passwd': 0, 'login': 0}
>>> s = "eJzT2sr5Jezh942TUrMty6q1j\n" + \
... "WsLGUM54uMLEtNT4+MLmUJZc/LTM/MKmYv1AH8XEAY=\n"
>>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:])
{'__page__': 'login'}

The first string contains a dictionary that defines the name and type of the input fields that were present in the form. This is placed into the form by the NameRecorderMixin class which is subclassed by SimpleAppContext. If you look at the definition of SimpleAppContext you will notice the following definition at the start of the class.

NORMAL, LIST, FILE = range(3)

The value 0 for each field in the dictionary above corresponds to field type NORMAL.

When merging the browser request into the execution context the dictionary of field names is used to assign the value None to any NORMAL or FILE fields, or [] to any LIST fields that were left empty by the user. This is useful because it lets us write application code which can ignore the fact that fields left empty by the user will not be sent by the browser when the form is submitted.

The second string contains all of the session values for the application. In the application start page the only session variable that exists is the __page__ variable. The HiddenFieldSessionMixin places this field in the form when the template is executed and pulls the field value back out of the browser request into the execution context when the session is loaded.

The first 16 bytes of the decompressed string is an MD5 sign. This was generated by MD5 signing the application secret (passed as the secret argument to the application object) prepended to the pickle string. When the field is sent back to the application the signing process is repeated. The pickle is only loaded if the sign sent by the browser and the regenerated sign are the same.

Since the popview application has been provided for example purposes you will probably forgive the usage of the HiddenFieldSessionMixin class to propagate session state. In a real application that placed usernames and passwords in the session you would probably do something to protect these values from prying eyes.

Now let's look at the list.html template file.

Note that the use of the <al-flush> tag causes the HTML output to be streamed to the browser. Use of this tag can give your application a much more responsive feel when generating pages that involve lengthy processing.

A slightly obscure feature of the page is the use of a separate form surrounding the <al-input type="image"> field used to select each message. An unfortunate limitation of the HTML <input type="image"> tag is that you cannot associate a value with the field because the browser returns the coordinates where the user pressed the mouse inside the image. In order to associate the message number with the image button we place the message number in a separate hidden <input> field and group the two fields using a form.

You have to be careful creating a large number of forms on the page because each of these forms will also contain the __albform__ and __albstate__ hidden fields. If you have a lot of data in your session the size of the __albstate__ field will cause the size of the generated HTML to explode.

<html>
 <head>
  <title>Message List</title>
 </head>
 <body>
  <al-form method="post">
   <al-input type="submit" name="refresh" value="Refresh">
  </al-form>
  <hr noshade>
  <table>
   <tr align="left">
    <td><b>View</b></td>
    <td><b>To</b></td>
    <td><b>From</b></td>
    <td><b>Subject</b></td>
   </tr>
   <al-for iter="m" expr="mbox">
   <tr align="left" valign="top">
    <td>
     <al-form method="post">
      <al-input type="image" name="detail" srcicons/generic.gif" border="0">
      <al-input type="hidden" name="msgnum" expr="m.value().msgnum">
     </al-form>
    </td>
    <td><al-value expr="m.value().hdrs['To']"></td>
    <td><al-value expr="m.value().hdrs['From']"></td>
    <td><al-value expr="m.value().hdrs['Subject']"></td>
   </tr>
   <al-if expr="(m.index() % 10) == 9"><al-flush></al-if>
   </al-for>
  </table>
 </body>
</html>

Finally, here is the message detail page detail.html.

<html>
 <head>
  <title>Message Detail</title>
 </head>
 <body>
  <al-form method="post">
   <al-input type="submit" name="list" value="Back">
  </al-form>
  <hr noshade>
  <table>
   <al-for iter="f" expr="('To', 'Cc', 'From', 'Subject')">
    <al-if expr="msg.hdrs[f.value()]">
     <tr align="left">
      <td><b><al-value expr="f.value()">:</b></td>
      <td><al-value expr="msg.hdrs[f.value()]"></td>
     </tr>
    </al-if>
   </al-for>
  </table>
  <hr noshade>
  <pre><al-value expr="msg.body"></pre>
 </body>
</html>