============================= 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. .. sectnum:: :depth: 4 .. contents:: :depth: 4 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: - Development is quick and easy, once you learn how, which this document will help you to do. - Because these applications are built in a REST style, they will scale and they are robust. - Because the client/user views and interacts with the user interface through a Web browser, the problem of cross-platform portability (for the user interface) is solved. - Because the client connects to the application via the Internet and HTTP, the application can be accessed remotely. The client is to a great deal freed from restrictions of location. - There are reasonable ways to convert a user interface developed with Quixote to other implementations. This document will explain: - How to develop a user interface with the Quixote form2 package. - Strategies for improving the user interface. - Alternatives to ``form2`` and PTL. 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 subclass of it. 2. Add components, for example radio buttons, check boxes, etc, to the form. 3. Extract data (argument values) from the form. 4. Render the form. 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. 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:: 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``. 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 check boxes. - 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 check boxes, you can create the form with an item checked initially. 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') '
\n'
        '%s' % content
        '
\n' '' '' '' '
' zipForm.render() '
' # Generate other data etc here. o o o '
' 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. 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. 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. 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. 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. 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:: A Cheetah Example #for idx in range($count):

Hello, $name.

#end for 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 A Cheetah Example

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

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. 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 ... 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") ], ] ] 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":: An interpolation Example

Hello, %(name)s.

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 An interpolation Example

Hello, davy.

- 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 A Cheetah Example

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

Hello, bettylou.

- Use ``Cheetah`` and pre-compile your templates as described in the ``Cheetah`` sub-section above. 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: - Continue to use ``form2``, but use it at a lower level. - Continue to use Quixote, but replace the use of ``form2`` with HTML/PTL written by hand. - Use a Web server and HTTP (and perhaps even Quixote), but implement the client in Python (e.g. using module ``urllib`` to make requests and the ``wxPython`` or ``pygtk`` or ``PyQt`` GUI library. - Implement the client in Python (e.g. with ``wxPython`` or ``pygtk`` or ``PyQt``), but access the model directly (i.e. without HTTP). 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') '' '\n' '\n' '
' '
\n'
        '%s' % content
        '
\n' '
\n' '
\n' '\n' '\n' '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' '
FileName:\n' fileNameWidget.render() '
Sort order:\n' sortOrderWidget.render() '
Columns:\n' columnsWidget.render() '
Reverse:\n' reverseWidget.render() '
Totals:\n' totalsWidget.render() '
\n' submitWidget.render() '
\n' '
\n\n' '
\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. 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') 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`_. .. _`HTML Screen Scraping: A How-To Document`: http://www.rexx.com/~dkuhlman/quixote_htmlscraping.html - 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. 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 incur the additional burden of explaining that more complex application to your users. 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 radiobuttons', 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') '' '' '' '
' '

String value: "' # [8] stringValue '"


\n' '

Password value: "' passwordValue '"


\n' '

Single select value:' singleselectValue '


\n' '

Multiple select value:' multipleselectValue '


\n' '

Radiobuttons value:' radiobuttonsValue '


\n' '

Checkbox 1 value:' checkboxValue1 '


\n' '

Checkbox 2 value:' checkboxValue2 '


\n' '

Text block value: "' textblockValue '"


\n' '

List value:' listValue '

\n' '
' testForm.render() # [9] '
' 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. See Also ======== `http://www.mems-exchange.org/software/quixote/`_: The Quixote support Web site. .. _`http://www.mems-exchange.org/software/quixote/`: http://www.mems-exchange.org/software/quixote/ `Cheetah, the Python-Powered Template Engine`_ .. _`Cheetah, the Python-Powered Template Engine`: http://www.cheetahtemplate.org/index.html `HTML Screen Scraping: A How-To Document`_: Information on how to extract data from HTML documents. .. _`HTML Screen Scraping: A How-To Document`: http://www.rexx.com/~dkuhlman/quixote_htmlscraping.html