Quixote User Interface How To

Author: Dave Kuhlman
Address:
dkuhlman@rexx.com
http://www.rexx.com/~dkuhlman
Revision: 1.0a
Date: Feb. 2, 2004
Copyright: Copyright (c) 2004 Dave Kuhlman. This documentation is covered by The MIT License: http://www.opensource.org/licenses/mit-license.

Abstract

This document provides some help with developing user interfaces with the Quixote form2 module. It also describes strategies for improving those user interfaces, converting to other user interface development technologies, etc.

Contents

1���Introduction

Quixote can be viewed as a simple, easy, quick way to develop front-ends or user interfaces to complex back-end applications and resources.

This strategy has the following advantages:

This document will explain:

2���Developing a User Interface with Quixote form2

Creating a user interface with form2 involves the following steps, which will be described in more detail below:

  1. Create an instance of form2.Form or a subclase of it.
  2. Add components, for example radio buttons, checkboxes, etc, to the form.
  3. Extract data (argument values) from the form.
  4. Render the form.

2.1���Create a form

Here is an example:

zipForm = MyZipForm(name='zip_form', action_url='show_contents')

Explanation:

  • I've created a sub-class of form2.Form, so we create a instance of my sub-class.

2.2���Add components to the form

Here is an example:

zipForm.add(form2.StringWidget, 'file_name', '/home/dkuhlman/tmp.zip',
    title='File name:',
    hint='Enter zip file name',
    size=40,
    required=False)
zipForm.add(form2.SingleSelectWidget, 'sort_order', 'file_name',
    title='Sort by:',
    hint='Column to sort by',
    options=[
        ('file_name', 'File name', 'key1'),
        ('date_time', 'Date/time', 'key2'),
        ('file_size', 'File size', 'key3'),
        ('compress_size', 'Compressed size', 'key4'),
        ],
    sort=False)
zipForm.add(form2.RadiobuttonsWidget, 'columns', 'l',
    title='Columns:',
    hint='Columns: s=short, m=medium, l=long, v=verbose',
    options=[
        ('s', 'Short', 'key1'),
        ('m', 'Medium', 'key2'),
        ('l', 'Long', 'key3'),
        ('v', 'Verbose', 'key4'),
        ],
    sort=False)
zipForm.add(form2.CheckboxWidget, 'reverse', '', title='Reverse:',
    hint='Reverse the sort order')
zipForm.add(form2.CheckboxWidget, 'totals', 't', title='Totals:',
    hint='Show totals')
zipForm.add_submit('Submit', 'Submit')

Explanation:

  • We use the add method of class form2.Form to create and add components to our form.

  • The arguments to add are the following:

    1. klass -- The class of the widget that we wish to create. It should be one of the classes in form2.widget.

    2. name -- The name of the widget. It is basically the CGI variable name. It is the value of the name attribute for the HTML element. We'll use this name to reference the widget within the form. For example, for the file_name widget:

      zipForm.add(form2.StringWidget, 'file_name', '', title='File name:',
          hint='Enter zip file name', size=40)
      

      the following name attribute is generated:

      <input type="text" name="file_name" value="" size="40" />
      
    3. value -- The initial value for the widget.

    4. title -- A label to be used in the user interface.

    5. hint -- Help information that is displayed in the user interface.

    6. options -- For Radiobuttons and Select widgets, we give a list of options. The options can be a list of string values. However, by providing a tuple for each option containing (value, description, key), we get the form and widget to display the descriptions and to translate from selected option to the value for that object.

    7. Extra keyword args are passed to the constructor method for the widget class.

  • The SelectWidget is an abstract class. Use SingleSelectWidget or MultipleSelectWidget.

2.3���Extract data values

Forms in form2 implement the dictionary interface. Therefore, you can retrieve data values that were entered in the Web browser by indexing the form instance with the name of the component. Here are some examples:

fileName = zipForm['file_name']
sortFlag = zipForm['sort_order']
columnFlag = zipForm['columns']
reverse = zipForm['reverse']
totals = zipForm['totals']

Explanation:

  • This example uses the components created above. In particular:
    • file_name is a text input field.
    • sort_order is a select input item.
    • columns is a radio button input item.
    • reverse and totals are checkboxes.
  • The values of widgets in a form are retrieved as though the form were a dictionary. We use the name of the widget as the key.
  • Where no value has been set in a form (e.g. a set of radio buttons, none of which has be checked), the form will return a value of None. But note that for radio buttons and checkboxes, you can create the form with an item checked initially.

