A Quixote Application Skeleton

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 a skeleton for a Quixote application and explains how to use that skeleton in order to create a minimal Quixote application.

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. The fact that these applications are delivered via HTTP and may be used remotely is an additional advantage.

This document and the attached sample files are intended to help you construct a Quixote application that connects the user to back-end resources. Quixote turns out to be a very quick and easy way to construct front ends and user interfaces for this style of application. And, for no extra cost, the application you construct can be used remotely, too.

This skeleton will help you to quickly implement a Quixote application that connects to the following back-end resources:

A distribution file containing the skeleton files and this documentation is available here: http://www.rexx.com/~dkuhlman/quixote_skeleton.zip.

Note that my testing is done with Quixote-0.7a3.

2���Install the Skeleton Files

You can obtain the distribution containing the skeleton files at: http://www.rexx.com/~dkuhlman/quixote_skeleton.zip.

Then, follow these steps:

The organization of the files in the skeleton is as follows:

skeleton/scripts/
Scripts for starting, restarting, and stoping the application.
skeleton/ui/
The user interface.
skeleton/ui/__init__.py
Parses and handles the URL. Also, exposes static files and directories.
skeleton/ui/servicesui.ptl
Implements the user interface.
skeleton/services/*.py
Implements the model. Provides logic and access to the back-end resources.
skeleton/xmlrpcserver.py
A sample XML-RPC server. It is used by servicesui.ServicesUI.do_temperature.
skeleton/soapserver.py
A sample SOAP server. It is used by servicesui.ServicesUI.do_structures.

3���Modifying the Server Script

The sample files include scripts for the following servers:

3.1���Medusa

Medusa provides an easy to install and easy to use Web server.

You can find more help with installing Medusa for Quixote here: http://www.rexx.com/~dkuhlman/quixote_usingmedusa.html.

In order to use the Medusa server, do the following:

  • Install Medusa.
  • Modify the server script (myapp/scripts/server-medusa.py):
    • Change all occurances of "skeleton" to the name of the directory in which you installed the sample files. For example, if you changed "/aaaa/bbbb/skeleton" to "/aaaa/bbbb/myapp", then replace "skeleton' with "myapp" in server-medusa.py.

3.2���Apache/SCGI

You can find more help with setup for Apache/SCGI at: http://www.rexx.com/~dkuhlman/quixote_scgi.html.

  • Build and install SCGI support for Quixote.

  • Install Apache. If you are on a Linux system, it is possible, even likely, that Apache has already been installed.

  • Configure Apache for SCGI. Add a stanza to your Apache httpd.conf file (which, on my system, is located in /etc/apache). The stanza should look something like the following:

    LoadModule scgi_module /usr/local/lib/python2.3/site-packages/mod_scgi.so
    <Location "/">
        SCGIServer 127.0.0.1 3001
        SCGIHandler On
    </Location>
    
    Listen 8081
    

    This assumes that mod_scgi was installed in /usr/local/lib/python2.3/site-packages.

  • Modify the SCGI server script (myapp/scripts/server-scgi.py):

    • Change all occurances of "skeleton" to the name of the directory in which you installed the sample files. For example, if you changed the directory containing your application from "/aaaa/bbbb/skeleton" to "/aaaa/bbbb/myapp", then replace "skeleton' with "myapp" in server-scgi.py.

3.3���Connections, proxies, etc

In both server scripts, there are examples of how to create connections and proxies for (1) a relational database (PostgreSQL), (2) XML-RPC, (3) SOAP (using SOAPpy). You will need to remove those you do not need and to modify those you do need.

3.3.1���A little theory

Quixote creates and re-uses processes.

For each process, Quixote creates an instance of your publisher class (a subclass of quixote.publish.Publisher).

Quixote will re-use this process to handle requests, but will create a new process if existing processes are in use.

Immediately before handling a request, the server calls the start_request method in the publisher subclass.

So, in the skeleton files (in particular, in myapp/scripts/server-medusa.py and myapp/scripts/server-scgi.py, we do the following:

  • In method MyPublisher.__init__, we initialize and save variables whose values we will use over and over. In particular, we create a connection to our relational database, an XML-RPC proxy, and a SOAP proxy.
  • In method start_request, we stuff the values that we will need during request processing into the request object. I've put them in a little container object, but you could just as easily insert the publisher object itself (i.e. self).
  • In method __del__, we put clean-up code. This will be called when we restart or stop the server.

3.3.2���Modify the skeleton

Do the following in myapp/scripts/server-medusa.py or myapp/scripts/server-scgi.py, as appropriate:

  • In method Mypublisher.__init__, remove any initialization code that you do not need, and add initialization code or create proxies, connections, and other values that you will re-use while satisfying requests.
  • In method start_request, save the values you will need during request processing in the request object.
  • In method __del__ do any clean-up the you need. The current code closes the connection to the database that was created for this process.

4���Modifying the Application

In file myapp/ui/__init__.py, do the following:

In file myapp/ui/servicesui.ptl, do the following:

5���Start Your Server

5.1���Medusa

The following will start your Quixote application serving with Medusa:

python server-medusa.py

To restart the server, use Ctrl-C, then start it again.

To stop the server, use Ctrl-C.

By the way, restarting or stopping the Medusa server does not result in a call to the __del__ method in the Publisher subclass. This means that it is not possible to do clean-up when you are restarting your application, e.g. while testing. If anyone learns how to do that, please drop me an email.

5.2���Apache/SCGI

To start serving your applicaton, use the following:

python server-scgi.py -p 3001 -l tmp.log

which starts the SCGI server listening on port 3001 and delivering log messages to tmp.log.

Here are the available options. Note that these flags are passed through to the SCGI server:

$ python server-scgi.py --help
option --help not recognized
Usage: server-scgi.py [options]

    -F -- stay in foreground (don't fork)
    -P -- PID filename
    -l -- log filename
    -m -- max children
    -p -- TCP port to listen on
    -u -- user id to run under

To restart your application, for example, after changing the application:

kill -HUP `cat /var/tmp/quixote-scgi.pid`

To stop the server, use the following:

$ kill `cat /var/tmp/quixote-scgi.pid`

Note that if you used the -P flag to change the PID file name, then you will need to modify the above commands.

6���Adding New Functionality to the Application

These instructions assume that you will add a new module to the model (i.e. in directory myapp/services/). If instead, you add your new functionality to an existing module, you will need to modify the following instructions slightly.

In order to add a new capability to the skeleton application, do the following:

6.1���In myapp/ui/__init__.py

Let's assume that our new capability will be accessed using the following URLs:

http://host:8081/mycapability/myfunction1
http://host:8081/mycapability/myfunction2
http://host:8081/mycapability/myfunction3
  1. Add a new clause to the if statement in function _q_lookup:

    elif name == 'mycapability':
        # Process requests whose URL is: http://host:8081/mycapability/do_xxxx.
        handler = MycapabilityHandler()
        return handler
    
  2. Add a new handler class:

    #
    # Process requests whose URL is: http://host:8081/mycapability/xxxx.
    # If class ZipfileUI contains a method 'do_+name, call it.
    #
    class MycapabilityHandler:
        _q_exports = []
        def _q_lookup(self, request, name):
            ui = MycapabilityUI()
            meth = getattr(ui, 'do_' + name, None)
            if meth:
                return meth(request)
            else:
                menu = MenuUI()
                return menu.index(request)
    

6.2���In myapp/ui/servicesui.ptl

  1. Add a new user interface class:

    class MycapabilityUI:
        _q_exports = []
    
  2. Add a new method to your new user interface class. If you want the method to respond to the URL http://host:8081/mycapability/function1, then add a method whose name is do_function1. This function might look a bit like the following:

    def do_show_contents  [html](self, request):
        #
        # Create the user interface, i.e. the form and widgets.
        #
        mycapabilityForm = form2.Form(name='mycapability_form', action_url='show_contents')
        mycapabilityForm.add(form2.StringWidget, 'argument1', value=None,
            title='File name:',
            hint='Enter zip file name',
            size=40,
            required=False)
        mycapabilityForm.add(form2.SingleSelectWidget, 'argument2', 'File name',
            title='Sort by:',
            hint='Column to sort by',
            options=['File name', 'Date/time', 'File size', 'Compressed size'],
            sort=False)
        mycapabilityForm.add(form2.RadiobuttonsWidget, 'argument3', 'Long',
            title='Columns:',
            hint=columnHint(),
            #hint='Columns: s=short, m=medium, l=long, v=verbose',
            options=['Short', 'Medium', 'Long', 'Verbose'],
            #delim=' || ',
            sort=False)
        mycapabilityForm.add(form2.CheckboxWidget, 'argument4', '', title='Reverse:',
            hint='Reverse the sort order')
        mycapabilityForm.add_submit('Submit', 'Submit')
        #
        # Extract data from the form.
        #
        arg1 = mycapabilityForm['argument1']
        arg2 = mycapabilityForm['argument2']
        arg3 = mycapabilityForm['argument3']
        arg4 = mycapabilityForm['argument4']
        #
        # Access the model.
        #
        mycapability = mymodule.MyCapability()
        content = mycapability.function1(request.container.connection, arg1, arg2, arg3, arg4)
        #
        # Render the model.
        #
        header('Mycapability Response Display')
        '<table border="2" width="100%">'
        '<tr><td>'
        '<pre>\n'
        '%s' % content
        '</pre>\n'
        '</td></tr>'
        '<tr><td>'
        mycapabilityForm.render()
        '</td></tr>'
        '</table>'
        footer()
    

    Explanation:

    • We create the form, then use the add method to create widgets and add them to the form.

    • The Form class implements the dictionary protocol, so we can retrieve values from the form by indexing with the widget name as key.

    • header and footer are functions that return HTML content. For example:

      def header [html] (subtitle):
          '<html>\n'
          '<head>\n'
          '<title>Quixote Skeleton App -- %s</title>\n' % subtitle
          o
          o
          o
      
    • This is a PTL method, so content is returned automatically. There is no need for an explicit return statement.

  3. Add a new import statement (at the top of the file), for example:

    from myapp.mymodule import My
    from myapp.services import mymodule
    

6.3���In myapp/services/mymodule.py

  1. Add a new module, for example, mymodule.py. For example:

    class Mycapability:
        o
        o
        o
    
  2. In the new module, add a new class. It might look something like the following:

    class Mycapability:
        def function1(self, connection, arg1, arg2, arg3, arg4):
            #
            # Use the database connection and the arguments to produce and
            #   format content.
            o
            o
            o
            return content
    

7���Customizing the form2 User Interface

The form2 module generates a user interface automatically. All your code needs to do is to add widgets to the form and then render the form (call the form's render method).

However, there are some things that you can do to customize the the look of the user interface. This section gives you a bit of guidance in doing so.

7.1���Ordering the widgets

Widgets are rendered, from top to bottom, in the same order that you add them to the form.

7.2���Adding labels and hints

Use the title argument of the form2.Form.add method to give a widget a title.

Use the hint argument of the form2.Form.add method to give the widget additional user information. The hint is (by default) displayed to the right of the input item.

Here is an example:

zipForm.add(form2.StringWidget, 'user_name', '[enter user name]',
    title='User name:',
    hint='Enter your name',
    size=40,
    required=False)

7.3���Customizing rendering of the form

To customize the way in which the form and the widets in it are rendered, do the following:

  1. Create a subclass of form2.Form.
  2. Override the methods _render_body, render_sep, and _render_components.

Here is an example:

class MyForm(form2.Form):
    def _render_body(self):
        r = TemplateIO(html=True)
        r += htmltext('<table border="0" width="100%">')
        r += self._render_error_notice()
        r += self._render_components()
        r += self._render_button_widgets()
        r += htmltext('</table>')
        return r.getvalue()

    def render_sep(self):
        return htmltext('<tr><td colspan="3"><hr/></td></tr>')

    def _render_components(self):
        r = TemplateIO(html=True)
        firstTime = True
        for component in self.components:
            if not firstTime:
                r += self.render_sep()
            firstTime = False
            r += component.render()
        return r.getvalue()

Explanation:

  • This subclass adds a horizontal rule (tag <hr/>) between widgets. See method render_sep.

7.4���Customizing the rendering of the widget

By default, each widget is rendered in a pair of table rows within an HTML table. We can change this and can change the way in which a widget is rendered by subclassing the class form2.WidgetRow.

Here is an example in which we render each widget in a single row:

class MyWidgetRow(form2.WidgetRow):
    def render(self):
        title = self.title or ''
        if title and self.required:
            title = title + htmltext('&nbsp;*')
        r = TemplateIO(html=True)
        r += htmltext('<tr><th width="15%" align="left">')
        r += title
        r += htmltext('</th><td>')
        r += self.widget.render()
        r += htmltext('</td><td>')
        r += self._render_error()
        r += self._render_hint()
        r += htmltext('</td></tr>')
        return r.getvalue()

Explanation:

  • We've subclassed form2.WidgetRow and overridden method render.
  • Our new render method is a copy of the superclass's method, except that it puts the title, the input item, and the hint all in a single row. That is, it eliminates one pair of <tr> tags.

Then, we create a form that uses our subclass with something like the following:

aSimpleForm = form2.Form(name='simple_form',
    action_url='show_contents',
    component_class=MyWidgetRow
    )

8���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.

9���See Also

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

Support for Quixote Etc: A variety of helpful documents on Quixote.