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.
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.
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:
The sample files include scripts for the following servers:
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:
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):
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.
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:
Do the following in myapp/scripts/server-medusa.py or myapp/scripts/server-scgi.py, as appropriate:
In file myapp/ui/__init__.py, do the following:
In file myapp/ui/servicesui.ptl, do the following:
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.
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.
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:
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
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
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)
Add a new user interface class:
class MycapabilityUI: _q_exports = []
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.
Add a new import statement (at the top of the file), for example:
from myapp.mymodule import My from myapp.services import mymodule
Add a new module, for example, mymodule.py. For example:
class Mycapability: o o o
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
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.
Widgets are rendered, from top to bottom, in the same order that you add them to the form.
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)
To customize the way in which the form and the widets in it are rendered, do the following:
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:
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('<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:
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 )
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):
http://www.mems-exchange.org/software/quixote/: The Quixote support Web site.
Support for Quixote Etc: A variety of helpful documents on Quixote.