============================== 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