2.4���Render the form

Use the render method to generate the HTML code for the form. In our example below, we are assuming that we are using PTL. That is that our code is in a .ptl file and that we have evaluated the following:

from quixote import enable_ptl
enable_ptl()

Here is an example in which we create the form inside a cell of a 2-cell table with borders:

def example_ui [html] (self, request):
    header('Zip File Display')
    '<pre>\n'
    '%s' % content
    '</pre>\n'
    '<table border="2" width="100%">'
    '<tr><td>'
    zipForm.render()
    '</td></tr>'
    '<tr><td>'
    # Generate other data etc here.
    o
    o
    o
    '</td></tr>'
    '</table>'
    footer()

Explanation:

  • The functions header and footer generate and return content for the top and bottom of the HTML page. If you use these, you will need to implement them yourself.
  • The render method generates content for the form and all components in it.

3���PTL and other UI Options

PTL is not the only way to generate HTML (or XHTML or XML) content within Quixote. This sections explores a few of those other options. In fact, you are free to use any method you can dream up to produce content to be returned to the client.

3.1���PTL plus a plain Python function

From your PTL function you can call a function implemented in plain Python, i.e. a function or method that does not have the "[plain]" or "[html]" modifier after the function/method name.

3.2���PTL plus content from a file

Use an HTML editor (or plain text editor to create content and store it in a file. From Python (PTL or not) read that file and return it as content.

This technique would enable you to use your favorite HTML editor such as Quanta, Bluefish.

3.3���PTL plus content plus a template processor

There are a variety of template languages and processors for Python. If you do not already have a favorite, here is a good place to look: http://www.python.org/cgi-bin/moinmoin/WebProgramming. (Look for "Templating Systems".)

For example, you could use an HTML editor to create content and store it in a file. Then at run-time, read that content from the file, apply a template processor to it, and return the results for Quixote to be returned to the client.

In the following sub-section we'll consider the use of the Cheetah Template Engine.

3.3.1���Cheetah

Cheetah has a Python style. It uses "#" to mark Python code. It can be used as an independent template processor. And, for our purposes, it is especially good for generating mark-up content such as HTML and XML.

Here is a simple example of how you might use Cheetah to produce content for delivery by Quixote. Suppose that we have the following Cheetah template:

<html>
<head>
<title>A Cheetah Example</title>
</head>
<body>
#for idx in range($count):
<p>Hello, $name.</p>
#end for
</body>
</html>

If this template is in a file name "test2.tmpl", we can compile it with:

cheetah compile test2.tmpl

which will produce a file named "test2.py" containing a definition for a class named "test2".

Then, we can import and use this template to generate content with something like the following:

/w1/Python/Cheetah/Test [152] python
Python 2.3.3 (#1, Dec 21 2003, 13:10:04)
[GCC 3.3.2 (Debian)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test2                             # [1]
>>> values = {'name': 'bettylou', 'count': 5}    # [2]
>>> t = test2.test2(searchList=[values])     # [3]
>>> content = str(t)                         # [4]
>>>
>>> print content
<html>
<head>
<title>A Cheetah Example</title>
</head>
<body>
<p>Hello, bettylou.</p>
<p>Hello, bettylou.</p>
<p>Hello, bettylou.</p>
<p>Hello, bettylou.</p>
<p>Hello, bettylou.</p>
</body>
</html>

Explanation:

  1. We import the module containing the definition of class test2.
  2. We create a look-up object, in this case a dictionary. This dictionary defines keys whose values replace the placeholders in the template.
  3. We create an instance of the template, passing it the look-up object.
  4. We convert the instance to a string in order to produce the HTML content. Note that this works because in the generated class a __str__ method is defined.

You can learn much more about Cheetah, how to define templates, how to compile them, how to use them, etc from the documentation at: http://www.cheetahtemplate.org/learn.html.

3.4���PTL plus constructing a DOM and HTMLWriter

You could also construct a DOM (document object model) of Python objects, the apply the PrettyPrinter to that DOM to produce XHTML.

But, then maybe this is doing things the hard way ...

3.5���PTL plus Nevow

Nevow is a package borrowed from Twisted. It provides a very Python-esque style for defining HTML elements and generating HTML code.

Work is just beginning in the Quixote community on Nevow. There is some initial work on an implementation for Nevow that is being coordinated on the Quixote email list. We'll see where this work goes.

Here is a tiny example of the use of Nevow:

document = html[
    body[
        form(action="")[ input(type="text", name="name") ],
    ]
]

3.6���PTL plus some combination of the above

Here are a few possibilities and combinations:

  • Place your HTML content in a file. From a python function, read that file. Use Python string interpolation to file values into this content. For example, if you have the following HTML in a file "test3.html":

    <html>
    <head>
    <title>An interpolation Example</title>
    </head>
    <body>
    <p>Hello, %(name)s.</p>
    </body>
    </html>
    

    Then, you can use the following to generate content:

    >>> infile = file('test3.html', 'r')
    >>> s1 = infile.read()
    >>> infile.close()
    >>> values = {'name': 'davy'}
    >>> content = s1 % values
    >>>
    >>> print content
    <html>
    <head>
    <title>An interpolation Example</title>
    </head>
    <body>
    <p>Hello, davy.</p>
    </body>
    </html>
    
  • Same as the previous suggestion, but do it directly from a PTL function/method.

  • Use a similar string interpolation technique, but use a Python local variable to define the initial content.

  • Use Cheetah, but instead of compiling your template, read it from a file, create a Cheetah Template object, and convert it to a string. Here is an example that shows how to do this:

    >>> from Cheetah.Template import Template
    >>>
    >>> infile = file('test2.tmpl', 'r')
    >>> s1 = infile.read()
    >>> infile.close()
    >>>
    >>> values = {'name': 'bettylou', 'count': 5}
    >>> t = Template(s1, searchList=[values])
    >>> content = str(t)
    >>>
    >>> print content
    <html>
    <head>
    <title>A Cheetah Example</title>
    </head>
    <body>
    <p>Hello, bettylou.</p>
    <p>Hello, bettylou.</p>
    <p>Hello, bettylou.</p>
    <p>Hello, bettylou.</p>
    <p>Hello, bettylou.</p>
    </body>
    </html>
    
  • Use Cheetah and pre-compile your templates as described in the Cheetah sub-section above.

4���Options for Improving the User Interface

In this section we discuss some of the options available to improve or re-write the user interface that you have developed with Quixote and form2.

Here are a few of your options, each of which is discussed in a subsequence sub-section:

4.1���Lower level form2

An alternative is to use form2 but to interact with and render individual widgets. Using this approach, we

  • Create each widget separately, instead of using the add method of the Form class.
  • Extract data from each widget individually, instead of accessing their values by indexing into the form.
  • Render the widgets individually by calling the render method for each widget separately. Doing this enables us to customize the look of the form, for example, by adding PTL/HTML code around each widget.

Here is an example:

def do_show_contents  [html](self, request):
    fileNameWidget = form.StringWidget('file_name', '',
        title='File name',
        size=40)
    sortOrderWidget = form.RadiobuttonsWidget('sort_order', 'File name',
        title='Column to sort by',
        options=['File name', 'Date/time', 'File size', 'Compressed size'],
        sort=False)
    columnsWidget = form.RadiobuttonsWidget('columns', 'Long',
        title='Columns to show',
        options=['Short', 'Medium', 'Long', 'Verbose'],
        sort=False)
    reverseWidget = form.CheckboxWidget('reverse', '', title='Reverse:',
        hint='Reverse the sort order')
    totalsWidget = form.CheckboxWidget('totals', 't', title='Totals:',
        hint='Show totals')
    submitWidget = form.SubmitWidget('submit')
    fileName = fileNameWidget.get_value()
    sortFlag = sortOrderWidget.get_value()
    columnFlag = columnsWidget.get_value()
    reverse = reverseWidget.get_value()
    totals = totalsWidget.get_value()
    content = '*** empty content ***'
    if fileName:
        outstream = StringIO.StringIO()
        try:
            zipls.listArchive(outstream, sortFlag, columnFlag, reverse, totals, fileName)
            content = outstream.getvalue()
        except IOError, exp:
            content = exp
        outstream.close()
    header('Zip File Display')
    '<table border="2" width="100%">'
    '<tr><td>'
    '<pre>\n'
    '%s' % content
    '</pre>\n'
    '</td></tr>\n'
    '<tr><td>\n'
    '<form action="show_contents" name="zip_form" method="post">\n'
    '<table border="0" width="100%">\n'
    '<tr>\n'
    '<td width="25%">FileName:</td><td width="75%">\n'
    fileNameWidget.render()
    '</td>\n</tr>\n<tr>\n<td>Sort order:</td><td>\n'
    sortOrderWidget.render()
    '</td>\n</tr>\n<tr>\n<td>Columns:</td><td>\n'
    columnsWidget.render()
    '</td>\n</tr>\n<tr>\n<td>Reverse:</td><td>\n'
    reverseWidget.render()
    '</td>\n</tr>\n<tr>\n<td>Totals:</td><td>\n'
    totalsWidget.render()
    '</td>\n</tr>\n<tr><td>\n'
    submitWidget.render()
    '</td></tr>\n'
    '</table>\n'
    '</form>\n\n'
    '</td></tr>\n'
    '</table>\n'
    footer()

Explanation:

  • We create each widget individually, rather that creating a (form2) form and adding widgets to it. Also note that we do not add the widgets to a form. We will create the form tag explicitly with a PTL string.
  • We use each widget's get_value method, rather than the form's parse method or indexing on the form, to get each widget's value (i.e. the value entered at the browser).
  • We render each widget separately. We add HTML mark-up around the widgets to improve the appearance (we hope). And, we add a form tag around the widgets.

4.2���Quixote, but without form2

We can create HTML user interfaces without using Quixote's form package at all. In order to do so, in PTL for example, do the following:

  • Explicitly produce mark-up for the HTML input items themselves, as well as some of the decoration around them.

  • Use the get_form_var method in the request object to get the value for each input item. For example:

    value1 = request.get_form_var('arg1')
    

4.3���HTTP, but with a Python client

There are a variety of (sub-)strategies in this area. In general, we use Quixote and HTTP to provide access to the model, however, we implement the client user interface in Python, possibly with the wxPython or pygtk or PyQt GUI toolkit.

Here are a couple of ways to do this:

  • If you already have a Quixote Web application that generates HTML, you could implement a client that makes HTTP requests to the Quixote application (perhaps using urllib), then extracts data from those HTML documents (possibly using techniques described in HTML Screen Scraping: A How-To Document.
  • Write (or re-write) the Quixote application so that it generates and delivers XML instead of HTML. Then write the client so that it parses and displays content extracted from XML.

4.4���Direct access to the model

In this approach we take advantage of the fact that we implemented the original Quixote application with a strong separation between the user interface and the model. In fact, we implemented the model in a separate set of Python modules in a Python package. This enabled us to use the Python unit test framework to test the model separately. It also enables us to import and call the model from a separate application, which is what we are exploring in this sub-section.

How is it done? Write your user interface with, for example, wxPython or pygtk or PyQt, then, wherever in your application you need to interact with the model, import it and call it.

However, if you switch to any technology that is

  • If you do not use a server architecture, your users will not be able to access your application remotely. They will be restricted to working from the machine on which your application is running.
  • If you use a graphical toolkit to implement a client (e.g. wxPython or pygtk or PyQt), then your clients will be limited to running your application on a machine that supports that graphical environment. And, you will need to ensure that each client machine has the required graphical environment.
  • In implementing your application, you may need to concern yourself with implementing it in a way that is portable across several platforms. And, you may need to distribute several versions of you application, if you can install the same graphical environment on all client machines.
  • And, if you implement your application based on a model that is more complex than the REST model which a server-based implementation would lead you towards, then you will incure the additional burden of explaining that more complex application to your users.

5���The Widgets -- Examples

Documentation on form2 is a bit slight. Here are examples of how to create and use the widgets in the form2 module:

from quixote.form2.widget import ButtonWidget    # [1]

class TestsUI:

    _q_exports = []
    
    def do_test_form2  [html](self, request):
        testForm = form2.Form(name='test_form',
            action_url='test_form2',
            )
        testForm.add(form2.StringWidget, 'string_widget', '[default string]',
            title='String widget:',
            hint='Enter a string.',
            size=40,                    # [2]
            required=False)
        testForm.add(form2.PasswordWidget, 'password_widget', '',
            title='Password widget:',
            hint='Enter a password.',
            size=40,
            required=False)
        testForm.add(form2.SingleSelectWidget, 'singleselect_widget', 'value2',
            title='Single select widget:',
            hint='Select a single value.',
            options=[
                ('value1', 'Value #1', 'key1'),                      # [3]
                ('value2', 'Value #2', 'key2'),
                ('value3', 'Value #3', 'key3'),
                ('value4', 'Value #4', 'key4'),
                ],
            sort=False)
        testForm.add(form2.MultipleSelectWidget, 'multipleselect_widget', ('value3', 'value5'),
            title='Multiple select widget:',
            hint='Select multiple values.',
            options=[
                ('value1', 'Value #1', 'key1'),                       # [3]
                ('value2', 'Value #2', 'key2'),
                ('value3', 'Value #3', 'key3'),
                ('value4', 'Value #4', 'key4'),
                ('value5', 'Value #5', 'key5'),
                ('value6', 'Value #6', 'key6'),
                ],
            sort=False)
        testForm.add(form2.RadiobuttonsWidget, 'radiobuttons_widget', 'value3',
            title='Radiobuttons widget:',
            hint='Select a radiobutton',
            options=[
                ('value1', 'Value #1', 'key1'),                      # [3]
                ('value2', 'Value #2', 'key2'),
                ('value3', 'Value #3', 'key3'),
                ('value4', 'Value #4', 'key4'),
                ],
            sort=False)
        testForm.add(form2.CheckboxWidget, 'checkbox_widget1', 1,    # [4]
            title='Checkbox widget 1:',
            hint='Unselected checkbox')
        testForm.add(form2.CheckboxWidget, 'checkbox_widget2', 0,    # [4]
            title='Checkbox widget 2:',
            hint='Selected checkbox')
        testForm.add(form2.TextWidget, 'textblock_widget', 0,
            title='Text block widget 2:',
            hint='Enter text in text block.',
            rows= 6,                                                 # [5]
            cols=60,
            )
        testForm.add_component(MyWidgetList, 'list_widget',
            value=['value1', 'value2', 'value3', 'value4'],
            title='List widget:',
            hint='WidgetList for test only',
            )
        testForm.add(ButtonWidget, 'button_widget', 'Say hello',
            onClick='javascript: alert(\'Hello\')',                  # [6]
            )
        testForm.add_submit('Submit', 'Submit')
        stringValue = testForm['string_widget']                      # [7]
        passwordValue = testForm['password_widget']
        singleselectValue = testForm['singleselect_widget']
        multipleselectValue = testForm['multipleselect_widget']
        radiobuttonsValue = testForm['radiobuttons_widget']
        checkboxValue1 = testForm['checkbox_widget1']
        checkboxValue2 = testForm['checkbox_widget2']
        textblockValue = testForm['textblock_widget']
        listValue = testForm['list_widget']
        header('Zip File Display')
        '<table border="2" width="100%">'
        '<tr><td>'
        '<p>String value: "'                                         # [8]
        stringValue
        '"</p><hr/>\n'
        '<p>Password value: "'
        passwordValue
        '"</p><hr/>\n'
        '<p>Single select value:'
        singleselectValue
        '</p><hr/>\n'
        '<p>Multiple select value:'
        multipleselectValue
        '</p><hr/>\n'
        '<p>Radiobuttons value:'
        radiobuttonsValue
        '</p><hr/>\n'
        '<p>Checkbox 1 value:'
        checkboxValue1
        '</p><hr/>\n'
        '<p>Checkbox 2 value:'
        checkboxValue2
        '</p><hr/>\n'
        '<p>Text block value: "'
        textblockValue
        '"</p><hr/>\n'
        '<p>List value:'
        listValue
        '</p>\n'
        '</td></tr>'
        '<tr><td>'
        testForm.render()                                            # [9]
        '</td></tr>'
        '</table>'
        footer()

Notes (see numbered references above):

  1. The Button widget is not automatically exposed by the form2 module, so we import it explicitly.
  2. The size argument is passed through to the StringWidget class. It is attached to the input item as a "size" attribute.
  3. Because, when we create SingleSelectWidget, MultipleSelectWidget, we create each option with a tuple that contains (1) a value, (2) a description, and (3) a key, the following happens:
    • The widget (the HTML input item) displays each description (not the value).
    • When we request the value of the widget from the form, we get the value (not the description).
  4. On check boxes, a true value causes the check box to be checked by default and a false value causes the check box to be un-checked by default.
  5. We can pass through rows and cols arguments to control the size of the text block.
  6. For a ButtonWidget, we pass through an onClick argument in order to attach JavaScript to the button.
  7. We retrieve the value entered or selected for each input item (widget).
  8. And, we display the values.
  9. Finally, we render the form.

6���See Also

http://www.mems-exchange.org/software/quixote/: The Quixote support Web site.

Cheetah, the Python-Powered Template Engine

HTML Screen Scraping: A How-To Document: Information on how to extract data from HTML documents.