4.3 The Popview Application
Description: The Popview Application
Keywords: albatross

#
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

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)

    app = App()

    if __name__ == '__main__':
        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>