==============================
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.
.. 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. 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:
- PostgreSQL using ``pyPgSQL``. But, if you use MySQL and an
implementation for the Python DB API, then switching to MySQL
and an implementation of the Python DB API for MySQL should be
trivial. More information about implementations of the Python
DB API is here: http://python.org/sigs/db-sig/.
- XML-RPC using ``xmlrpclib`` from the Python standard library.
- SOAP using ``SOAPpy``. It's available from
http://pywebsvcs.sourceforge.net/.
- HTTP using several screen scraping techniques to extract data.
You can find more information on these techniques here:
http://www.rexx.com/~dkuhlman/quixote_htmlscraping.html.
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.
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:
- Create a directory in which to place the skeleton files, for
example::
mkdir Skeleton
cd Skeleton
- Unroll the skeleton files. Something like the following should do
it::
unzip quixote_skeleton.zip
- Rename the directory containing the sample files. In what
follows, I'm going to explain things as if you had renamed it to
"myapp", which, on a UNIX/Linux system, you would do as
follows::
mv skeleton myapp
- Add the directory in which you unrolled the sample files to your
PYTHONPATH. For example, if, when you unrolled the sample
files, you created /aaaa/bbbb/skeleton, then you will need
something like the following (or the equivalent)::
export PYTHONPATH=/aaaa/bbbb:$PYTHONPATH
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``.
Modifying the Server Script
===========================
The sample files include scripts for the following servers:
- Medusa -- server-medusa.py.
- Apache/SCGI -- server-scgi.py
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``.
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
SCGIServer 127.0.0.1 3001
SCGIHandler On
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``.
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.
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.
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.
Modifying the Application
=========================
In file ``myapp/ui/__init__.py``, do the following:
- Change all occurances of "skeleton" to "myapp" or whatever you
have named your applicaton.
- There is code in this file that exposes several static files and
directories. Uncomment or comment out this code and modify it
as needed.
- This file exposes several classes in
``myapp/ui/servicesui.ptl``.
- Look at method _q_lookup (in __init__.py). It processes the
first component in the URL. Remove and add a clause to the
``if`` statement to handle the component you need.
- Create a class to handle the next component of the URL. You
can use classes ``InfoHandler``, ``ServiceHandler``,
``MeerkatHandler``, and ``ZipfileHandler`` as examples.
In file ``myapp/ui/servicesui.ptl``, do the following:
- Add the classes required by your additions to
myapp/ui/__init__.py.
- Use the existing classes (for example, ``InfoUI``,
``ServicesUI``, ``MeerkatUI``, ``ZipfileUI``) as examples. Here
are a few notes and suggestions:
- As much as possible, put only user interface code in this
module. We'll discuss the code that does the rest of the work
when we get to ``myapp/services/``.
- You can find examples in this module for creating user
interfaces with each of the following:
- Plain PTL strings and ``request.get_form_var()`` to get
values entered at the browser.
- Module ``form.widget``, without the form object.
- Modules ``form2.form`` and form2.widget``.
Start Your Server
=================
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.
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.
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:
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)
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')
'
'
''
'\n'
'%s' % content
' \n'
' |
'
''
mycapabilityForm.render()
' |
'
'
'
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):
'\n'
'\n'
'Quixote Skeleton App -- %s\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
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
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.
Ordering the widgets
--------------------
Widgets are rendered, from top to bottom, in the same order that
you add them to the form.
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)
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('')
r += self._render_error_notice()
r += self._render_components()
r += self._render_button_widgets()
r += htmltext('
')
return r.getvalue()
def render_sep(self):
return htmltext('
|
')
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
) between
widgets. See method ``render_sep``.
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(' *')
r = TemplateIO(html=True)
r += htmltext('')
r += title
r += htmltext(' | ')
r += self.widget.render()
r += htmltext(' | ')
r += self._render_error()
r += self._render_hint()
r += htmltext(' |
')
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 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
)
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')
''
''
' 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/
`Support for Quixote Etc`_: A variety of helpful documents on
Quixote.
.. _`Support for Quixote Etc`:
http://www.rexx.com/~dkuhlman/quixote_index.html