Quixote-1.2/0000775000225700100400000000000010131012121013533 5ustar naschememems00000000000000Quixote-1.2/demo/0000775000225700100400000000000010131012120014456 5ustar naschememems00000000000000Quixote-1.2/demo/__init__.py0000664000225700100400000000266610127042023016613 0ustar naschememems00000000000000 _q_exports = ["simple", "error", "publish_error", "widgets", "form_demo", "dumpreq", "srcdir", ("favicon.ico", "q_ico")] import sys from quixote.demo.pages import _q_index, _q_exception_handler, dumpreq from quixote.demo.widgets import widgets from quixote.demo.integer_ui import IntegerUI from quixote.errors import PublishError from quixote.util import StaticDirectory, StaticFile def simple(request): # This function returns a plain text document, not HTML. request.response.set_content_type("text/plain") return "This is the Python function 'quixote.demo.simple'.\n" def error(request): raise ValueError, "this is a Python exception" def publish_error(request): raise PublishError(public_msg="Publishing error raised by publish_error") def _q_lookup(request, component): return IntegerUI(request, component) def _q_resolve(component): # _q_resolve() is a hook that can be used to import only # when it's actually accessed. This can be used to make # start-up of your application faster, because it doesn't have # to import every single module when it starts running. if component == 'form_demo': from quixote.demo.forms import form_demo return form_demo # Get current directory import os from quixote.demo import forms curdir = os.path.dirname(forms.__file__) srcdir = StaticDirectory(curdir, list_directory=1) q_ico = StaticFile(os.path.join(curdir, 'q.ico')) Quixote-1.2/demo/demo_scgi.py0000775000225700100400000000163510102223515017003 0ustar naschememems00000000000000#!/www/python/bin/python # Example SCGI driver script for the Quixote demo: publishes the contents of # the quixote.demo package. To use this script with mod_scgi and Apache # add the following section of your Apache config file: # # # SCGIServer 127.0.0.1 4000 # SCGIHandler On # from scgi.quixote_handler import QuixoteHandler, main from quixote import enable_ptl, Publisher class DemoPublisher(Publisher): def __init__(self, *args, **kwargs): Publisher.__init__(self, *args, **kwargs) # (Optional step) Read a configuration file self.read_config("demo.conf") # Open the configured log files self.setup_logs() class DemoHandler(QuixoteHandler): publisher_class = DemoPublisher root_namespace = "quixote.demo" prefix = "/qdemo" # Install the import hook that enables PTL modules. enable_ptl() main(DemoHandler) Quixote-1.2/demo/forms.ptl0000664000225700100400000000720410127042023016342 0ustar naschememems00000000000000# quixote.demo.forms # # Demonstrate the Quixote form class. __revision__ = "$Id: forms.ptl 25234 2004-09-30 17:36:19Z nascheme $" import time from quixote.form import Form class Topping: def __init__(self, name, cost): self.name = name self.cost = cost # in cents def __str__(self): return "%s: $%.2f" % (self.name, self.cost/100.) def __repr__(self): return "<%s at %08x: %s>" % (self.__class__.__name__, id(self), self) TOPPINGS = [Topping('cheese', 50), Topping('pepperoni', 110), Topping('green peppers', 75), Topping('mushrooms', 90), Topping('sausage', 100), Topping('anchovies', 30), Topping('onions', 25)] class FormDemo(Form): def __init__(self): # build form Form.__init__(self) self.add_widget("string", "name", title="Your Name", size=20, required=1) self.add_widget("password", "password", title="Password", size=20, maxlength=20, required=1) self.add_widget("checkbox", "confirm", title="Are you sure?") self.add_widget("radiobuttons", "color", title="Eye color", allowed_values=['green', 'blue', 'brown', 'other']) self.add_widget("single_select", "size", title="Size of pizza", value='medium', allowed_values=['tiny', 'small', 'medium', 'large', 'enormous'], descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")', 'Large (14")', 'Enormous (18")'], size=1) # select widgets can use any type of object, no just strings self.add_widget("multiple_select", "toppings", title="Pizza Toppings", value=TOPPINGS[0], allowed_values=TOPPINGS, size=5) self.add_widget('hidden', 'time', value=time.time()) self.add_submit_button("go", "Go!") def render [html] (self, request, action_url): """ Quixote Form Demo

Quixote Form Demo

""" Form.render(self, request, action_url) """ """ def process(self, request): # check data form_data = Form.process(self, request) if not form_data["name"]: self.error["name"] = "You must provide your name." if not form_data["password"]: self.error["password"] = "You must provide a password." return form_data def action [html] (self, request, submit_button, form_data): # The data has been submitted and verified. Do something interesting # with it (save it in DB, send email, etc.). We'll just display it. """ Quixote Form Demo

Form data:

""" for name, value in form_data.items(): '' ' ' % name ' ' % type(value).__name__ if value is None: value = "no value" ' ' % value '' """
Name Type Value
%s%s%s
""" def form_demo(request): return FormDemo().handle(request) Quixote-1.2/demo/integer_ui.py0000664000225700100400000000322107611615551017212 0ustar naschememems00000000000000import sys from quixote.errors import TraversalError def fact(n): f = 1L while n > 1: f *= n n -= 1 return f class IntegerUI: _q_exports = ["factorial", "prev", "next"] def __init__(self, request, component): try: self.n = int(component) except ValueError, exc: raise TraversalError(str(exc)) def factorial(self, request): if self.n > 10000: sys.stderr.write("warning: possible denial-of-service attack " "(request for factorial(%d))\n" % self.n) request.response.set_header("content-type", "text/plain") return "%d! = %d\n" % (self.n, fact(self.n)) def _q_index(self, request): return """\ The Number %d You have selected the integer %d.

You can compute its factorial (%d!)

Or, you can visit the web page for the previous or next integer.

Or, you can use redirects to visit the previous or next integer. This makes it a bit easier to generate this HTML code, but it's less efficient -- your browser has to go through two request/response cycles. And someone still has to generate the URLs for the previous/next pages -- only now it's done in the prev() and next() methods for this integer.

""" % (self.n, self.n, self.n, self.n-1, self.n+1) def prev(self, request): return request.redirect("../%d/" % (self.n-1)) def next(self, request): return request.redirect("../%d/" % (self.n+1)) Quixote-1.2/demo/pages.ptl0000664000225700100400000000543410127042023016316 0ustar naschememems00000000000000# quixote.demo.pages # # Provides miscellaneous pages for the Quixote demo (currently # just the index page). __revision__ = "$Id: pages.ptl 25234 2004-09-30 17:36:19Z nascheme $" def _q_index [html] (request): print "debug message from the index page" package_name = str('.').join(__name__.split(str('.'))[:-1]) module_name = __name__ module_file = __file__ """ Quixote Demo

Hello, world!

(This page is generated by the index function for the %(package_name)s package. This index function is actually a PTL template, _q_index(), in the %(module_name)s PTL module. Look in %(module_file)s to see the source code for this PTL template.)

To understand what's going on here, be sure to read the doc/demo.txt file included with Quixote.

Here are some other features of this demo:

""" % vars() def _q_exception_handler [html] (request, exc): """ Quixote Demo

Exception Handler

A _q_exception_handler method, if present, is called when a PublishError exception is raised. It can do whatever it likes to provide a friendly page.

Here's the exception that was raised:
%s (%s).

""" % (repr(exc), str(exc)) def dumpreq [html] (request): """ HTTPRequest Object

HTTPRequest Object

""" htmltext(request.dump_html()) """ """ Quixote-1.2/demo/run_cgi.py0000664000225700100400000000202007611615551016502 0ustar naschememems00000000000000# This is a simple script that makes it easy to write one file CGI # applications that use Quixote. To use, add the following line to the top # of your CGI script: # # #!/usr/local/bin/python /run_cgi.py # # Your CGI script becomes the root namespace and you may use PTL syntax # inside the script. Errors will go to stderr and should end up in the server # error log. # # Note that this will be quite slow since the script will be recompiled on # every hit. If you are using Apache with mod_fastcgi installed you should be # able to use .fcgi as an extension instead of .cgi and get much better # performance. Maybe someday I will write code that caches the compiled # script on the filesystem. :-) import sys import new from quixote import enable_ptl, ptl_compile, Publisher enable_ptl() filename = sys.argv[1] root_code = ptl_compile.compile_template(open(filename), filename) root = new.module("root") root.__file__ = filename root.__name__ = "root" exec root_code in root.__dict__ p = Publisher(root) p.publish_cgi() Quixote-1.2/demo/session.ptl0000664000225700100400000001067210015506201016701 0ustar naschememems00000000000000# quixote.demo.session # # Application code for the Quixote session management demo. # Driver script is session_demo.cgi. __revision__ = "$Id: session.ptl 23532 2004-02-20 22:38:57Z dbinger $" from quixote import get_session_manager from quixote.errors import QueryError _q_exports = ['login', 'logout'] # Typical stuff for any Quixote app. def page_header [html] (title): '''\ %s

%s

''' % (title, title) def page_footer [html] (): '''\ ''' # We include the login form on two separate pages, so it's been factored # out to a separate template. def login_form [html] (): '''
''' def _q_index [html] (request): page_header("Quixote Session Management Demo") session = request.session # All Quixote sessions have the ability to track the user's identity # in session.user. In this simple application, session.user is just # a string which the user enters directly into this form. In the # real world, you would of course use a more sophisticated form of # authentication (eg. enter a password over an SSL connection), and # session.user might be an object with information about the user # (their email address, password hash, preferences, etc.). if session.user is None: '''

You haven\'t introduced yourself yet.
Please tell me your name: ''' login_form() else: '

Hello, %s. Good to see you again.

\n' % session.user ''' You can now: \n' # The other piece of information we track here is the number of # requests made in each session; report that information for the # current session here. """\

Your session is %s
You have made %d request(s) (including this one) in this session.

""" % (repr(session), session.num_requests) # The session manager is the collection of all sessions managed by # the current publisher, ie. in this process. Poking around in the # session manager is not something you do often, but it's really # handy for debugging/site administration. mgr = get_session_manager() session_ids = mgr.keys() '''

The current session manager is %s
It has %d session(s) in it right now:

''' % (repr(mgr), len(session_ids)) for sess_id in session_ids: sess = mgr[sess_id] (' \n' % (sess.id, sess.user and sess.user or "none", sess.num_requests)) '
session idusernum requests
%s%s%d
\n' page_footer() # The login() template has two purposes: to display a page with just a # login form, and to process the login form submitted either from the # index page or from login() itself. This is a fairly common idiom in # Quixote (just as it's a fairly common idiom with CGI scripts -- it's # just cleaner with Quixote). def login [html] (request): page_header("Quixote Session Demo: Login") session = request.session # We seem to be processing the login form. if request.form: user = request.form.get("name") if not user: raise QueryError("no user name supplied") session.user = user '

Welcome, %s! Thank you for logging in.

\n' % user 'back to start\n' # No form data to process, so generate the login form instead. When # the user submits it, we'll return to this template and take the # above branch. else: '

Please enter your name here:

\n' login_form() page_footer() # logout() just expires the current session, ie. removes it from the # session manager and instructs the client to forget about the session # cookie. The only code necessary is the call to # SessionManager.expire_session() -- the rest is just user interface. def logout [html] (request): page_header("Quixote Session Demo: Logout") session = request.session if session.user: '

Goodbye, %s. See you around.

\n' % session.user get_session_manager().expire_session(request) '

Your session has been expired.

\n' '

start over

\n' page_footer() Quixote-1.2/demo/widgets.ptl0000664000225700100400000001002210015506201016651 0ustar naschememems00000000000000# quixote.demo.widgets # # Demonstrate the Quixote widget classes. __revision__ = "$Id: widgets.ptl 23532 2004-02-20 22:38:57Z dbinger $" import time from quixote.form import widget def widgets(request): # Whether we are generating or processing the form with these # widgets, we need all the widget objects -- so create them now. widgets = {} widgets['name'] = widget.StringWidget('name', size=20) widgets['password'] = widget.PasswordWidget( 'password', size=20, maxlength=20) widgets['confirm'] = widget.CheckboxWidget('confirm') widgets['colour'] = widget.RadiobuttonsWidget( 'colour', allowed_values=['green', 'blue', 'brown', 'other']) widgets['size'] = widget.SingleSelectWidget( 'size', value='medium', allowed_values=['tiny', 'small', 'medium', 'large', 'enormous'], descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")', 'Large (14")', 'Enormous (18")']) widgets['toppings'] = widget.MultipleSelectWidget( 'toppings', value=['cheese'], allowed_values=['cheese', 'pepperoni', 'green peppers', 'mushrooms', 'sausage', 'anchovies', 'onions'], size=5) widgets['time'] = widget.HiddenWidget('time', value=time.time()) if request.form: # If we have some form data, then we're being invoked to process # the form; call process_widgets() to do the real work. We only # handle it in this page to conserve urls: the "widget" url both # generates the form and processes it, and behaves very # differently depending on whether there are form variables # present when it is invoked. return process_widgets(request, widgets) else: # No form data, so generate the form from scratch. When the # user submits it, we'll come back to this page, but # request.form won't be empty that time -- so we'll call # process_widgets() instead. return render_widgets(request, widgets) def render_widgets [html] (request, widgets): """\ Quixote Widget Demo

Quixote Widget Demo

""" """\
""" row_fmt = '''\ ''' row_fmt % ("Your name", widgets['name'].render(request)) row_fmt % ("Password", widgets['password'].render(request)) row_fmt % ("Are you sure?", widgets['confirm'].render(request)) row_fmt % ("Eye colour", widgets['colour'].render(request)) '''\ ''' % (widgets['size'].render(request), widgets['toppings'].render(request)) widgets['time'].render(request) '
%s %s
Select a
size of pizza
%s And some
pizza toppings
%s
\n' widget.SubmitButtonWidget(value="Submit").render(request) '''\ ''' def process_widgets [html] (request, widgets): """\ Quixote Widget Demo

You entered the following values:

""" row_fmt = ' \n' fallback = 'nothing' row_fmt % ("name", widgets['name'].parse(request) or fallback) row_fmt % ("password", widgets['password'].parse(request) or fallback) row_fmt % ("confirmation", widgets['confirm'].parse(request)) row_fmt % ("eye colour", widgets['colour'].parse(request) or fallback) row_fmt % ("pizza size", widgets['size'].parse(request) or fallback) toppings = widgets['toppings'].parse(request) row_fmt % ("pizza toppings", toppings and (", ".join(toppings)) or fallback) '
%s%s
\n' form_time = float(widgets['time'].parse(request)) now = time.time() ("

It took you %.1f sec to fill out and submit the form

\n" % (now - form_time)) """\ """ Quixote-1.2/demo/demo.cgi0000775000225700100400000000073307635441114016124 0ustar naschememems00000000000000#!/www/python/bin/python # Example driver script for the Quixote demo: publishes the contents of # the quixote.demo package. from quixote import enable_ptl, Publisher # Install the import hook that enables PTL modules. enable_ptl() # Create a Publisher instance app = Publisher('quixote.demo') # (Optional step) Read a configuration file app.read_config("demo.conf") # Open the configured log files app.setup_logs() # Enter the publishing main loop app.publish_cgi() Quixote-1.2/demo/demo.conf0000664000225700100400000000061510034560124016271 0ustar naschememems00000000000000# Config file for the Quixote demo. This ensures that debug and error # messages will be logged, and that you will see full error information # in your browser. (The default settings shipped in Quixote's config.py # module are for security rather than ease of testing/development.) ERROR_LOG = "/tmp/quixote-demo-error.log" DISPLAY_EXCEPTIONS = "plain" SECURE_ERRORS = 0 FIX_TRAILING_SLASH = 0 Quixote-1.2/demo/demo_scgi.sh0000775000225700100400000000211607615501221016770 0ustar naschememems00000000000000#!/bin/sh # # Example init.d script for demo_scgi.py server PATH=/bin:/usr/bin:/usr/local/bin DAEMON=./demo_scgi.py PIDFILE=/var/tmp/demo_scgi.pid NAME=`basename $DAEMON` case "$1" in start) if [ -f $PIDFILE ]; then if ps -p `cat $PIDFILE` > /dev/null 2>&1 ; then echo "$NAME appears to be already running ($PIDFILE exists)." exit 1 else echo "$PIDFILE exists, but appears to be obsolete; removing it" rm $PIDFILE fi fi echo -n "Starting $NAME: " env -i PATH=$PATH \ $DAEMON -P $PIDFILE -l /var/tmp/quixote-error.log echo "done" ;; stop) if [ -f $PIDFILE ]; then echo -n "Stopping $NAME: " kill `cat $PIDFILE` echo "done" if ps -p `cat $PIDFILE` > /dev/null 2>&1 ; then echo "$NAME is still running, not removing $PIDFILE" else rm -f $PIDFILE fi else echo "$NAME does not appear to be running ($PIDFILE doesn't exist)." fi ;; restart) $0 stop $0 start ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 ;; esac Quixote-1.2/demo/session_demo.cgi0000775000225700100400000001444507635441114017674 0ustar naschememems00000000000000#!/www/python/bin/python # Demonstrate Quixote session management, along with the application # code in session.ptl (aka quixote.demo.session). __revision__ = "$Id: session_demo.cgi 21182 2003-03-17 21:46:52Z gward $" import os from stat import ST_MTIME from time import time from cPickle import load, dump from quixote import enable_ptl from quixote.session import Session, SessionManager from quixote.publish import SessionPublisher class DemoSession (Session): """ Session class that tracks the number of requests made within a session. """ def __init__ (self, request, id): Session.__init__(self, request, id) self.num_requests = 0 def start_request (self, request): # This is called from the main object publishing loop whenever # we start processing a new request. Obviously, this is a good # place to track the number of requests made. (If we were # interested in the number of *successful* requests made, then # we could override finish_request(), which is called by # the publisher at the end of each successful request.) Session.start_request(self, request) self.num_requests += 1 def has_info (self): # Overriding has_info() is essential but non-obvious. The # session manager uses has_info() to know if it should hang on # to a session object or not: if a session is "dirty", then it # must be saved. This prevents saving sessions that don't need # to be saved, which is especially important as a defensive # measure against clients that don't handle cookies: without it, # we might create and store a new session object for every # request made by such clients. With has_info(), we create the # new session object every time, but throw it away unsaved as # soon as the request is complete. # # (Of course, if you write your session class such that # has_info() always returns true after a request has been # processed, you're back to the original problem -- and in fact, # this class *has* been written that way, because num_requests # is incremented on every request, which makes has_info() return # true, which makes SessionManager always store the session # object. In a real application, think carefully before putting # data in a session object that causes has_info() to return # true.) return (self.num_requests > 0) or Session.has_info(self) is_dirty = has_info class DirMapping: """A mapping object that stores values as individual pickle files all in one directory. You wouldn't want to use this in production unless you're using a filesystem optimized for handling large numbers of small files, like ReiserFS. However, it's pretty easy to implement and understand, it doesn't require any external libraries, and it's really easy to browse the "database". """ def __init__ (self, save_dir=None): self.set_save_dir(save_dir) self.cache = {} self.cache_time = {} def set_save_dir (self, save_dir): self.save_dir = save_dir if save_dir and not os.path.isdir(save_dir): os.mkdir(save_dir, 0700) def keys (self): return os.listdir(self.save_dir) def values (self): # This is pretty expensive! return [self[id] for id in self.keys()] def items (self): return [(id, self[id]) for id in self.keys()] def _gen_filename (self, session_id): return os.path.join(self.save_dir, session_id) def __getitem__ (self, session_id): filename = self._gen_filename(session_id) if (self.cache.has_key(session_id) and os.stat(filename)[ST_MTIME] <= self.cache_time[session_id]): return self.cache[session_id] if os.path.exists(filename): try: file = open(filename, "rb") try: print "loading session from %r" % file session = load(file) self.cache[session_id] = session self.cache_time[session_id] = time() return session finally: file.close() except IOError, err: raise KeyError(session_id, "error reading session from %s: %s" % (filename, err)) else: raise KeyError(session_id, "no such file %s" % filename) def get (self, session_id, default=None): try: return self[session_id] except KeyError: return default def has_key (self, session_id): return os.path.exists(self._gen_filename(session_id)) def __setitem__ (self, session_id, session): filename = self._gen_filename(session.id) file = open(filename, "wb") print "saving session to %s" % file dump(session, file, 1) file.close() self.cache[session_id] = session self.cache_time[session_id] = time() def __delitem__ (self, session_id): filename = self._gen_filename(session_id) if os.path.exists(filename): os.remove(filename) if self.cache.has_key(session_id): del self.cache[session_id] del self.cache_time[session_id] else: raise KeyError(session_id, "no such file: %s" % filename) # This is mostly the same as the standard boilerplate for any Quixote # driver script. The main difference is that we have to instantiate a # session manager, and use SessionPublisher instead of the normal # Publisher class. Just like demo.cgi, we use demo.conf to setup log # files and ensure that error messages are more informative than secure. # You can use the 'shelve' module to create an alternative persistent # mapping to the DirMapping class above. #import shelve #sessions = shelve.open("/tmp/quixote-sessions") enable_ptl() sessions = DirMapping(save_dir="/tmp/quixote-session-demo") session_mgr = SessionManager(session_class=DemoSession, session_mapping=sessions) app = SessionPublisher('quixote.demo.session', session_mgr=session_mgr) app.read_config("demo.conf") app.setup_logs() app.publish_cgi() Quixote-1.2/demo/upload.cgi0000775000225700100400000000350207635441114016461 0ustar naschememems00000000000000#!/www/python/bin/python # Simple demo of HTTP upload with Quixote. Also serves as an example # of how to put a (simple) Quixote application into a single file. __revision__ = "$Id: upload.cgi 21182 2003-03-17 21:46:52Z gward $" import os import stat from quixote import Publisher from quixote.html import html_quote _q_exports = ['receive'] def header (title): return '''\ %s ''' % title def footer (): return '\n' def _q_index (request): return header("Quixote Upload Demo") + '''\
Your name:

File to upload:

''' + footer() def receive (request): result = [] name = request.form.get("name") if name: result.append("

Thanks, %s!

" % html_quote(name)) upload = request.form.get("upload") size = os.stat(upload.tmp_filename)[stat.ST_SIZE] if not upload.base_filename or size == 0: title = "Empty Upload" result.append("

You appear not to have uploaded anything.

") else: title = "Upload Received" result.append("

You just uploaded %s (%d bytes)
" % (html_quote(upload.base_filename), size)) result.append("which is temporarily stored in %s.

" % html_quote(upload.tmp_filename)) return header(title) + "\n".join(result) + footer() def main (): pub = Publisher('__main__') pub.read_config("demo.conf") pub.configure(UPLOAD_DIR="/tmp/quixote-upload-demo") pub.setup_logs() pub.publish_cgi() main() Quixote-1.2/form/0000775000225700100400000000000010131012120014475 5ustar naschememems00000000000000Quixote-1.2/form/__init__.py0000664000225700100400000000300007716516105016632 0ustar naschememems00000000000000"""quixote.form The web interface framework, consisting of Form and Widget base classes (and a bunch of standard widget classes recognized by Form). Application developers will typically create a Form subclass for each form in their application; each form object will contain a number of widget objects. Custom widgets can be created by inheriting and/or composing the standard widget classes. """ # created 2000/09/19 - 22, GPW __revision__ = "$Id: __init__.py 22253 2003-08-13 20:15:01Z rmasse $" from quixote.form.form import Form, register_widget_class, FormTokenWidget from quixote.form.widget import Widget, StringWidget, FileWidget, \ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ SingleSelectWidget, SelectWidget, OptionSelectWidget, \ MultipleSelectWidget, ListWidget, SubmitButtonWidget, HiddenWidget, \ FloatWidget, IntWidget, CollapsibleListWidget, FormValueError # Register the standard widget classes register_widget_class(StringWidget) register_widget_class(FileWidget) register_widget_class(PasswordWidget) register_widget_class(TextWidget) register_widget_class(CheckboxWidget) register_widget_class(RadiobuttonsWidget) register_widget_class(SingleSelectWidget) register_widget_class(OptionSelectWidget) register_widget_class(MultipleSelectWidget) register_widget_class(ListWidget) register_widget_class(SubmitButtonWidget) register_widget_class(HiddenWidget) register_widget_class(FloatWidget) register_widget_class(IntWidget) register_widget_class(CollapsibleListWidget) Quixote-1.2/form/form.py0000664000225700100400000005026410127042023016033 0ustar naschememems00000000000000"""quixote.form.form Provides the Form class and bureaucracy for registering widget classes. (The standard widget classes are registered automatically.) """ __revision__ = "$Id: form.py 25234 2004-09-30 17:36:19Z nascheme $" from types import StringType from quixote import get_session, get_publisher from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO from quixote.form.widget import FormValueError, HiddenWidget class FormTokenWidget (HiddenWidget): def render(self, request): self.value = get_session().create_form_token() return HiddenWidget.render(self, request) JAVASCRIPT_MARKUP = htmltext('''\ ''') class Form: """ A form is the major element of an interactive web page. A form consists of the following: * widgets (input/interaction elements) * text * layout * code to process the form All four of these are the responsibility of Form classes. Typically, you will create one Form subclass for each form in your application. Thanks to the separation of responsibilities here, it's not too hard to structure things so that a given form is rendered and/or processed somewhat differently depending on context. That separation is as follows: * the constructor declares what widgets are in the form, and any static text that is always associated with those widgets (in particular, a widget title and "hint" text) * the 'render()' method combines the widgets and their associated text to create a (1-D) stream of HTML that represents the (2-D) web page that will be presented to the user * the 'process()' method parses the user input values from the form and validates them * the 'action()' method takes care of finishing whatever action was requested by the user submitting the form -- commit a database transaction, update session flags, redirect the user to a new page, etc. This class provides a default 'process()' method that just parses each widget, storing any error messages for display on the next 'render()', and returns the results (if the form parses successfully) in a dictionary. This class also provides a default 'render()' method that lays out widgets and text in a 3-column table: the first column is the widget title, the second column is the widget itself, and the third column is any hint and/or error text associated with the widget. Also provided are methods that can be used to construct this table a row at a time, so you can use this layout for most widgets, but escape from it for oddities. Instance attributes: widgets : { widget_name:string : widget:Widget } dictionary of all widgets in the form widget_order : [Widget] same widgets as 'widgets', but ordered (because order matters) submit_buttons : [SubmitButtonWidget] the submit button widgets in the form error : { widget_name:string : error_message:string } hint : { widget_name:string : hint_text:string } title : { widget_name:string : widget_title:string } required : { widget_name:string : boolean } """ TOKEN_NAME = "_form_id" # name of hidden token widget def __init__(self, method="post", enctype=None, use_tokens=1): if method not in ("post", "get"): raise ValueError("Form method must be 'post' or 'get', " "not %r" % method) self.method = method if enctype is not None and enctype not in ( "application/x-www-form-urlencoded", "multipart/form-data"): raise ValueError, ("Form enctype must be " "'application/x-www-form-urlencoded' or " "'multipart/form-data', not %r" % enctype) self.enctype = enctype # The first major component of a form: its widgets. We want # both easy access and order, so we have a dictionary and a list # of the same objects. The dictionary is keyed on widget name. # These are populated by the 'add_*_widget()' methods. self.widgets = {} self.widget_order = [] self.submit_buttons = [] self.cancel_url = None # The second major component: text. It's up to the 'render()' # method to figure out how to lay these out; the standard # 'render()' does so in a fairly sensible way that should work # for most of our forms. These are also populated by the # 'add_*_widget()' methods. self.error = {} self.hint = {} self.title = {} self.required = {} config = get_publisher().config if self.method == "post" and use_tokens and config.form_tokens: # unique token for each form, this prevents many cross-site # attacks and prevents a form from being submitted twice self.add_widget(FormTokenWidget, self.TOKEN_NAME) self.use_form_tokens = 1 else: self.use_form_tokens = 0 # Subclasses should override this method to specify the actual # widgets in this form -- typically this consists of a series of # calls to 'add_widget()', which updates the data structures we # just defined. # -- Layout (rendering) methods ------------------------------------ # The third major component of a web form is layout. These methods # combine text and widgets in a 1-D stream of HTML, or in a 2-D web # page (depending on your level of abstraction). def render(self, request, action_url): # render(request : HTTPRequest, # action_url : string) # -> HTML text # # Render a form as HTML. assert type(action_url) in (StringType, htmltext) r = TemplateIO(html=1) r += self._render_start(request, action_url, enctype=self.enctype, method=self.method) r += self._render_body(request) r += self._render_finish(request) return r.getvalue() def _render_start(self, request, action, enctype=None, method='post', name=None): r = TemplateIO(html=1) r += htmltag('form', enctype=enctype, method=method, action=action, name=name) r += self._render_hidden_widgets(request) return r.getvalue() def _render_finish(self, request): r = TemplateIO(html=1) r += htmltext('') r += self._render_javascript(request) return r.getvalue() def _render_sep(self, text, line=1): return htmltext('%s%s' '') % \ (line and htmltext('
') or '', text) def _render_error(self, error): if error: return htmltext('%s
') % nl2br(error) else: return '' def _render_hint(self, hint): if hint: return htmltext('%s') % hint else: return '' def _render_widget_row(self, request, widget): if widget.widget_type == 'hidden': return '' title = self.title[widget.name] or '' if self.required.get(widget.name): title = title + htmltext(' *') r = TemplateIO(html=1) r += htmltext('') r += title r += htmltext('' '  ') r += widget.render(request) r += htmltext('') r += self._render_error(self.error.get(widget.name)) r += self._render_hint(self.hint.get(widget.name)) r += htmltext('') return r.getvalue() def _render_hidden_widgets(self, request): r = TemplateIO(html=1) for widget in self.widget_order: if widget.widget_type == 'hidden': r += widget.render(request) r += self._render_error(self.error.get(widget.name)) return r.getvalue() def _render_submit_buttons(self, request, ncols=3): r = TemplateIO(html=1) r += htmltext('\n') % ncols for button in self.submit_buttons: r += button.render(request) r += htmltext('') return r.getvalue() def _render_visible_widgets(self, request): r = TemplateIO(html=1) for widget in self.widget_order: r += self._render_widget_row(request, widget) return r.getvalue() def _render_error_notice(self, request): if self.error: r = htmltext('' 'Warning: ' 'there were errors processing your form. ' 'See below for details.' '') else: r = '' return r def _render_required_notice(self, request): if filter(None, self.required.values()): r = htmltext('' '* = required field' '') else: r = '' return r def _render_body(self, request): r = TemplateIO(html=1) r += htmltext('') r += self._render_error_notice(request) r += self._render_required_notice(request) r += self._render_visible_widgets(request) r += self._render_submit_buttons(request) r += htmltext('
') return r.getvalue() def _render_javascript(self, request): """Render javacript code for the form, if any. Insert code lexically sorted by code_id """ javascript_code = request.response.javascript_code if javascript_code: form_code = [] code_ids = javascript_code.keys() code_ids.sort() for code_id in code_ids: code = javascript_code[code_id] if code: form_code.append(code) javascript_code[code_id] = '' if form_code: return JAVASCRIPT_MARKUP % htmltext(''.join(form_code)) return '' # -- Processing methods -------------------------------------------- # The fourth and final major component: code to process the form. # The standard 'process()' method just parses every widget and # returns a { field_name : field_value } dictionary as 'values'. def process(self, request): """process(request : HTTPRequest) -> values : { string : any } Process the form data, validating all input fields (widgets). If any errors in input fields, adds error messages to the 'error' attribute (so that future renderings of the form will include the errors). Returns a dictionary mapping widget names to parsed values. """ self.error.clear() values = {} for widget in self.widget_order: try: val = widget.parse(request) except FormValueError, exc: self.error[widget.name] = exc.msg else: values[widget.name] = val return values def action(self, request, submit, values): """action(request : HTTPRequest, submit : string, values : { string : any }) -> string Carry out the action required by a form submission. 'submit' is the name of submit button used to submit the form. 'values' is the dictionary of parsed values from 'process()'. Note that error checking cannot be done here -- it must done in the 'process()' method. """ raise NotImplementedError, "sub-classes must implement 'action()'" def handle(self, request): """handle(request : HTTPRequest) -> string Master method for handling forms. It should be called after initializing a form. Controls form action based on a request. You probably should override 'process' and 'action' instead of overriding this method. """ action_url = self.get_action_url(request) if not self.form_submitted(request): return self.render(request, action_url) submit = self.get_submit_button(request) if submit == "cancel": return request.redirect(self.cancel_url) values = self.process(request) if submit == "": # The form was submitted by unknown submit button, assume that # the submission was required to update the layout of the form. # Clear the errors and re-render the form. self.error.clear() return self.render(request, action_url) if self.use_form_tokens: # before calling action() ensure that there is a valid token # present token = values.get(self.TOKEN_NAME) if not request.session.has_form_token(token): if not self.error: # if there are other errors then don't show the token # error, the form needs to be resubmitted anyhow self.error[self.TOKEN_NAME] = ( "The form you have submitted is invalid. It has " "already been submitted or has expired. Please " "review and resubmit the form.") else: request.session.remove_form_token(token) if self.error: return self.render(request, action_url) else: return self.action(request, submit, values) # -- Convenience methods ------------------------------------------- def form_submitted(self, request): """form_submitted(request : HTTPRequest) -> boolean Return true if a form was submitted in the current request. """ return len(request.form) > 0 def get_action_url(self, request): action_url = url_quote(request.get_path()) query = request.get_environ("QUERY_STRING") if query: action_url += "?" + query return action_url def get_submit_button(self, request): """get_submit_button(request : HTTPRequest) -> string | None Get the name of the submit button that was used to submit the current form. If the browser didn't include this information in the request, use the first submit button registered. """ for button in self.submit_buttons: if request.form.has_key(button.name): return button.name else: if request.form and self.submit_buttons: return "" else: return None def get_widget(self, widget_name): return self.widgets.get(widget_name) def parse_widget(self, name, request): """parse_widget(name : string, request : HTTPRequest) -> any Parse the value of named widget. If any parse errors, store the error message (in self.error) for use in the next rendering of the form and return None; otherwise, return the value parsed from the widget (whose type depends on the widget type). """ try: return self.widgets[name].parse(request) except FormValueError, exc: self.error[name] = str(exc) return None def store_value(self, widget_name, request, target, mode="modifier", key=None, missing_error=None): """store_value(widget_name : string, request : HTTPRequest, target : instance | dict, mode : string = "modifier", key : string = widget_name, missing_error : string = None) Parse a widget and, if it parsed successfully, store its value in 'target'. The value is stored in 'target' by name 'key'; if 'key' is not supplied, it defaults to 'widget_name'. How the value is stored depends on 'mode': * modifier: call a modifier method, eg. if 'key' is "foo", call 'target.set_foo(value)' * direct: direct attribute update, eg. if 'key' is "foo" do "target.foo = value" * dict: dictionary update, eg. if 'key' is "foo" do "target['foo'] = value" If 'missing_error' is supplied, use it as an error message if the field doesn't have a value -- ie. supplying 'missing_error' means this field is required. """ value = self.parse_widget(widget_name, request) if (value is None or value == "") and missing_error: self.error[widget_name] = missing_error return None if key is None: key = widget_name if mode == "modifier": # eg. turn "name" into "target.set_name", and # call it like "target.set_name(value)" mod = getattr(target, "set_" + key) mod(value) elif mode == "direct": if not hasattr(target, key): raise AttributeError, \ ("target object %s doesn't have attribute %s" % (`target`, key)) setattr(target, key, value) elif mode == "dict": target[key] = value else: raise ValueError, "unknown update mode %s" % `mode` def clear_widget(self, widget_name): self.widgets[widget_name].clear() def get_widget_value(self, widget_name): return self.widgets[widget_name].value def set_widget_value(self, widget_name, value): self.widgets[widget_name].set_value(value) # -- Form population methods --------------------------------------- def add_widget(self, widget_type, name, value=None, title=None, hint=None, required=0, **args): """add_widget(widget_type : string | Widget, name : string, value : any = None, title : string = None, hint : string = None, required : boolean = 0, ...) -> Widget Create a new Widget object and add it to the form. The widget class used depends on 'widget_type', and the expected type of 'value' also depends on the widget class. Any extra keyword args are passed to the widget constructor. Returns the new Widget. """ if self.widgets.has_key(name): raise ValueError, "form already has '%s' variable" % name klass = get_widget_class(widget_type) new_widget = apply(klass, (name, value), args) self.widgets[name] = new_widget self.widget_order.append(new_widget) self.title[name] = title self.hint[name] = hint self.required[name] = required return new_widget def add_submit_button(self, name, value): global _widget_class if self.widgets.has_key(name): raise ValueError, "form already has '%s' variable" % name new_widget = _widget_class['submit_button'](name, value) self.widgets[name] = new_widget self.submit_buttons.append(new_widget) def add_cancel_button(self, caption, url): if not isinstance(url, (StringType, htmltext)): raise TypeError, "url must be a string (got %r)" % url self.add_submit_button("cancel", caption) self.cancel_url = url # class Form _widget_class = {} def register_widget_class(klass, widget_type=None): global _widget_class if widget_type is None: widget_type = klass.widget_type assert widget_type is not None, "widget_type must be defined" _widget_class[widget_type] = klass def get_widget_class(widget_type): global _widget_class if callable(widget_type): # Presumably someone passed a widget class object to # Widget.create_subwidget() or Form.add_widget() -- # don't bother with the widget class registry at all. return widget_type else: try: return _widget_class[widget_type] except KeyError: raise ValueError("unknown widget type %r" % widget_type) Quixote-1.2/form/widget.py0000664000225700100400000006322410127042023016353 0ustar naschememems00000000000000"""quixote.form.widget Provides the basic web widget classes: Widget itself, plus StringWidget, TextWidget, CheckboxWidget, etc. """ # created 2000/09/20, GPW __revision__ = "$Id: widget.py 25234 2004-09-30 17:36:19Z nascheme $" import struct from types import FloatType, IntType, ListType, StringType, TupleType from quixote import get_request from quixote.html import htmltext, htmlescape, htmltag, ValuelessAttr from quixote.upload import Upload class FormValueError (Exception): """Raised whenever a widget has problems parsing its value.""" def __init__(self, msg): self.msg = msg def __str__(self): return str(self.msg) class Widget: """Abstract base class for web widgets. The key elements of a web widget are: - name - widget type (how the widget looks/works in the browser) - value The name and value are instance attributes (because they're specific to a particular widget in a particular context); widget type is a class attributes. Instance attributes: name : string value : any Feel free to access these directly; to set them, use the 'set_*()' modifier methods. """ # Subclasses must define. 'widget_type' is just a string, e.g. # "string", "text", "checkbox". widget_type = None def __init__(self, name, value=None): assert self.__class__ is not Widget, "abstract class" self.set_name(name) self.set_value(value) def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.name) def __str__(self): return "%s: %s" % (self.widget_type, self.name) def set_name(self, name): self.name = name def set_value(self, value): self.value = value def clear(self): self.value = None # -- Subclasses must implement these ------------------------------- def render(self, request): """render(request) -> HTML text""" raise NotImplementedError def parse(self, request): """parse(request) -> any""" value = request.form.get(self.name) if type(value) is StringType and value.strip(): self.value = value else: self.value = None return self.value # -- Convenience methods for subclasses ---------------------------- # This one's really only for composite widgets; lives here until # we have a demonstrated need for a CompositeWidget class. def get_subwidget_name(self, name): return "%s$%s" % (self.name, name) def create_subwidget(self, widget_type, widget_name, value=None, **args): from quixote.form.form import get_widget_class klass = get_widget_class(widget_type) name = self.get_subwidget_name(widget_name) return apply(klass, (name, value), args) # class Widget # -- Fundamental widget types ------------------------------------------ # These correspond to the standard types of input tag in HTML: # text StringWidget # password PasswordWidget # radio RadiobuttonWidget # checkbox CheckboxWidget # # and also to the other basic form elements: # ")) def parse(self, request): value = Widget.parse(self, request) if value: value = value.replace("\r\n", "\n") self.value = value return self.value class CheckboxWidget (Widget): """Widget for a single checkbox: corresponds to "". Do not put multiple CheckboxWidgets with the same name in the same form. Instance attributes: value : boolean """ widget_type = "checkbox" def render(self, request): return htmltag("input", xml_end=1, type="checkbox", name=self.name, value="yes", checked=self.value and ValuelessAttr or None) def parse(self, request): self.value = request.form.has_key(self.name) return self.value class SelectWidget (Widget): """Widget for single or multiple selection; corresponds to Instance attributes: options : [ (value:any, description:any, key:string) ] value : any The value is None or an element of dict(options.values()). size : int The number of options that should be presented without scrolling. """ # NB. 'widget_type' not set here because this is an abstract class: it's # set by subclasses SingleSelectWidget and MultipleSelectWidget. def __init__(self, name, value=None, allowed_values=None, descriptions=None, options=None, size=None, sort=0, verify_selection=1): assert self.__class__ is not SelectWidget, "abstract class" self.options = [] # if options passed, cannot pass allowed_values or descriptions if allowed_values is not None: assert options is None, ( 'cannot pass both allowed_values and options') assert allowed_values, ( 'cannot pass empty allowed_values list') self.set_allowed_values(allowed_values, descriptions, sort) elif options is not None: assert descriptions is None, ( 'cannot pass both options and descriptions') assert options, ( 'cannot pass empty options list') self.set_options(options, sort) self.set_name(name) self.set_value(value) self.size = size self.verify_selection = verify_selection def get_allowed_values(self): return [item[0] for item in self.options] def get_descriptions(self): return [item[1] for item in self.options] def set_value(self, value): self.value = None for object, description, key in self.options: if value == object: self.value = value break def _generate_keys(self, values, descriptions): """Called if no keys were provided. Try to generate a set of keys that will be consistent between rendering and parsing. """ # try to use ZODB object IDs keys = [] for value in values: if value is None: oid = "" else: oid = getattr(value, "_p_oid", None) if not oid: break hi, lo = struct.unpack(">LL", oid) oid = "%x" % ((hi << 32) | lo) keys.append(oid) else: # found OID for every value return keys # can't use OIDs, try using descriptions used_keys = {} keys = map(str, descriptions) for key in keys: if used_keys.has_key(key): raise ValueError, "duplicated descriptions (provide keys)" used_keys[key] = 1 return keys def set_options(self, options, sort=0): """(options: [objects:any], sort=0) or (options: [(object:any, description:any)], sort=0) or (options: [(object:any, description:any, key:any)], sort=0) """ """ Set the options list. The list of options can be a list of objects, in which case the descriptions default to map(htmlescape, objects) applying htmlescape() to each description and key. If keys are provided they must be distinct. If the sort keyword argument is true, sort the options by case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if options: first = options[0] values = [] descriptions = [] keys = [] if type(first) is TupleType: if len(first) == 2: for value, description in options: values.append(value) descriptions.append(description) elif len(first) == 3: for value, description, key in options: values.append(value) descriptions.append(description) keys.append(str(key)) else: raise ValueError, 'invalid options %r' % options else: values = descriptions = options if not keys: keys = self._generate_keys(values, descriptions) options = zip(values, descriptions, keys) if sort: def make_sort_key(option): value, description, key = option if value is None: return ('', option) else: return (str(description).lower(), option) doptions = map(make_sort_key, options) doptions.sort() options = [item[1] for item in doptions] self.options = options def parse_single_selection(self, parsed_key): for value, description, key in self.options: if key == parsed_key: return value else: if self.verify_selection: raise FormValueError, "invalid value selected" else: return self.options[0][0] def set_allowed_values(self, allowed_values, descriptions=None, sort=0): """(allowed_values:[any], descriptions:[any], sort:boolean=0) Set the options for this widget. The allowed_values and descriptions parameters must be sequences of the same length. The sort option causes the options to be sorted using case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if descriptions is None: self.set_options(allowed_values, sort) else: assert len(descriptions) == len(allowed_values) self.set_options(zip(allowed_values, descriptions), sort) def is_selected(self, value): return value == self.value def render(self, request): if self.widget_type == "multiple_select": multiple = ValuelessAttr else: multiple = None if self.widget_type == "option_select": onchange = "submit()" else: onchange = None tags = [htmltag("select", name=self.name, multiple=multiple, onchange=onchange, size=self.size)] for object, description, key in self.options: if self.is_selected(object): selected = ValuelessAttr else: selected = None if description is None: description = "" r = htmltag("option", value=key, selected=selected) tags.append(r + htmlescape(description) + htmltext('')) tags.append(htmltext("")) return htmltext("\n").join(tags) class SingleSelectWidget (SelectWidget): """Widget for single selection. """ widget_type = "single_select" def parse(self, request): parsed_key = request.form.get(self.name) self.value = None if parsed_key: if type(parsed_key) is ListType: raise FormValueError, "cannot select multiple values" self.value = self.parse_single_selection(parsed_key) return self.value class RadiobuttonsWidget (SingleSelectWidget): """Widget for a *set* of related radiobuttons -- all have the same name, but different values (and only one of those values is returned by the whole group). Instance attributes: delim : string = None string to emit between each radiobutton in the group. If None, a single newline is emitted. """ widget_type = "radiobuttons" def __init__(self, name, value=None, allowed_values=None, descriptions=None, options=None, delim=None): SingleSelectWidget.__init__(self, name, value, allowed_values, descriptions, options) if delim is None: self.delim = "\n" else: self.delim = delim def render(self, request): tags = [] for object, description, key in self.options: if self.is_selected(object): checked = ValuelessAttr else: checked = None r = htmltag("input", xml_end=True, type="radio", name=self.name, value=key, checked=checked) tags.append(r + htmlescape(description)) return htmlescape(self.delim).join(tags) class MultipleSelectWidget (SelectWidget): """Widget for multiple selection. Instance attributes: value : [any] for multipe selects, the value is None or a list of elements from dict(self.options).values() """ widget_type = "multiple_select" def set_value(self, value): allowed_values = self.get_allowed_values() if value in allowed_values: self.value = [ value ] elif type(value) in (ListType, TupleType): self.value = [ element for element in value if element in allowed_values ] or None else: self.value = None def is_selected(self, value): if self.value is None: return value is None else: return value in self.value def parse(self, request): parsed_keys = request.form.get(self.name) self.value = None if parsed_keys: if type(parsed_keys) is ListType: self.value = [value for value, description, key in self.options if key in parsed_keys] or None else: self.value = [self.parse_single_selection(parsed_keys)] return self.value class SubmitButtonWidget (Widget): """ Instance attributes: value : boolean """ widget_type = "submit_button" def __init__(self, name=None, value=None): Widget.__init__(self, name, value) def render(self, request): value = (self.value and htmlescape(self.value) or None) return htmltag("input", xml_end=1, type="submit", name=self.name, value=value) def parse(self, request): return request.form.get(self.name) def is_submitted(self): return self.parse(get_request()) class HiddenWidget (Widget): """ Instance attributes: value : string """ widget_type = "hidden" def render(self, request): if self.value is None: value = None else: value = htmlescape(self.value) return htmltag("input", xml_end=1, type="hidden", name=self.name, value=value) def set_current_value(self, value): self.value = value request = get_request() if request.form: request.form[self.name] = value def get_current_value(self): request = get_request() if request.form: return self.parse(request) else: return self.value # -- Derived widget types ---------------------------------------------- # (these don't correspond to fundamental widget types in HTML, # so they're separated) class NumberWidget (StringWidget): """ Instance attributes: none """ # Parameterize the number type (either float or int) through # these class attributes: type_object = None # eg. int, float type_error = None # human-readable error message type_converter = None # eg. int(), float() def __init__(self, name, value=None, size=None, maxlength=None): assert self.__class__ is not NumberWidget, "abstract class" assert value is None or type(value) is self.type_object, ( "form value '%s' not a %s: got %r" % (name, self.type_object, value)) StringWidget.__init__(self, name, value, size, maxlength) def parse(self, request): value = StringWidget.parse(self, request) if value: try: self.value = self.type_converter(value) except ValueError: raise FormValueError, self.type_error return self.value class FloatWidget (NumberWidget): """ Instance attributes: value : float """ widget_type = "float" type_object = FloatType type_converter = float type_error = "must be a number" class IntWidget (NumberWidget): """ Instance attributes: value : int """ widget_type = "int" type_object = IntType type_converter = int type_error = "must be an integer" class OptionSelectWidget (SingleSelectWidget): """Widget for single selection with automatic submission and early parsing. This widget parses the request when it is created. This allows its value to be used to decide what other widgets need to be created in a form. It's a powerful feature but it can be hard to understand what's going on. Instance attributes: value : any """ widget_type = "option_select" def __init__(self, *args, **kwargs): SingleSelectWidget.__init__(self, *args, **kwargs) request = get_request() if request.form: SingleSelectWidget.parse(self, request) if self.value is None: self.value = self.options[0][0] def render(self, request): return (SingleSelectWidget.render(self, request) + htmltext('')) def parse(self, request): return self.value def get_current_option(self): return self.value class ListWidget (Widget): """Widget for lists of objects. Instance attributes: value : [any] """ widget_type = "list" def __init__(self, name, value=None, element_type=None, element_name="row", **args): assert value is None or type(value) is ListType, ( "form value '%s' not a list: got %r" % (name, value)) assert type(element_name) in (StringType, htmltext), ( "form value '%s' element_name not a string: " "got %r" % (name, element_name)) Widget.__init__(self, name, value) if element_type is None: self.element_type = "string" else: self.element_type = element_type self.args = args self.added_elements_widget = self.create_subwidget( "hidden", "added_elements") added_elements = int(self.added_elements_widget.get_current_value() or '1') self.add_button = self.create_subwidget( "submit_button", "add_element", value="Add %s" % element_name) if self.add_button.is_submitted(): added_elements += 1 self.added_elements_widget.set_current_value(str(added_elements)) self.element_widgets = [] self.element_count = 0 if self.value is not None: for element in self.value: self.add_element(element) for index in range(added_elements): self.add_element() def add_element(self, value=None): self.element_widgets.append( self.create_subwidget(self.element_type, "element_%d" % self.element_count, value=value, **self.args)) self.element_count += 1 def render(self, request): tags = [] for element_widget in self.element_widgets: tags.append(element_widget.render(request)) tags.append(self.add_button.render(request)) tags.append(self.added_elements_widget.render(request)) return htmltext('
\n').join(tags) def parse(self, request): self.value = [] for element_widget in self.element_widgets: value = element_widget.parse(request) if value is not None: self.value.append(value) self.value = self.value or None return self.value class CollapsibleListWidget (ListWidget): """Widget for lists of objects with associated delete buttons. CollapsibleListWidget behaves like ListWidget except that each element is rendered with an associated delete button. Pressing the delete button will cause the associated element name to be added to a hidden widget that remembers all deletions until the form is submitted. Only elements that are not marked as deleted will be rendered and ultimately added to the value of the widget. Instance attributes: value : [any] """ widget_type = "collapsible_list" def __init__(self, name, value=None, element_name="row", **args): self.name = name self.element_name = element_name self.deleted_elements_widget = self.create_subwidget( "hidden", "deleted_elements") self.element_delete_buttons = [] self.deleted_elements = ( self.deleted_elements_widget.get_current_value() or '') ListWidget.__init__(self, name, value=value, element_name=element_name, **args) def add_element(self, value=None): element_widget_name = "element_%d" % self.element_count if self.deleted_elements.find(element_widget_name) == -1: delete_button = self.create_subwidget( "submit_button", "delete_" + element_widget_name, value="Delete %s" % self.element_name) if delete_button.is_submitted(): self.element_count += 1 self.deleted_elements += element_widget_name self.deleted_elements_widget.set_current_value( self.deleted_elements) else: self.element_delete_buttons.append(delete_button) ListWidget.add_element(self, value=value) else: self.element_count += 1 def render(self, request): tags = [] for element_widget, element_delete_button in zip( self.element_widgets, self.element_delete_buttons): if self.deleted_elements.find(element_widget.name) == -1: tags.append(element_widget.render(request) + element_delete_button.render(request)) tags.append(self.add_button.render(request)) tags.append(self.added_elements_widget.render(request)) tags.append(self.deleted_elements_widget.render(request)) return htmltext('
\n').join(tags) Quixote-1.2/form2/0000775000225700100400000000000010131012120014557 5ustar naschememems00000000000000Quixote-1.2/form2/__init__.py0000664000225700100400000000160010026100307016675 0ustar naschememems00000000000000"""$URL: svn+ssh://svn/repos/trunk/quixote/form2/__init__.py $ $Id: __init__.py 23720 2004-03-17 16:45:59Z nascheme $ The web interface framework, consisting of Form and Widget base classes (and a bunch of standard widget classes recognized by Form). Application developers will typically create a Form instance each form in their application; each form object will contain a number of widget objects. Custom widgets can be created by inheriting and/or composing the standard widget classes. """ from quixote.form2.form import Form, FormTokenWidget from quixote.form2.widget import Widget, StringWidget, FileWidget, \ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ SingleSelectWidget, SelectWidget, OptionSelectWidget, \ MultipleSelectWidget, SubmitWidget, HiddenWidget, \ FloatWidget, IntWidget, subname, WidgetValueError, CompositeWidget, \ WidgetList Quixote-1.2/form2/compatibility.py0000664000225700100400000000653110127042023020021 0ustar naschememems00000000000000'''$URL: svn+ssh://svn/repos/trunk/quixote/form2/compatibility.py $ $Id: compatibility.py 25234 2004-09-30 17:36:19Z nascheme $ A Form subclass that provides close to the same API as the old form class (useful for transitioning existing forms). ''' from quixote.form2 import Form as _Form, Widget, StringWidget, FileWidget, \ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ SingleSelectWidget, SelectWidget, OptionSelectWidget, \ MultipleSelectWidget, SubmitWidget, HiddenWidget, \ FloatWidget, IntWidget from quixote.html import url_quote _widget_names = { "string" : StringWidget, "file" : FileWidget, "password" : PasswordWidget, "text" : TextWidget, "checkbox" : CheckboxWidget, "single_select" : SingleSelectWidget, "radiobuttons" : RadiobuttonsWidget, "multiple_select" : MultipleSelectWidget, "submit_button" : SubmitWidget, "hidden" : HiddenWidget, "float" : FloatWidget, "int" : IntWidget, "option_select" : OptionSelectWidget, } class Form(_Form): def __init__(self, *args, **kwargs): _Form.__init__(self, *args, **kwargs) self.cancel_url = None def add_widget(self, widget_class, name, value=None, title=None, hint=None, required=0, **kwargs): try: widget_class = _widget_names[widget_class] except KeyError: pass self.add(widget_class, name, value=value, title=title, hint=hint, required=required, **kwargs) def add_submit_button(self, name, value): self.add_submit(name, value) def add_cancel_button(self, caption, url): self.add_submit("cancel", caption) self.cancel_url = url def get_action_url(self, request): action_url = url_quote(request.get_path()) query = request.get_environ("QUERY_STRING") if query: action_url += "?" + query return action_url def render(self, request, action_url=None): if action_url: self.action_url = action_url return _Form.render(self) def process(self, request): values = {} for name, widget in self._names.items(): values[name] = widget.parse(request) return values def action(self, request, submit, values): raise NotImplementedError, "sub-classes must implement 'action()'" def handle(self, request): """handle(request : HTTPRequest) -> string Master method for handling forms. It should be called after initializing a form. Controls form action based on a request. You probably should override 'process' and 'action' instead of overriding this method. """ if not self.is_submitted(): return self.render(request, self.action_url) submit = self.get_submit() if submit == "cancel": return request.redirect(self.cancel_url) values = self.process(request) if submit == True: # The form was submitted by an unregistered submit button, assume # that the submission was required to update the layout of the form. self.clear_errors() return self.render(request, self.action_url) if self.has_errors(): return self.render(request, self.action_url) else: return self.action(request, submit, values) Quixote-1.2/form2/css.py0000664000225700100400000000233010061363373015744 0ustar naschememems00000000000000 BASIC_FORM_CSS = """\ form.quixote div.title { font-weight: bold; } form.quixote br.submit, form.quixote br.widget, br.quixoteform { clear: left; } form.quixote div.submit br.widget { display: none; } form.quixote div.widget { float: left; padding: 4px; padding-right: 1em; margin-bottom: 6px; } /* pretty forms (attribute selector hides from broken browsers (e.g. IE) */ form.quixote[action] { float: left; } form.quixote[action] > div.widget { float: none; } form.quixote[action] > br.widget { display: none; } form.quixote div.widget div.widget { padding: 0; margin-bottom: 0; } form.quixote div.SubmitWidget { float: left } form.quixote div.content { margin-left: 0.6em; /* indent content */ } form.quixote div.content div.content { margin-left: 0; /* indent content only for top-level widgets */ } form.quixote div.error { color: #c00; font-size: small; margin-top: .1em; } form.quixote div.hint { font-size: small; font-style: italic; margin-top: .1em; } form.quixote div.errornotice { color: #c00; padding: 0.5em; margin: 0.5em; } form.quixote div.FormTokenWidget, form.quixote.div.HiddenWidget { display: none; } """ Quixote-1.2/form2/form.py0000664000225700100400000003124210127042023016110 0ustar naschememems00000000000000"""$URL: svn+ssh://svn/repos/trunk/quixote/form2/form.py $ $Id: form.py 25234 2004-09-30 17:36:19Z nascheme $ Provides the Form class and related classes. Forms are a convenient way of building HTML forms that are composed of Widget objects. """ from quixote import get_request, get_session, get_publisher from quixote.html import url_quote, htmltag, htmltext, TemplateIO from quixote.form2.widget import HiddenWidget, StringWidget, TextWidget, \ CheckboxWidget, SingleSelectWidget, RadiobuttonsWidget, \ MultipleSelectWidget, ResetWidget, SubmitWidget, FloatWidget, \ IntWidget try: True, False, bool except NameError: True = 1 False = 0 def bool(v): return not not v class FormTokenWidget(HiddenWidget): def _parse(self, request): token = request.form.get(self.name) session = get_session() if not session.has_form_token(token): self.error = 'invalid' else: session.remove_form_token(token) def render_error(self, error): return '' def render(self): self.value = get_session().create_form_token() return HiddenWidget.render(self) class Form: """ Provides a high-level mechanism for collecting and processing user input that is based on HTML forms. Instance attributes: widgets : [Widget] widgets that are not subclasses of SubmitWidget or HiddenWidget submit_widgets : [SubmitWidget] subclasses of SubmitWidget, normally rendered at the end of the form hidden_widgets : [HiddenWidget] subclasses of HiddenWidget, normally rendered at the beginning of the form _names : { name:string : Widget } names used in the form and the widgets associated with them """ TOKEN_NAME = "_form_id" # name of hidden token widget JAVASCRIPT_MARKUP = htmltext('\n') def __init__(self, name=None, method="post", action_url=None, enctype=None, use_tokens=True, attrs=None): if method not in ("post", "get"): raise ValueError("Form method must be 'post' or 'get', " "not %r" % method) self.name = name self.method = method self.action_url = action_url or self._get_default_action_url() if not attrs: attrs = {'class': 'quixote'} elif 'class' not in attrs: attrs = attrs.copy() attrs['class'] = 'quixote' self.attrs = attrs self.widgets = [] self.submit_widgets = [] self.hidden_widgets = [] self._names = {} if enctype is not None and enctype not in ( "application/x-www-form-urlencoded", "multipart/form-data"): raise ValueError, ("Form enctype must be " "'application/x-www-form-urlencoded' or " "'multipart/form-data', not %r" % enctype) self.enctype = enctype if use_tokens and self.method == "post": config = get_publisher().config if config.form_tokens: # unique token for each form, this prevents many cross-site # attacks and prevents a form from being submitted twice self.add(FormTokenWidget, self.TOKEN_NAME, value=None) def _get_default_action_url(self): request = get_request() action_url = url_quote(request.get_path()) query = request.get_environ("QUERY_STRING") if query: action_url += "?" + query return action_url # -- Form data access methods -------------------------------------- def __getitem__(self, name): """(name:string) -> any Return a widget's value. Raises KeyError if widget named 'name' does not exist. """ try: return self._names[name].parse() except KeyError: raise KeyError, 'no widget named %r' % name def has_key(self, name): """Return true if the widget named 'name' is in the form.""" return self._names.has_key(name) def get(self, name, default=None): """(name:string, default=None) -> any Return a widget's value. Returns 'default' if widget named 'name' does not exist. """ widget = self._names.get(name) if widget is not None: return widget.parse() else: return default def get_widget(self, name): """(name:string) -> Widget | None Return the widget named 'name'. Returns None if the widget does not exist. """ return self._names.get(name) def get_submit_widgets(self): """() -> [SubmitWidget] """ return self.submit_widgets def get_all_widgets(self): """() -> [Widget] Return all the widgets that have been added to the form. Note that this while this list includes submit widgets and hidden widgets, it does not include sub-widgets (e.g. widgets that are part of CompositeWidgets) """ return self._names.values() # -- Form processing and error checking ---------------------------- def is_submitted(self): """() -> bool Return true if a form was submitted. If the form method is 'POST' and the page was not requested using 'POST', then the form is not considered to be submitted. If the form method is 'GET' then the form is considered submitted if there is any form data in the request. """ request = get_request() if self.method == 'post': if request.get_method() == 'POST': return True else: return False else: return bool(request.form) def has_errors(self): """() -> bool Ensure that all components of the form have parsed themselves. Return true if any of them have errors. """ request = get_request() has_errors = False if self.is_submitted(): for widget in self.get_all_widgets(): if widget.has_error(request=request): has_errors = True return has_errors def clear_errors(self): """Ensure that all components of the form have parsed themselves. Clear any errors that might have occured during parsing. """ request = get_request() for widget in self.get_all_widgets(): widget.clear_error(request) def get_submit(self): """() -> string | bool Get the name of the submit button that was used to submit the current form. If the form is submitted but not by any known SubmitWidget then return True. Otherwise, return False. """ request = get_request() for button in self.submit_widgets: if button.parse(request): return button.name else: if self.is_submitted(): return True else: return False def set_error(self, name, error): """(name : string, error : string) Set the error attribute of the widget named 'name'. """ widget = self._names.get(name) if not widget: raise KeyError, "unknown name %r" % name widget.set_error(error) # -- Form population methods --------------------------------------- def add(self, widget_class, name, *args, **kwargs): if self._names.has_key(name): raise ValueError, "form already has '%s' widget" % name widget = widget_class(name, *args, **kwargs) self._names[name] = widget if isinstance(widget, SubmitWidget): self.submit_widgets.append(widget) # will be rendered at end elif isinstance(widget, HiddenWidget): self.hidden_widgets.append(widget) # will be render at beginning else: self.widgets.append(widget) # convenience methods def add_submit(self, name, value=None, **kwargs): self.add(SubmitWidget, name, value, **kwargs) def add_reset(self, name, value=None, **kwargs): self.add(ResetWidget, name, value, **kwargs) def add_hidden(self, name, value=None, **kwargs): self.add(HiddenWidget, name, value, **kwargs) def add_string(self, name, value=None, **kwargs): self.add(StringWidget, name, value, **kwargs) def add_text(self, name, value=None, **kwargs): self.add(TextWidget, name, value, **kwargs) def add_checkbox(self, name, value=None, **kwargs): self.add(CheckboxWidget, name, value, **kwargs) def add_single_select(self, name, value=None, **kwargs): self.add(SingleSelectWidget, name, value, **kwargs) def add_multiple_select(self, name, value=None, **kwargs): self.add(MultipleSelectWidget, name, value, **kwargs) def add_radiobuttons(self, name, value=None, **kwargs): self.add(RadiobuttonsWidget, name, value, **kwargs) def add_float(self, name, value=None, **kwargs): self.add(FloatWidget, name, value, **kwargs) def add_int(self, name, value=None, **kwargs): self.add(IntWidget, name, value, **kwargs) # -- Layout (rendering) methods ------------------------------------ def render(self): """() -> HTML text Render a form as HTML. """ r = TemplateIO(html=True) r += self._render_start() r += self._render_body() r += self._render_finish() return r.getvalue() def _render_start(self): r = TemplateIO(html=True) r += htmltag('form', name=self.name, method=self.method, enctype=self.enctype, action=self.action_url, **self.attrs) r += self._render_hidden_widgets() return r.getvalue() def _render_finish(self): r = TemplateIO(html=True) r += htmltext('
') code = get_request().response.javascript_code if code: r += self._render_javascript(code) return r.getvalue() def _render_widgets(self): r = TemplateIO(html=True) for widget in self.widgets: r += widget.render() return r.getvalue() def _render_hidden_widgets(self): r = TemplateIO(html=True) for widget in self.hidden_widgets: r += widget.render() return r.getvalue() def _render_submit_widgets(self): r = TemplateIO(html=True) if self.submit_widgets: r += htmltext('
') for widget in self.submit_widgets: r += widget.render() r += htmltext('

') return r.getvalue() def _render_error_notice(self): token_widget = self.get_widget(self.TOKEN_NAME) if token_widget is not None and token_widget.has_error(): # form tokens are enabled but the token data in the request # does not match anything in the session. It could be an # a cross-site attack but most likely the back button has # be used return htmltext('
' 'The form you have submitted is invalid. Most ' 'likely it has been successfully submitted once ' 'already. Please review the the form data ' 'and submit the form again.' '
') else: return htmltext('
' 'There were errors processing your form. ' 'See below for details.' '
') def _render_javascript(self, javascript_code): """Render javacript code for the form. Insert code lexically sorted by code_id. """ form_code = [] code_ids = javascript_code.keys() code_ids.sort() for code_id in code_ids: code = javascript_code[code_id] if code: form_code.append(code) javascript_code[code_id] = '' if form_code: return self.JAVASCRIPT_MARKUP % htmltext(''.join(form_code)) else: return '' def _render_body(self): r = TemplateIO(html=True) if self.has_errors(): r += self._render_error_notice() r += self._render_widgets() r += self._render_submit_widgets() return r.getvalue() Quixote-1.2/form2/widget.py0000664000225700100400000007430310127262031016440 0ustar naschememems00000000000000"""$URL: svn+ssh://svn/repos/trunk/quixote/form2/widget.py $ $Id: widget.py 25244 2004-10-01 14:05:13Z dbinger $ Provides the basic web widget classes: Widget itself, plus StringWidget, TextWidget, CheckboxWidget, etc. """ import struct from types import FloatType, IntType, ListType, StringType, TupleType from quixote import get_request from quixote.html import htmltext, htmlescape, htmltag, TemplateIO from quixote.upload import Upload try: True, False except NameError: True = 1 False = 0 def subname(prefix, name): """Create a unique name for a sub-widget or sub-component.""" # $ is nice because it's valid as part of a Javascript identifier return "%s$%s" % (prefix, name) def merge_attrs(base, overrides): """({string: any}, {string: any}) -> {string: any} """ items = [] if base: items.extend(base.items()) if overrides: items.extend(overrides.items()) attrs = {} for name, val in items: if name.endswith('_'): name = name[:-1] attrs[name] = val return attrs class WidgetValueError(Exception): """May be raised a widget has problems parsing its value.""" def __init__(self, msg): self.msg = msg def __str__(self): return str(self.msg) class Widget: """Abstract base class for web widgets. Instance attributes: name : string value : any error : string title : string hint : string required : bool attrs : {string: any} _parsed : bool Feel free to access these directly; to set them, use the 'set_*()' modifier methods. """ def __init__(self, name, value=None, title="", hint="", required=False, render_br=True, attrs=None, **kwattrs): assert self.__class__ is not Widget, "abstract class" self.name = name self.value = value self.error = None self.title = title self.hint = hint self.required = required self.render_br = render_br self.attrs = merge_attrs(attrs, kwattrs) self._parsed = False def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.name) def __str__(self): return "%s: %s" % (self.__class__.__name__, self.name) def get_name(self): return self.name def set_value(self, value): self.value = value def set_error(self, error): self.error = error def get_error(self, request=None): self.parse(request=request) return self.error def has_error(self, request=None): return bool(self.get_error(request=request)) def clear_error(self, request=None): self.parse(request=request) self.error = None def set_title(self, title): self.title = title def get_title(self): return self.title def set_hint(self, hint): self.hint = hint def get_hint(self): return self.hint def is_required(self): return self.required def parse(self, request=None): if not self._parsed: self._parsed = True if request is None: request = get_request() if request.form or request.get_method() == 'POST': try: self._parse(request) except WidgetValueError, exc: self.set_error(str(exc)) if (self.required and self.value is None and not self.has_error()): self.set_error('required') return self.value def _parse(self, request): # subclasses may override but this is not part of the public API value = request.form.get(self.name) if type(value) is StringType and value.strip(): self.value = value else: self.value = None def render_title(self, title): if title: if self.required: title += htmltext('*') return htmltext('
%s
') % title else: return '' def render_hint(self, hint): if hint: return htmltext('
%s
') % hint else: return '' def render_error(self, error): if error: return htmltext('
%s
') % error else: return '' def render(self): r = TemplateIO(html=True) classnames = '%s widget' % self.__class__.__name__ r += htmltext('
') % classnames r += self.render_title(self.get_title()) r += htmltext('
') r += self.render_content() r += self.render_hint(self.get_hint()) r += self.render_error(self.get_error()) r += htmltext('
') r += htmltext('
') if self.render_br: r += htmltext('
') % classnames r += htmltext('\n') return r.getvalue() def render_content(self): raise NotImplementedError # class Widget # -- Fundamental widget types ------------------------------------------ # These correspond to the standard types of input tag in HTML: # text StringWidget # password PasswordWidget # radio RadiobuttonsWidget # checkbox CheckboxWidget # # and also to the other basic form elements: # ")) class CheckboxWidget(Widget): """Widget for a single checkbox: corresponds to "". Do not put multiple CheckboxWidgets with the same name in the same form. Instance attributes: value : boolean """ def _parse(self, request): self.value = request.form.has_key(self.name) def render_content(self): return htmltag("input", xml_end=True, type="checkbox", name=self.name, value="yes", checked=self.value and "checked" or None, **self.attrs) class SelectWidget(Widget): """Widget for single or multiple selection; corresponds to Instance attributes: options : [ (value:any, description:any, key:string) ] value : any The value is None or an element of dict(options.values()). """ def __init__(self, name, value=None, options=None, sort=False, verify_selection=True, **kwargs): assert self.__class__ is not SelectWidget, "abstract class" Widget.__init__(self, name, value, **kwargs) self.options = [] if not options: raise ValueError, "a non-empty list of 'options' is required" else: self.set_options(options, sort) self.verify_selection = verify_selection def get_allowed_values(self): return [item[0] for item in self.options] def get_descriptions(self): return [item[1] for item in self.options] def set_value(self, value): self.value = None for object, description, key in self.options: if value == object: self.value = value break def _generate_keys(self, values, descriptions): """Called if no keys were provided. Try to generate a set of keys that will be consistent between rendering and parsing. """ # try to use ZODB object IDs keys = [] for value in values: if value is None: oid = "" else: oid = getattr(value, "_p_oid", None) if not oid: break hi, lo = struct.unpack(">LL", oid) oid = "%x" % ((hi << 32) | lo) keys.append(oid) else: # found OID for every value return keys # can't use OIDs, try using descriptions used_keys = {} keys = map(str, descriptions) for key in keys: if used_keys.has_key(key): raise ValueError, "duplicated descriptions (provide keys)" used_keys[key] = 1 return keys def set_options(self, options, sort=False): """(options: [objects:any], sort=False) or (options: [(object:any, description:any)], sort=False) or (options: [(object:any, description:any, key:any)], sort=False) """ """ Set the options list. The list of options can be a list of objects, in which case the descriptions default to map(htmlescape, objects) applying htmlescape() to each description and key. If keys are provided they must be distinct. If the sort keyword argument is true, sort the options by case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if options: first = options[0] values = [] descriptions = [] keys = [] if type(first) is TupleType: if len(first) == 2: for value, description in options: values.append(value) descriptions.append(description) elif len(first) == 3: for value, description, key in options: values.append(value) descriptions.append(description) keys.append(str(key)) else: raise ValueError, 'invalid options %r' % options else: values = descriptions = options if not keys: keys = self._generate_keys(values, descriptions) options = zip(values, descriptions, keys) if sort: def make_sort_key(option): value, description, key = option if value is None: return ('', option) else: return (str(description).lower(), option) doptions = map(make_sort_key, options) doptions.sort() options = [item[1] for item in doptions] self.options = options def _parse_single_selection(self, parsed_key, default=None): for value, description, key in self.options: if key == parsed_key: return value else: if self.verify_selection: self.error = "invalid value selected" return default elif self.options: return self.options[0][0] else: return default def set_allowed_values(self, allowed_values, descriptions=None, sort=False): """(allowed_values:[any], descriptions:[any], sort:boolean=False) Set the options for this widget. The allowed_values and descriptions parameters must be sequences of the same length. The sort option causes the options to be sorted using case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if descriptions is None: self.set_options(allowed_values, sort) else: assert len(descriptions) == len(allowed_values) self.set_options(zip(allowed_values, descriptions), sort) def is_selected(self, value): return value == self.value def render_content(self): tags = [htmltag("select", name=self.name, **self.attrs)] for object, description, key in self.options: if self.is_selected(object): selected = 'selected' else: selected = None if description is None: description = "" r = htmltag("option", value=key, selected=selected) tags.append(r + htmlescape(description) + htmltext('')) tags.append(htmltext("")) return htmltext("\n").join(tags) class SingleSelectWidget(SelectWidget): """Widget for single selection. """ SELECT_TYPE = "single_select" def _parse(self, request): parsed_key = request.form.get(self.name) if parsed_key: if type(parsed_key) is ListType: self.error = "cannot select multiple values" else: self.value = self._parse_single_selection(parsed_key) else: self.value = None class RadiobuttonsWidget(SingleSelectWidget): """Widget for a *set* of related radiobuttons -- all have the same name, but different values (and only one of those values is returned by the whole group). Instance attributes: delim : string = None string to emit between each radiobutton in the group. If None, a single newline is emitted. """ SELECT_TYPE = "radiobuttons" def __init__(self, name, value=None, options=None, delim=None, **kwargs): SingleSelectWidget.__init__(self, name, value, options=options, **kwargs) if delim is None: self.delim = "\n" else: self.delim = delim def render_content(self): tags = [] for object, description, key in self.options: if self.is_selected(object): checked = 'checked' else: checked = None r = htmltag("input", xml_end=True, type="radio", name=self.name, value=key, checked=checked, **self.attrs) tags.append(r + htmlescape(description)) return htmlescape(self.delim).join(tags) class MultipleSelectWidget(SelectWidget): """Widget for multiple selection. Instance attributes: value : [any] for multipe selects, the value is None or a list of elements from dict(self.options).values() """ SELECT_TYPE = "multiple_select" def __init__(self, name, value=None, options=None, **kwargs): SelectWidget.__init__(self, name, value, options=options, multiple='multiple', **kwargs) def set_value(self, value): allowed_values = self.get_allowed_values() if value in allowed_values: self.value = [ value ] elif type(value) in (ListType, TupleType): self.value = [ element for element in value if element in allowed_values ] or None else: self.value = None def is_selected(self, value): if self.value is None: return value is None else: return value in self.value def _parse(self, request): parsed_keys = request.form.get(self.name) if parsed_keys: if type(parsed_keys) is ListType: self.value = [value for value, description, key in self.options if key in parsed_keys] or None else: _marker = [] value = self._parse_single_selection(parsed_keys, _marker) if value is _marker: self.value = None else: self.value = [value] else: self.value = None class ButtonWidget(Widget): """ Instance attributes: label : string value : boolean """ HTML_TYPE = "button" def __init__(self, name, value=None, **kwargs): Widget.__init__(self, name, value=None, **kwargs) self.set_label(value) def set_label(self, label): self.label = label def get_label(self): return self.label def render_content(self): # slightly different behavior here, we always render the # tag using the 'value' passed in as a parameter. 'self.value' # is a boolean that is true if the button's name appears # in the request. value = (self.label and htmlescape(self.label) or None) return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name, value=value, **self.attrs) def _parse(self, request): self.value = request.form.has_key(self.name) class SubmitWidget(ButtonWidget): HTML_TYPE = "submit" class ResetWidget(SubmitWidget): HTML_TYPE = "reset" class HiddenWidget(Widget): """ Instance attributes: value : string """ def set_error(self, error): if error is not None: raise TypeError, 'error not allowed on hidden widgets' def render_content(self): if self.value is None: value = None else: value = htmlescape(self.value) return htmltag("input", xml_end=True, type="hidden", name=self.name, value=value, **self.attrs) def render(self): return self.render_content() # Input elements of type hidden have no decoration. # -- Derived widget types ---------------------------------------------- # (these don't correspond to fundamental widget types in HTML, # so they're separated) class NumberWidget(StringWidget): """ Instance attributes: none """ # Parameterize the number type (either float or int) through # these class attributes: TYPE_OBJECT = None # eg. int, float TYPE_ERROR = None # human-readable error message TYPE_CONVERTER = None # eg. int(), float() def __init__(self, name, value=None, **kwargs): assert self.__class__ is not NumberWidget, "abstract class" assert value is None or type(value) is self.TYPE_OBJECT, ( "form value '%s' not a %s: got %r" % (name, self.TYPE_OBJECT, value)) StringWidget.__init__(self, name, value, **kwargs) def _parse(self, request): StringWidget._parse(self, request) if self.value is not None: try: self.value = self.TYPE_CONVERTER(self.value) except ValueError: self.error = self.TYPE_ERROR class FloatWidget(NumberWidget): """ Instance attributes: value : float """ TYPE_OBJECT = FloatType TYPE_CONVERTER = float TYPE_ERROR = "must be a number" class IntWidget(NumberWidget): """ Instance attributes: value : int """ TYPE_OBJECT = IntType TYPE_CONVERTER = int TYPE_ERROR = "must be an integer" class OptionSelectWidget(SingleSelectWidget): """Widget for single selection with automatic submission. Parse will always return a value from it's options, even if the form is not submitted. This allows its value to be used to decide what other widgets need to be created in a form. It's a powerful feature but it can be hard to understand what's going on. Instance attributes: value : any """ SELECT_TYPE = "option_select" def __init__(self, name, value=None, options=None, **kwargs): SingleSelectWidget.__init__(self, name, value, options=options, onchange='submit()', **kwargs) def parse(self, request=None): if not self._parsed: if request is None: request = get_request() self._parse(request) self._parsed = True return self.value def _parse(self, request): parsed_key = request.form.get(self.name) if parsed_key: if type(parsed_key) is ListType: self.error = "cannot select multiple values" else: self.value = self._parse_single_selection(parsed_key) elif self.value is None: self.value = self.options[0][0] def render_content(self): return (SingleSelectWidget.render_content(self) + htmltext('')) class CompositeWidget(Widget): """ Instance attributes: widgets : [Widget] _names : {name:string : Widget} """ def __init__(self, name, value=None, **kwargs): Widget.__init__(self, name, value, **kwargs) self.widgets = [] self._names = {} def _parse(self, request): for widget in self.widgets: widget.parse(request) def __getitem__(self, name): return self._names[name].parse() def get(self, name): widget = self._names.get(name) if widget: return widget.parse() return None def get_widget(self, name): return self._names.get(name) def get_widgets(self): return self.widgets def clear_error(self, request=None): Widget.clear_error(self, request) for widget in self.widgets: widget.clear_error(request) def set_widget_error(self, name, error): self._names[name].set_error(error) def has_error(self, request=None): has_error = False if Widget.has_error(self, request=request): has_error = True for widget in self.widgets: if widget.has_error(request=request): has_error = True return has_error def add(self, widget_class, name, *args, **kwargs): if self._names.has_key(name): raise ValueError, 'the name %r is already used' % name widget = widget_class(subname(self.name, name), *args, **kwargs) self._names[name] = widget self.widgets.append(widget) def render_content(self): r = TemplateIO(html=True) for widget in self.get_widgets(): r += widget.render() return r.getvalue() class WidgetList(CompositeWidget): """A variable length list of widgets. There is only one title and hint but each element of the list can have its own error. You can also set an error on the WidgetList itself (e.g. as a result of higher-level processing). Instance attributes: element_names : [string] """ def __init__(self, name, value=None, element_type=StringWidget, element_kwargs={}, element_name="row", **kwargs): assert value is None or type(value) is list, ( "value '%s' not a list: got %r" % (name, value)) assert issubclass(element_type, Widget), ( "value '%s' element_type not a Widget: " "got %r" % (name, element_type)) assert type(element_kwargs) is dict, ( "value '%s' element_kwargs not a dict: " "got %r" % (name, element_kwargs)) assert type(element_name) in (str, htmltext), ( "value '%s' element_name not a string: " "got %r" % (name, element_name)) CompositeWidget.__init__(self, name, value, **kwargs) self.element_names = [] self.add(HiddenWidget, 'added_elements') added_elements_widget = self.get_widget('added_elements') def add_element(value=None): name = "element%d" % len(self.element_names) self.add(element_type, name, value=value, **element_kwargs) self.element_names.append(name) # Add element widgets for initial value if value is not None: for element_value in value: add_element(value=element_value) # Add at least one additional element widget num_added = int(added_elements_widget.parse() or 1) for i in range(num_added): add_element() # Add submit to add more element widgets self.add(SubmitWidget, 'add_element', value='Add %s' % element_name) if self.get('add_element'): add_element() num_added += 1 added_elements_widget.set_value(num_added) def _parse(self, request): values = [] for name in self.element_names: value = self.get(name) if value is not None: values.append(value) self.value = values or None def render_content(self): r = TemplateIO(html=True) add_element_widget = self.get_widget('add_element') for widget in self.get_widgets(): if widget is add_element_widget: continue r += widget.render() r += add_element_widget.render() return r.getvalue() def render(self): r = TemplateIO(html=True) r += self.render_title(self.get_title()) add_element_widget = self.get_widget('add_element') for widget in self.get_widgets(): if widget is add_element_widget: continue r += widget.render() r += add_element_widget.render() r += self.render_hint(self.get_hint()) return r.getvalue() class WidgetDict(CompositeWidget): """A variable length dict of widgets. There is only one title and hint but each element of the list can have its own error. You can also set an error on the WidgetList itself (e.g. as a result of higher-level processing). Instance attributes: element_names : [string] """ def __init__(self, name, value=None, title='', hint='', element_key_type=StringWidget, element_value_type=StringWidget, element_key_kwargs={}, element_value_kwargs={}, element_name='row', **kwargs): assert value is None or type(value) is dict, ( 'value %r not a dict: got %r' % (name, value)) assert issubclass(element_key_type, Widget), ( "value '%s' element_key_type not a Widget: " "got %r" % (name, element_key_type)) assert issubclass(element_value_type, Widget), ( "value '%s' element_value_type not a Widget: " "got %r" % (name, element_value_type)) assert type(element_key_kwargs) is dict, ( "value '%s' element_key_kwargs not a dict: " "got %r" % (name, element_key_kwargs)) assert type(element_value_kwargs) is dict, ( "value '%s' element_value_kwargs not a dict: " "got %r" % (name, element_value_kwargs)) assert type(element_name) in (str, htmltext), ( 'value %r element_name not a string: ' 'got %r' % (name, element_name)) CompositeWidget.__init__(self, name, value, **kwargs) self.element_names = [] self.add(HiddenWidget, 'added_elements') added_elements_widget = self.get_widget('added_elements') def add_element(key=None, value=None): name = 'element%d' % len(self.element_names) self.add(element_key_type, name + 'key', value=key, render_br=False, **element_key_kwargs) self.add(element_value_type, name + 'value', value=value, **element_value_kwargs) self.element_names.append(name) # Add element widgets for initial value if value is not None: for key, element_value in value.items(): add_element(key=key, value=element_value) # Add at least one additional element widget num_added = int(added_elements_widget.parse() or 1) for i in range(num_added): add_element() # Add submit to add more element widgets self.add(SubmitWidget, 'add_element', value='Add %s' % element_name) if self.get('add_element'): add_element() num_added += 1 added_elements_widget.set_value(num_added) def _parse(self, request): values = {} for name in self.element_names: key = self.get(name + 'key') value = self.get(name + 'value') if key and value: values[key] = value self.value = values or None def render_content(self): r = TemplateIO(html=True) for name in self.element_names: if name in ('add_element', 'added_elements'): continue key_widget = self.get_widget(name + 'key') value_widget = self.get_widget(name + 'value') r += htmltext('%s
:
%s') % ( key_widget.render(), value_widget.render()) if self.render_br: r += htmltext('
') r += htmltext('\n') r += self.get_widget('add_element').render() r += self.get_widget('added_elements').render() return r.getvalue() Quixote-1.2/server/0000775000225700100400000000000010131012120015040 5ustar naschememems00000000000000Quixote-1.2/server/__init__.py0000664000225700100400000000053010127042023017161 0ustar naschememems00000000000000"""quixote.server This package is for HTTP servers, built using one or another framework, that publish a Quixote application. These servers can make it easy to run a small application without having to install and configure a full-blown Web server such as Apache. """ __revision__ = "$Id: __init__.py 25234 2004-09-30 17:36:19Z nascheme $" Quixote-1.2/server/medusa_http.py0000664000225700100400000001154210131011555017745 0ustar naschememems00000000000000#!/usr/bin/env python """quixote.server.medusa_http An HTTP handler for Medusa that publishes a Quixote application. """ __revision__ = "$Id: medusa_http.py 25276 2004-10-06 15:46:53Z nascheme $" # A simple HTTP server, using Medusa, that publishes a Quixote application. import sys import asyncore, rfc822, socket, urllib from StringIO import StringIO from medusa import http_server, xmlrpc_handler from quixote.http_response import Stream from quixote.publish import Publisher class StreamProducer: def __init__(self, stream): self.iterator = iter(stream) def more(self): try: return self.iterator.next() except StopIteration: return '' class QuixoteHandler: def __init__(self, publisher, server_name, server): """QuixoteHandler(publisher:Publisher, server_name:string, server:medusa.http_server.http_server) Publish the specified Quixote publisher. 'server_name' will be passed as the SERVER_NAME environment variable. """ self.publisher = publisher self.server_name = server_name self.server = server def match(self, request): # Always match, since this is the only handler there is. return 1 def handle_request(self, request): msg = rfc822.Message(StringIO('\n'.join(request.header))) length = int(msg.get('Content-Length', '0')) if length: request.collector = xmlrpc_handler.collector(self, request) else: self.continue_request('', request) def continue_request(self, data, request): msg = rfc822.Message(StringIO('\n'.join(request.header))) remote_addr, remote_port = request.channel.addr if '#' in request.uri: # MSIE is buggy and sometimes includes fragments in URLs [request.uri, fragment] = request.uri.split('#', 1) if '?' in request.uri: [path, query_string] = request.uri.split('?', 1) else: path = request.uri query_string = '' path = urllib.unquote(path) server_port = str(self.server.port) http_host = msg.get("Host") if http_host: if ":" in http_host: server_name, server_port = http_host.split(":", 1) else: server_name = http_host else: server_name = (self.server.ip or socket.gethostbyaddr(socket.gethostname())[0]) environ = {'REQUEST_METHOD': request.command, 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''), 'CONTENT_TYPE': msg.get('Content-type', ''), 'CONTENT_LENGTH': len(data), "GATEWAY_INTERFACE": "CGI/1.1", 'PATH_INFO': path, 'QUERY_STRING': query_string, 'REMOTE_ADDR': remote_addr, 'REMOTE_PORT': str(remote_port), 'REQUEST_URI': request.uri, 'SCRIPT_NAME': '', "SCRIPT_FILENAME": '', 'SERVER_NAME': server_name, 'SERVER_PORT': server_port, 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': self.server_name, } for title, header in msg.items(): envname = 'HTTP_' + title.replace('-', '_').upper() environ[envname] = header stdin = StringIO(data) qreq = self.publisher.create_request(stdin, environ) output = self.publisher.process_request(qreq, environ) qresponse = qreq.response if output: qresponse.set_body(output) # Copy headers from Quixote's HTTP response for name, value in qresponse.generate_headers(): # XXX Medusa's HTTP request is buggy, and only allows unique # headers. request[name] = value request.response(qresponse.status_code) # XXX should we set a default Last-Modified time? if qresponse.body is not None: if isinstance(qresponse.body, Stream): request.push(StreamProducer(qresponse.body)) else: request.push(qresponse.body) request.done() def main(): from quixote import enable_ptl enable_ptl() if len(sys.argv) == 2: port = int(sys.argv[1]) else: port = 8080 print 'Now serving the Quixote demo on port %d' % port server = http_server.http_server('', port) publisher = Publisher('quixote.demo') # When initializing the Publisher in your own driver script, # you'll want to parse a configuration file. ##publisher.read_config("/full/path/to/demo.conf") publisher.setup_logs() dh = QuixoteHandler(publisher, 'Quixote/demo', server) server.install_handler(dh) asyncore.loop() if __name__ == '__main__': main() Quixote-1.2/server/twisted_http.py0000664000225700100400000002125210127042023020150 0ustar naschememems00000000000000#!/usr/bin/env python """ twist -- Demo of an HTTP server built on top of Twisted Python. """ __revision__ = "$Id: medusa_http.py 21221 2003-03-20 16:02:41Z akuchlin $" # based on qserv, created 2002/03/19, AMK # last mod 2003.03.24, Graham Fawcett # tested on Win32 / Twisted 0.18.0 / Quixote 0.6b5 # # version 0.2 -- 2003.03.24 11:07 PM # adds missing support for session management, and for # standard Quixote response headers (expires, date) # # modified 2004/04/10 jsibre # better support for Streams # wraps output (whether Stream or not) into twisted type producer. # modified to use reactor instead of Application (Appication # has been deprecated) import urllib from twisted.protocols import http from twisted.web import server # imports for the TWProducer object from twisted.spread import pb from twisted.python import threadable from twisted.internet import abstract from quixote.http_response import Stream class QuixoteTWRequest(server.Request): def process(self): self.publisher = self.channel.factory.publisher environ = self.create_environment() # this seek is important, it doesn't work without it (it doesn't # matter for GETs, but POSTs will not work properly without it.) self.content.seek(0, 0) qxrequest = self.publisher.create_request(self.content, environ) self.quixote_publish(qxrequest, environ) resp = qxrequest.response self.setResponseCode(resp.status_code) for hdr, value in resp.generate_headers(): self.setHeader(hdr, value) if resp.body is not None: TWProducer(resp.body, self) else: self.finish() def quixote_publish(self, qxrequest, env): """ Warning, this sidesteps the Publisher.publish method, Hope you didn't override it... """ pub = self.publisher output = pub.process_request(qxrequest, env) # don't write out the output, just set the response body # the calling method will do the rest. if output: qxrequest.response.set_body(output) pub._clear_request() def create_environment(self): """ Borrowed heavily from twisted.web.twcgi """ # Twisted doesn't decode the path for us, # so let's do it here. This is also # what medusa_http.py does, right or wrong. if '%' in self.path: self.path = urllib.unquote(self.path) serverName = self.getRequestHostname().split(':')[0] env = {"SERVER_SOFTWARE": server.version, "SERVER_NAME": serverName, "GATEWAY_INTERFACE": "CGI/1.1", "SERVER_PROTOCOL": self.clientproto, "SERVER_PORT": str(self.getHost()[2]), "REQUEST_METHOD": self.method, "SCRIPT_NAME": '', "SCRIPT_FILENAME": '', "REQUEST_URI": self.uri, "HTTPS": (self.isSecure() and 'on') or 'off', "ACCEPT_ENCODING": self.getHeader('Accept-encoding'), 'CONTENT_TYPE': self.getHeader('Content-type'), 'HTTP_COOKIE': self.getHeader('Cookie'), 'HTTP_REFERER': self.getHeader('Referer'), 'HTTP_USER_AGENT': self.getHeader('User-agent'), 'SERVER_PROTOCOL': 'HTTP/1.1', } client = self.getClient() if client is not None: env['REMOTE_HOST'] = client ip = self.getClientIP() if ip is not None: env['REMOTE_ADDR'] = ip xx, xx, remote_port = self.transport.getPeer() env['REMOTE_PORT'] = remote_port env["PATH_INFO"] = self.path qindex = self.uri.find('?') if qindex != -1: env['QUERY_STRING'] = self.uri[qindex+1:] else: env['QUERY_STRING'] = '' # Propogate HTTP headers for title, header in self.getAllHeaders().items(): envname = title.replace('-', '_').upper() if title not in ('content-type', 'content-length'): envname = "HTTP_" + envname env[envname] = header return env class TWProducer(pb.Viewable): """ A class to represent the transfer of data over the network. JES Note: This has more stuff in it than is minimally neccesary. However, since I'm no twisted guru, I built this by modifing twisted.web.static.FileTransfer. FileTransfer has stuff in it that I don't really understand, but know that I probably don't need. I'm leaving it in under the theory that if anyone ever needs that stuff (e.g. because they're running with multiple threads) it'll be MUCH easier for them if I had just left it in than if they have to figure out what needs to be in there. Furthermore, I notice no performance penalty for leaving it in. """ request = None def __init__(self, data, request): self.request = request self.data = "" self.size = 0 self.stream = None self.streamIter = None self.outputBufferSize = abstract.FileDescriptor.bufferSize if isinstance(data, Stream): # data could be a Stream self.stream = data self.streamIter = iter(data) self.size = data.length elif data: # data could be a string self.data = data self.size = len(data) else: # data could be None # We'll just leave self.data as "" pass request.registerProducer(self, 0) def resumeProducing(self): """ This is twisted's version of a producer's '.more()', or an iterator's '.next()'. That is, this function is responsible for returning some content. """ if not self.request: return if self.stream: # If we were provided a Stream, let's grab some data # and push it into our data buffer buffer = [self.data] bytesInBuffer = len(buffer[-1]) while bytesInBuffer < self.outputBufferSize: try: buffer.append(self.streamIter.next()) bytesInBuffer += len(buffer[-1]) except StopIteration: # We've exhausted the Stream, time to clean up. self.stream = None self.streamIter = None break self.data = "".join(buffer) if self.data: chunkSize = min(self.outputBufferSize, len(self.data)) data, self.data = self.data[:chunkSize], self.data[chunkSize:] else: data = "" if data: self.request.write(data) if not self.data: self.request.unregisterProducer() self.request.finish() self.request = None def pauseProducing(self): pass def stopProducing(self): self.data = "" self.request = None self.stream = None self.streamIter = None # Remotely relay producer interface. def view_resumeProducing(self, issuer): self.resumeProducing() def view_pauseProducing(self, issuer): self.pauseProducing() def view_stopProducing(self, issuer): self.stopProducing() synchronized = ['resumeProducing', 'stopProducing'] threadable.synchronize(TWProducer) class QuixoteFactory(http.HTTPFactory): def __init__(self, publisher): self.publisher = publisher http.HTTPFactory.__init__(self, None) def buildProtocol(self, addr): p = http.HTTPFactory.buildProtocol(self, addr) p.requestFactory = QuixoteTWRequest return p def Server(namespace, http_port): from twisted.internet import reactor from quixote.publish import Publisher # If you want SSL, make sure you have OpenSSL, # uncomment the follownig, and uncomment the # listenSSL() call below. ##from OpenSSL import SSL ##class ServerContextFactory: ## def getContext(self): ## ctx = SSL.Context(SSL.SSLv23_METHOD) ## ctx.use_certificate_file('/path/to/pem/encoded/ssl_cert_file') ## ctx.use_privatekey_file('/path/to/pem/encoded/ssl_key_file') ## return ctx publisher = Publisher(namespace) ##publisher.setup_logs() qf = QuixoteFactory(publisher) reactor.listenTCP(http_port, qf) ##reactor.listenSSL(http_port, qf, ServerContextFactory()) return reactor def run(namespace, port): app = Server(namespace, port) app.run() if __name__ == '__main__': from quixote import enable_ptl enable_ptl() run('quixote.demo', 8080) Quixote-1.2/doc/0000775000225700100400000000000010131012120014277 5ustar naschememems00000000000000Quixote-1.2/doc/INSTALL.html0000664000225700100400000000535710131012076016317 0ustar naschememems00000000000000 Installing Quixote

Installing Quixote

Best-case scenario

If you are using Python 2.2 or later, and have never installed Quixote with your current version of Python, you're in luck. Just run

python setup.py install

and you're done. Proceed to the demo documentation to learn how to get Quixote working.

If you're using an older Python, or if you're upgrading from an older Quixote version, read on.

Upgrading from an older Quixote version

We strongly recommend that you remove any old Quixote version before installing a new one. First, find out where your old Quixote installation is:

python -c "import os, quixote; print os.path.dirname(quixote.__file__)"

and then remove away the reported directory. (If the import fails, then you don't have an existing Quixote installation.)

Then proceed as above:

python setup.py install

Using Quixote with Python 2.0 or 2.1

If you are using Python 2.0 or 2.1 then you need to install the compiler package from the Python source distribution. The compiler package is for parsing Python source code and generating Python bytecode, and the PTL compiler is built on top of it. With Python 2.0 and 2.1, this package was included in Python's source distribution, but not installed as part of the standard library.

Assuming your Python source distribution is in /tmp/Python-2.1.2:

cd /tmp/Python-2.1.2/Tools/compiler
python setup.py install

(Obviously, you'll have to adjust this to reflect your Python version and where you kept the source distribution after installing Python.)

Quixote-1.2/doc/INSTALL.txt0000664000225700100400000000317407723733642016213 0ustar naschememems00000000000000Installing Quixote ================== Best-case scenario ------------------ If you are using Python 2.2 or later, and have never installed Quixote with your current version of Python, you're in luck. Just run python setup.py install and you're done. Proceed to the demo documentation to learn how to get Quixote working. If you're using an older Python, or if you're upgrading from an older Quixote version, read on. Upgrading from an older Quixote version --------------------------------------- We strongly recommend that you remove any old Quixote version before installing a new one. First, find out where your old Quixote installation is: python -c "import os, quixote; print os.path.dirname(quixote.__file__)" and then remove away the reported directory. (If the import fails, then you don't have an existing Quixote installation.) Then proceed as above: python setup.py install Using Quixote with Python 2.0 or 2.1 ------------------------------------ If you are using Python 2.0 or 2.1 then you need to install the ``compiler`` package from the Python source distribution. The ``compiler`` package is for parsing Python source code and generating Python bytecode, and the PTL compiler is built on top of it. With Python 2.0 and 2.1, this package was included in Python's source distribution, but not installed as part of the standard library. Assuming your Python source distribution is in ``/tmp/Python-2.1.2``:: cd /tmp/Python-2.1.2/Tools/compiler python setup.py install (Obviously, you'll have to adjust this to reflect your Python version and where you kept the source distribution after installing Python.) Quixote-1.2/doc/Makefile0000664000225700100400000000114107611615551015766 0ustar naschememems00000000000000# # Makefile to convert Quixote docs to HTML # # $Id: Makefile 20217 2003-01-16 20:51:53Z akuchlin $ # TXT_FILES = $(wildcard *.txt) HTML_FILES = $(filter-out ZPL%,$(TXT_FILES:%.txt=%.html)) RST2HTML = /www/python/bin/rst2html RST2HTML_OPTS = -o us-ascii DEST_HOST = staging.mems-exchange.org DEST_DIR = /www/www-docroot/software/quixote/doc SS = default.css %.html: %.txt $(RST2HTML) $(RST2HTML_OPTS) $< $@ all: $(HTML_FILES) clean: rm -f $(HTML_FILES) install: rsync -vptgo *.html $(SS) $(DEST_HOST):$(DEST_DIR) local-install: dir=`pwd` ; \ cd $(DEST_DIR) && ln -sf $$dir/*.html $$dir/$(SS) . Quixote-1.2/doc/PTL.html0000664000225700100400000003640010131012077015642 0ustar naschememems00000000000000 PTL: Python Template Language

PTL: Python Template Language

Introduction

PTL is the templating language used by Quixote. Most web templating languages embed a real programming language in HTML, but PTL inverts this model by merely tweaking Python to make it easier to generate HTML pages (or other forms of text). In other words, PTL is basically Python with a novel way to specify function return values.

Specifically, a PTL template is designated by inserting a [plain] or [html] modifier after the function name. The value of expressions inside templates are kept, not discarded. If the type is [html] then non-literal strings are passed through a function that escapes HTML special characters.

Plain text templates

Here's a sample plain text template:

def foo [plain] (x, y = 5):
    "This is a chunk of static text."
    greeting = "hello world" # statement, no PTL output
    print 'Input values:', x, y
    z = x + y
    """You can plug in variables like x (%s)
in a variety of ways.""" % x

    "\n\n"
    "Whitespace is important in generated text.\n"
    "z = "; z
    ", but y is "
    y
    "."

Obviously, templates can't have docstrings, but otherwise they follow Python's syntactic rules: indentation indicates scoping, single-quoted and triple-quoted strings can be used, the same rules for continuing lines apply, and so forth. PTL also follows all the expected semantics of normal Python code: so templates can have parameters, and the parameters can have default values, be treated as keyword arguments, etc.

The difference between a template and a regular Python function is that inside a template the result of expressions are saved as the return value of that template. Look at the first part of the example again:

def foo [plain] (x, y = 5):
    "This is a chunk of static text."
    greeting = "hello world" # statement, no PTL output
    print 'Input values:', x, y
    z = x + y
    """You can plug in variables like x (%s)
in a variety of ways.""" % x

Calling this template with foo(1, 2) results in the following string:

This is a chunk of static text.You can plug in variables like x (1)
in a variety of ways.

Normally when Python evaluates expressions inside functions, it just discards their values, but in a [plain] PTL template the value is converted to a string using str() and appended to the template's return value. There's a single exception to this rule: None is the only value that's ever ignored, adding nothing to the output. (If this weren't the case, calling methods or functions that return None would require assigning their value to a variable. You'd have to write dummy = list.sort() in PTL code, which would be strange and confusing.)

The initial string in a template isn't treated as a docstring, but is just incorporated in the generated output; therefore, templates can't have docstrings. No whitespace is ever automatically added to the output, resulting in ...text.You can ... from the example. You'd have to add an extra space to one of the string literals to correct this.

The assignment to the greeting local variable is a statement, not an expression, so it doesn't return a value and produces no output. The output from the print statement will be printed as usual, but won't go into the string generated by the template. Quixote directs standard output into Quixote's debugging log; if you're using PTL on its own, you should consider doing something similar. print should never be used to generate output returned to the browser, only for adding debugging traces to a template.

Inside templates, you can use all of Python's control-flow statements:

def numbers [plain] (n):
    for i in range(n):
        i
        " " # PTL does not add any whitespace

Calling numbers(5) will return the string "1 2 3 4 5 ". You can also have conditional logic or exception blocks:

def international_hello [plain] (language):
    if language == "english":
        "hello"
    elif language == "french":
        "bonjour"
    else:
        raise ValueError, "I don't speak %s" % language

HTML templates

Since PTL is usually used to generate HTML documents, an [html] template type has been provided to make generating HTML easier.

A common error when generating HTML is to grab data from the browser or from a database and incorporate the contents without escaping special characters such as '<' and '&'. This leads to a class of security bugs called "cross-site scripting" bugs, where a hostile user can insert arbitrary HTML in your site's output that can link to other sites or contain JavaScript code that does something nasty (say, popping up 10,000 browser windows).

Such bugs occur because it's easy to forget to HTML-escape a string, and forgetting it in just one location is enough to open a hole. PTL offers a solution to this problem by being able to escape strings automatically when generating HTML output, at the cost of slightly diminished performance (a few percent).

Here's how this feature works. PTL defines a class called htmltext that represents a string that's already been HTML-escaped and can be safely sent to the client. The function htmlescape(string) is used to escape data, and it always returns an htmltext instance. It does nothing if the argument is already htmltext. Both htmltext and htmlescape are available from the global namespace in PTL modules.

If a template function is declared [html] instead of [text] then two things happen. First, all literal strings in the function become instances of htmltext instead of Python's str. Second, the values of expressions are passed through htmlescape() instead of str().

htmltext type is like the str type except that operations combining strings and htmltext instances will result in the string being passed through htmlescape(). For example:

>>> from quixote.html import htmltext
>>> htmltext('a') + 'b'
<htmltext 'ab'>
>>> 'a' + htmltext('b')
<htmltext 'ab'>
>>> htmltext('a%s') % 'b'
<htmltext 'ab'>
>>> response = 'green eggs & ham'
>>> htmltext('The response was: %s') % response
<htmltext 'The response was: green eggs &amp; ham'>

Note that calling str() strips the htmltext type and should be avoided since it usually results in characters being escaped more than once. While htmltext behaves much like a regular string, it is sometimes necessary to insert a str() inside a template in order to obtain a genuine string. For example, the re module requires genuine strings. We have found that explicit calls to str() can often be avoided by splitting some code out of the template into a helper function written in regular Python.

It is also recommended that the htmltext constructor be used as sparingly as possible. The reason is that when using the htmltext feature of PTL, explicit calls to htmltext become the most likely source of cross-site scripting holes. Calling htmltext is like saying "I am absolutely sure this piece of data cannot contain malicious HTML code injected by a user. Don't escape HTML special characters because I want them."

Note that literal strings in template functions declared with [html] are htmltext instances, and therefore won't be escaped. You'll only need to use htmltext when HTML markup comes from outside the template. For example, if you want to include a file containing HTML:

def output_file [html] ():
    '<html><body>' # does not get escaped
    htmltext(open("myfile.html").read())
    '</body></html>'

In the common case, templates won't be dealing with HTML markup from external sources, so you can write straightforward code. Consider this function to generate the contents of the HEAD element:

def meta_tags [html] (title, description):
    '<title>%s</title>' % title
    '<meta name="description" content="%s">\n' % description

There are no calls to htmlescape() at all, but string literals such as <title>%s</title> have all be turned into htmltext instances, so the string variables will be automatically escaped:

>>> t.meta_tags('Catalog', 'A catalog of our cool products')
<htmltext '<title>Catalog</title>
  <meta name="description" content="A catalog of our cool products">\n'>
>>> t.meta_tags('Dissertation on <HEAD>', 
...             'Discusses the "LINK" and "META" tags')
<htmltext '<title>Dissertation on &lt;HEAD&gt;</title>
  <meta name="description" 
   content="Discusses the &quot;LINK&quot; and &quot;META&quot; tags">\n'>
>>>

Note how the title and description have had HTML-escaping applied to them. (The output has been manually pretty-printed to be more readable.)

Once you start using htmltext in one of your templates, mixing plain and HTML templates is tricky because of htmltext's automatic escaping; plain templates that generate HTML tags will be double-escaped. One approach is to just use HTML templates throughout your application. Alternatively you can use str() to convert htmltext instances to regular Python strings; just be sure the resulting string isn't HTML-escaped again.

Two implementations of htmltext are provided, one written in pure Python and a second one implemented as a C extension. Both versions have seen production use.

PTL modules

PTL templates are kept in files with the extension .ptl. Like Python files, they are byte-compiled on import, and the byte-code is written to a compiled file with the extension .ptlc. Since vanilla Python doesn't know anything about PTL, Quixote provides an import hook to let you import PTL files just like regular Python modules. The standard way to install this import hook is by calling the enable_ptl() function:

from quixote import enable_ptl
enable_ptl()

(Note: if you're using ZODB, always import ZODB before installing the PTL import hook. There's some interaction which causes importing the TimeStamp module to fail when the PTL import hook is installed; we haven't debugged the problem.)

Once the import hook is installed, PTL files can be imported as if they were Python modules. If all the example templates shown here were put into a file named foo.ptl, you could then write Python code that did this:

from foo import numbers
def f():
    return numbers(10)

You may want to keep this little function in your PYTHONSTARTUP file:

def ptl():
    try:
        import ZODB
    except ImportError:
        pass
    from quixote import enable_ptl
    enable_ptl()

This is useful if you want to interactively play with a PTL module.

$Id: PTL.txt 25237 2004-09-30 18:28:31Z nascheme $

Quixote-1.2/doc/PTL.txt0000664000225700100400000002543210127050117015522 0ustar naschememems00000000000000PTL: Python Template Language ============================= Introduction ------------ PTL is the templating language used by Quixote. Most web templating languages embed a real programming language in HTML, but PTL inverts this model by merely tweaking Python to make it easier to generate HTML pages (or other forms of text). In other words, PTL is basically Python with a novel way to specify function return values. Specifically, a PTL template is designated by inserting a ``[plain]`` or ``[html]`` modifier after the function name. The value of expressions inside templates are kept, not discarded. If the type is ``[html]`` then non-literal strings are passed through a function that escapes HTML special characters. Plain text templates -------------------- Here's a sample plain text template:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x "\n\n" "Whitespace is important in generated text.\n" "z = "; z ", but y is " y "." Obviously, templates can't have docstrings, but otherwise they follow Python's syntactic rules: indentation indicates scoping, single-quoted and triple-quoted strings can be used, the same rules for continuing lines apply, and so forth. PTL also follows all the expected semantics of normal Python code: so templates can have parameters, and the parameters can have default values, be treated as keyword arguments, etc. The difference between a template and a regular Python function is that inside a template the result of expressions are saved as the return value of that template. Look at the first part of the example again:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x Calling this template with ``foo(1, 2)`` results in the following string:: This is a chunk of static text.You can plug in variables like x (1) in a variety of ways. Normally when Python evaluates expressions inside functions, it just discards their values, but in a ``[plain]`` PTL template the value is converted to a string using ``str()`` and appended to the template's return value. There's a single exception to this rule: ``None`` is the only value that's ever ignored, adding nothing to the output. (If this weren't the case, calling methods or functions that return ``None`` would require assigning their value to a variable. You'd have to write ``dummy = list.sort()`` in PTL code, which would be strange and confusing.) The initial string in a template isn't treated as a docstring, but is just incorporated in the generated output; therefore, templates can't have docstrings. No whitespace is ever automatically added to the output, resulting in ``...text.You can ...`` from the example. You'd have to add an extra space to one of the string literals to correct this. The assignment to the ``greeting`` local variable is a statement, not an expression, so it doesn't return a value and produces no output. The output from the ``print`` statement will be printed as usual, but won't go into the string generated by the template. Quixote directs standard output into Quixote's debugging log; if you're using PTL on its own, you should consider doing something similar. ``print`` should never be used to generate output returned to the browser, only for adding debugging traces to a template. Inside templates, you can use all of Python's control-flow statements:: def numbers [plain] (n): for i in range(n): i " " # PTL does not add any whitespace Calling ``numbers(5)`` will return the string ``"1 2 3 4 5 "``. You can also have conditional logic or exception blocks:: def international_hello [plain] (language): if language == "english": "hello" elif language == "french": "bonjour" else: raise ValueError, "I don't speak %s" % language HTML templates -------------- Since PTL is usually used to generate HTML documents, an ``[html]`` template type has been provided to make generating HTML easier. A common error when generating HTML is to grab data from the browser or from a database and incorporate the contents without escaping special characters such as '<' and '&'. This leads to a class of security bugs called "cross-site scripting" bugs, where a hostile user can insert arbitrary HTML in your site's output that can link to other sites or contain JavaScript code that does something nasty (say, popping up 10,000 browser windows). Such bugs occur because it's easy to forget to HTML-escape a string, and forgetting it in just one location is enough to open a hole. PTL offers a solution to this problem by being able to escape strings automatically when generating HTML output, at the cost of slightly diminished performance (a few percent). Here's how this feature works. PTL defines a class called ``htmltext`` that represents a string that's already been HTML-escaped and can be safely sent to the client. The function ``htmlescape(string)`` is used to escape data, and it always returns an ``htmltext`` instance. It does nothing if the argument is already ``htmltext``. Both ``htmltext`` and ``htmlescape`` are available from the global namespace in PTL modules. If a template function is declared ``[html]`` instead of ``[text]`` then two things happen. First, all literal strings in the function become instances of ``htmltext`` instead of Python's ``str``. Second, the values of expressions are passed through ``htmlescape()`` instead of ``str()``. ``htmltext`` type is like the ``str`` type except that operations combining strings and ``htmltext`` instances will result in the string being passed through ``htmlescape()``. For example:: >>> from quixote.html import htmltext >>> htmltext('a') + 'b' >>> 'a' + htmltext('b') >>> htmltext('a%s') % 'b' >>> response = 'green eggs & ham' >>> htmltext('The response was: %s') % response Note that calling ``str()`` strips the ``htmltext`` type and should be avoided since it usually results in characters being escaped more than once. While ``htmltext`` behaves much like a regular string, it is sometimes necessary to insert a ``str()`` inside a template in order to obtain a genuine string. For example, the ``re`` module requires genuine strings. We have found that explicit calls to ``str()`` can often be avoided by splitting some code out of the template into a helper function written in regular Python. It is also recommended that the ``htmltext`` constructor be used as sparingly as possible. The reason is that when using the htmltext feature of PTL, explicit calls to ``htmltext`` become the most likely source of cross-site scripting holes. Calling ``htmltext`` is like saying "I am absolutely sure this piece of data cannot contain malicious HTML code injected by a user. Don't escape HTML special characters because I want them." Note that literal strings in template functions declared with ``[html]`` are htmltext instances, and therefore won't be escaped. You'll only need to use ``htmltext`` when HTML markup comes from outside the template. For example, if you want to include a file containing HTML:: def output_file [html] (): '' # does not get escaped htmltext(open("myfile.html").read()) '' In the common case, templates won't be dealing with HTML markup from external sources, so you can write straightforward code. Consider this function to generate the contents of the ``HEAD`` element:: def meta_tags [html] (title, description): '%s' % title '\n' % description There are no calls to ``htmlescape()`` at all, but string literals such as ``%s`` have all be turned into ``htmltext`` instances, so the string variables will be automatically escaped:: >>> t.meta_tags('Catalog', 'A catalog of our cool products') Catalog \n'> >>> t.meta_tags('Dissertation on ', ... 'Discusses the "LINK" and "META" tags') Dissertation on <HEAD> \n'> >>> Note how the title and description have had HTML-escaping applied to them. (The output has been manually pretty-printed to be more readable.) Once you start using ``htmltext`` in one of your templates, mixing plain and HTML templates is tricky because of ``htmltext``'s automatic escaping; plain templates that generate HTML tags will be double-escaped. One approach is to just use HTML templates throughout your application. Alternatively you can use ``str()`` to convert ``htmltext`` instances to regular Python strings; just be sure the resulting string isn't HTML-escaped again. Two implementations of ``htmltext`` are provided, one written in pure Python and a second one implemented as a C extension. Both versions have seen production use. PTL modules ----------- PTL templates are kept in files with the extension .ptl. Like Python files, they are byte-compiled on import, and the byte-code is written to a compiled file with the extension ``.ptlc``. Since vanilla Python doesn't know anything about PTL, Quixote provides an import hook to let you import PTL files just like regular Python modules. The standard way to install this import hook is by calling the ``enable_ptl()`` function:: from quixote import enable_ptl enable_ptl() (Note: if you're using ZODB, always import ZODB *before* installing the PTL import hook. There's some interaction which causes importing the TimeStamp module to fail when the PTL import hook is installed; we haven't debugged the problem.) Once the import hook is installed, PTL files can be imported as if they were Python modules. If all the example templates shown here were put into a file named ``foo.ptl``, you could then write Python code that did this:: from foo import numbers def f(): return numbers(10) You may want to keep this little function in your ``PYTHONSTARTUP`` file:: def ptl(): try: import ZODB except ImportError: pass from quixote import enable_ptl enable_ptl() This is useful if you want to interactively play with a PTL module. $Id: PTL.txt 25237 2004-09-30 18:28:31Z nascheme $ Quixote-1.2/doc/ZPL.txt0000664000225700100400000000447607722704516015555 0ustar naschememems00000000000000Zope Public License (ZPL) Version 2.0 ----------------------------------------------- This software is Copyright (c) Zope Corporation (tm) and Contributors. All rights reserved. This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name Zope Corporation (tm) must not be used to endorse or promote products derived from this software without prior written permission from Zope Corporation. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of Zope Corporation. Use of them is covered in a separate agreement (see http://www.zope.com/Marks). 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software consists of contributions made by Zope Corporation and many individuals on behalf of Zope Corporation. Specific attributions are listed in the accompanying credits file. Quixote-1.2/doc/default.css0000664000225700100400000000056507611615551016475 0ustar naschememems00000000000000/* Cascading style sheet for the Quixote documentation. Just overrides what I don't like about the standard docutils stylesheet. $Id: default.css 20217 2003-01-16 20:51:53Z akuchlin $ */ @import url(/misc/docutils.css); pre.literal-block, pre.doctest-block { margin-left: 1em ; margin-right: 1em ; background-color: #f4f4f4 } tt { background-color: transparent } Quixote-1.2/doc/demo.html0000664000225700100400000010477210131012100016122 0ustar naschememems00000000000000 Running the Quixote Demo

Running the Quixote Demo

Quixote comes with a tiny demonstration application that you can install and run on your web server. In a few dozen lines of Python and PTL code, it demonstrates most of Quixote's basic capabilities. It's also an easy way to make sure that your Python installation and web server configuration are cooperating so that Quixote applications can work.

Installation

The demo is included in the quixote.demo package, which is installed along with the rest of Quixote when you run python setup.py install. The driver script (demo.cgi) and associated configuration file (demo.conf) are not installed automatically -- you'll have to copy them from the demo/ subdirectory to your web server's CGI directory. Eg., if you happen to use the same web server tree as we do:

cp -p demo/demo.cgi demo/demo.conf /www/cgi-bin

You'll almost certainly need to edit the #! line of demo.cgi to ensure that it points to the correct Python interpreter -- it should be the same interpreter that you used to run setup.py install.

Verifying the installation

Before we try to access the demo via your web server, let's make sure that the quixote and quixote.demo packages are installed on your system:

$ python
Python 2.1.1 (#2, Jul 30 2001, 12:04:51) 
[GCC 2.95.2 20000220 (Debian GNU/Linux)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> import quixote
>>> quixote.enable_ptl()
>>> import quixote.demo

(Quixote requires Python 2.0 or greater; you might have to name an explicit Python interpreter, eg. /usr/local/bin/python2.1. Make sure that the Python interpreter you use here is the same as you put in the #! line of demo.cgi, and the same that you used to install Quixote.)

If this runs without errors, then Quixote (and its demo) are installed such that you can import them. It remains to be seen if the user that will run the driver script -- usually nobody -- can import them.

Running the demo directly

Assuming that

  • your web server is running on the current host
  • your web server is configured to handle requests to /cgi-bin/demo.cgi by running the demo.cgi script that you just installed (eg. to /www/cgi-bin/demo.cgi)

then you should now be able to run the Quixote demo by directly referring to the demo.cgi script.

Another option, if you have the Medusa package installed, is to run:

python server/medusa_http.py

This will start a small pure-Python web server running on port 8080. (Medusa is available from http://www.amk.ca/python/code/medusa.html .)

Start a web browser and load

http://localhost/cgi-bin/demo.cgi/

or

http://localhost:8080/

if you're using Medusa.

You should see a page titled "Quixote Demo" with the headline "Hello, world!". Feel free to poke around; you can't break anything through the demo. (That's not to say you can't break things with Quixote in general; since Quixote gives you the full power of Python for your web applications, you have the power to create stupid security holes.)

If you don't get the "Quixote Demo" page, go look in your web server's error log. Some things that might go wrong:

  • your web server is not configured to run CGI scripts, or it might use a different base URL for them. If you're running Apache, look for something like

    ScriptAlias /cgi-bin/ /www/cgi-bin/
    

    in your httpd.conf (for some value of "/www/cgi-bin").

    (This is not a problem with Quixote or the Quixote demo; this is a problem with your web server's configuration.)

  • your web server was unable to execute the script. Make sure its permissions are correct:

    chmod 755 /www/cgi-bin/demo.cgi
    

    (This shouldn't happen if you installed demo.cgi with cp -p as illustrated above.)

  • demo.cgi started, but was unable to import the Quixote modules. In this case, there should be a short Python traceback in your web server's error log ending with a message like

    ImportError: No module named quixote
    

    Remember, just because you can "import quixote" in a Python interpreter doesn't mean the user that runs CGI scripts (usually "nobody") can. You might have installed Quixote in a non-standard location, in which case you should either install it in the standard location (your Python interpreter's "site-packages" directory) or instruct your web server to set the PYTHONPATH environment variable. Or you might be using the wrong Python interpreter -- check the #! line of demo.cgi.

  • demo.cgi started and imported Quixote, but was unable to read its config file. There should be a short Python traceback in your web server's error log ending with a message like

    IOError: [Errno 2] No such file or directory: 'demo.conf'
    

    in this case.

    Make sure you copied demo.conf to the same directory as demo.cgi, and make sure it is readable:

    chmod 644 /www/cgi-bin/demo.conf
    

    (This shouldn't happen if you install demo.conf with cp -p as illustrated above.)

Running the demo indirectly

One of the main tenets of Quixote's design is that, in a web application, the URL is part of the user interface. We consider it undesirable to expose implementation details -- such as "/cgi-bin/demo.cgi" -- to users. That sort of thing should be tucked away out of sight. Depending on your web server, this should be easy to do with a simple tweak to its configuration.

For example, say you want the "/qdemo" URL to be the location of the Quixote demo. If you're using Apache with the rewrite engine loaded and enabled, all you need to do is add this to your httpd.conf:

RewriteRule ^/qdemo(/.*) /www/cgi-bin/demo.cgi$1 [last]

With this rule in effect (don't forget to restart your server!), accesses to "/qdemo/" are the same as accesses to "/cgi-bin/demo.cgi/" -- except they're a lot easier for the user to understand and don't expose implementation details of your application.

Try it out. In your web browser, visit http://localhost/qdemo/.

You should get exactly the same page as you got visiting "/cgi-bin/demo.cgi/" earlier, and all the links should work exactly the same.

You can use any URL prefix you like -- there's nothing special about "/qdemo".

One small but important detail here is "/qdemo" versus "/qdemo/". In the above configuration, requests for "/qdemo" will fail, and requests for "/qdemo/" will succeed. See the "URL rewriting" section of web-server.txt for details and how to fix this.

Understanding the demo

Now that you've gotten the demo to run successfully, let's look under the hood and see how it works. Before we start following links in the demo (don't worry if you already have, you can't hurt anything), make sure you're watching all the relevant log files. As with any web application, log files are essential for debugging Quixote applications.

Assuming that your web server's error log is in /www/log/error_log, and that you haven't changed the DEBUG_LOG and ERROR_LOG settings in demo.conf:

$ tail -f /www/log/error_log & \
  tail -f /tmp/quixote-demo-debug.log & \
  tail -f /tmp/quixote-demo-error.log 

(Note that recent versions of GNU tail let you tail multiple files with the same command. Cool!)

Lesson 1: the top page

Reload the top of the demo, presumably http://localhost/qdemo/. You should see "debug message from the index page" in the debug log file.

Where is this message coming from? To find out, we need to delve into the source code for the demo. Load up demo/__init__.py and let's take a look. In the process, we'll learn how to explore a Quixote application and find the source code that corresponds to a given URL.

First, why are we loading demo/__init__.py? Because that's where some of the names in the "quixote.demo" namespace are defined, and it's where the list of names that may be "exported" by Quixote from this namespace to the web is given. Recall that under Quixote, every URL boils down to a callable Python object -- usually a function or method. The root of this application is a Python package ("quixote.demo"), which is just a special kind of module. But modules aren't callable -- so what does the "/qdemo/" URL boil down to? That's what _q_index() is for -- you can define a special function that is called by default when Quixote resolves a URL to a namespace rather than a callable. That is, "/qdemo/" resolves to the "quixote.demo" package; a package is a namespace, so it can't be called; therefore Quixote looks for a function called _q_index() in that namespace and calls it.

In this case, _q_index() is not defined in demo/__init__.py -- but it is imported there from the quixote.demo.pages module. This is actually a PTL module -- demo/pages.py does not exist, but demo/pages.ptl does. So load it up and take a look:

def _q_index [plain] (request):
    print "debug message from the index page"
    """
    <html>
    <head><title>Quixote Demo</title></head>
    <body>
    <h1>Hello, world!</h1>
    [...]
    </body>
    </html>
    """

A-ha! There's the PTL code that generates the "Quixote Demo" page. This _q_index() template is quite simple PTL -- it's mostly an HTML document with a single debug print thrown in to demonstrate Quixote's debug logging facility.

Outcome of lesson 1:

  • a URL maps to either a namespace (package, module, class instance) or a callable (function, method, PTL template)
  • if a URL maps to a namespace, Quixote looks for a callable _q_index() in that namespace and calls it
  • _q_index() doesn't have to be explicitly exported by your namespace; if it exists, it will be used
  • anything your application prints to standard output goes to Quixote's debug log. (If you didn't specify a debug log in your config file, debug messages are discarded.)

Lesson 3: error-handling

The next link in the "Quixote Demo" page is to the "error" document, which is handled by the error() function in demo/__init__.py. All this function does is raise an exception:

def error (request):
    raise ValueError, "this is a Python exception"

Follow the link, and you should see a Python traceback followed by a dump of the CGI environment for this request (along with other request data, such as a list of cookies).

This is extremely useful when developing, testing, and debugging. In a production environment, though, it reveals way too much about your implementation to hapless users who should happen to hit an error, and it also reveals internal details to attackers who might use it to crack your site. (It's just as easy to write an insecure web application with Quixote as with any other tool.)

Thus, Quixote offers the DISPLAY_EXCEPTIONS config variable. This is false by default, but the demo.conf file enables it. To see what happens with DISPLAY_EXCEPTIONS off, edit demo.conf and reload the "error" page. You should see a bland, generic error message that reveals very little about your implementation. (This error page is deliberately very similar, but not identical, to Apache's "Internal Server Error" page.)

Unhandled exceptions raised by application code (aka "application bugs") are only one kind of error you're likely to encounter when developing a Quixote application. The other ones are:

  • driver script crashes or doesn't run (eg. can't import quixote modules, can't load config file). This is covered under "Running the demo directly" above
  • publishing errors, such as a request for "/simpel" that should have been "/simple", or a request for a resource that exists but is denied to the current user. Quixote has a family of exception classes for dealing with these; such exceptions may be raised by Quixote itself or by your application. They are usually handled by Quixote and turned into HTTP error responses (4xx status code), but it's possible for your application to define a special handler for such exceptions.
  • bugs in Quixote itself; hopefully this won't happen, but you never know. These usually look a lot like problems in the driver script: the script crashes and prints a traceback to stderr, which most likely winds up in your web server's error log. The length of the traceback is generally a clue as to whether there's a problem with your driver script or a bug in Quixote.

Publishing errors result in a 4xx HTTP response code, and can be entirely handled by Quixote -- that is, your web server just returns the HTTP response that Quixote prepares. It's also possible to write a _q_exception_handler() method that will be called on triggering a publishing error. This method can then provide a friendlier response; for example, the page might provide a link to the site map, or the method might look at the problematic path and try to correct misspellings. The demo defines a _q_exception_handler() in demo/pages.ptl.

Application bugs result in a 5xx HTTP response code, and are entirely handled by Quixote. Don't get confused by the fact that Quixote's and Apache's "Internal Server Error" pages are quite similar!

Driver script crashes and Quixote bugs (which are essentially the same thing; the main difference is who to blame) are handled by your web server. (In the first case, Quixote doesn't even enter into it; in the second case, Quixote dies horribly and is no longer in control.) Under Apache, the Python traceback resulting from the crash is written to Apache's error log, and a 5xx response is returned to the client with Apache's "Internal Server Error" error page.

Lesson 4: object publishing

Publishing Python callables on the web -- i.e., translating URLs to Python functions/methods/PTL templates and calling them to determine the HTTP response -- is a very powerful way of writing web applications. However, Quixote has one more trick up its sleeve: object publishing. You can translate arbitrary names to arbitrary objects which are then published on the web, and you can create URLs that call methods on those objects.

This is all accomplished with the _q_lookup() function. Every namespace that Quixote encounters may have a _q_lookup(), just like it may have a _q_index(). _q_index() is used to handle requests for the empty name -- as we saw in Lesson 1, a request for "/qdemo/", maps to the "quixote.demo" namespace; the empty string after the last slash means that Quixote will call _q_index() in this namespace to handle the request.

_q_lookup() is for requests that aren't handled by a Python callable in the namespace. As seen in Lessons 2 and 3, requests for "/qdemo/simple" and "/qdemo/error" are handled by the simple() and error() functions in the "quixote.demo" namespace. What if someone requests "/qdemo/foo"? There's no function foo() in the "quixote.demo" namespace, so normally this would be an error. (Specifically, it would be a publishing error: Quixote would raise TraversalError, which is the error used for non-existent or non-exported names. Another part of Quixote then turns this into an HTTP 404 response.)

However, this particular namespace also defines a _q_lookup() function. That means that the application wants a chance to handle unknown names before Quixote gives up entirely. Let's take a look at the implementation of _q_lookup():

from quixote.demo.integer_ui import IntegerUI
[...]
def _q_lookup(request, component):
    return IntegerUI(request, component)

Pretty simple: we just construct an IntegerUI object and return it. So what is IntegerUI? Take a look in the demo/integer_ui.py file to see; it's just a web interface to integers. (Normally, you would write a wrapper class that provides a web interface to something more interesting than integers. This just demonstrates how simple an object published by Quixote can be.)

So, what is an IntegerUI object? From Quixote's point of view, it's just another namespace to publish: like modules and packages, class instances have attributes, some of which (methods) are callable. In the case of IntegerUI, two of those attributes are _q_exports and _q_index -- every namespace published by Quixote must have an export list, and an index function is almost always advisable.

What this means is that any name that the IntegerUI constructor accepts is a valid name to tack onto the "/qdemo/" URL. Take a look at the IntegerUI constructor; you'll see that it works fine when passed something that can be converted to an integer (eg. "12" or 1.0), and raises Quixote's TraversalError if not. As it happens, Quixote always passes in a string -- URLs are just strings, after all -- so we only have to worry about things like "12" or "foo".

The error case is actually easier to understand, so try to access http://localhost/qdemo/foo/. You should get an error page that complains about an "invalid literal for int()".

Now let's build a real IntegerUI object and see the results. Follow the third link in the "Quixote Demo" page, or just go to http://localhost/qdemo/12/. You should see a web page titled "The Number 12".

This web page is generated by the _q_index() method of IntegerUI: after all, you've selected a namespace (the IntegerUI object corresponding to the number 12) with no explicit callable, so Quixote falls back on the _q_index() attribute of that namespace.

IntegerUI only exports one interesting method, factorial(). You can call this method by following the "factorial" link, or just by accessing http://localhost/qdemo/12/factorial.

Remember how I said the URL is part of the user interface? Here's a great example: edit the current URL to point to a different integer. A fun one to try is 2147483646. If you follow the "next" link, you'll get an OverflowError traceback (unless you're using a 64-bit Python!), because the web page for 2147483647 attempts to generate its own "next" link to the web page for 2147483648 -- but that fails because current versions of Python on 32-bit platforms can't handle regular integers larger than 2147483647.

Now go back to the page for 2147483646 and hit the "factorial" link. Run "top" on the web server. Get yourself a coffee. Await the heat death of the universe. (Actually, your browser will probably timeout first.) This doesn't overflow, because the factorial() function uses Python long integers, which can handle any integer -- they just take a while to get there. However, it illustrates another interesting vulnerability: an attacker could use this to launch a denial-of-service attack on the server running the Quixote demo. (Hey, it's just a demo!)

Rather than fix the DoS vulnerability, I decided to use it to illustrate another Quixote feature: if you write to stderr, the message winds up in the Quixote error log for this application (/tmp/quixote-demo-error.log by default). The IntegerUI.factorial() method uses this to log a warning of an apparent denial-of-service attack:

def factorial (self, request):
    if self.n > 10000:
        sys.stderr.write("warning: possible denial-of-service attack "
                         "(request for factorial(%d))\n" % self.n)
    return "%d! = %d" % (self.n, fact(self.n))

Since the Quixote error log is where application tracebacks are recorded, you should be watching this log file regularly, so you would presumably notice these messages.

In real life, you'd probably just deny such a ludicrous request. You could do this by raising a Quixote publishing error. For example:

def factorial (self, request):
    from quixote.errors import AccessError
    if self.n > 10000:
        raise AccessError("ridiculous request denied")
    return "%d! = %d" % (self.n, fact(self.n))

Lesson 5: widgets

You can't get very far writing web applications without writing forms, and the building blocks of web forms are generally called "form elements": string input, checkboxes, radiobuttons, select lists, and so forth. Quixote provides an abstraction for all of these form elements: the Widget class hierarchy. The widget classes are explained in detail in widget.txt; I'm going to give a brief description of the "Quixote Widget Demo" page and the code behind it here.

If you follow the "widgets" link from the main page, you'll see a fairly ordinary-looking web form -- the sort of thing you might have to fill out to order a pizza on-line, with the oddity that this pizza shop is asking for your eye colour. (Hey, I had to demonstrate radiobuttons somehow!) This form demonstrates most of HTML's basic form capabilities: a simple string, a password, a checkbox, a set of radiobuttons, a single-select list, and a multiple-select list.

Whenever you implement a web form, there are two things you have to worry about: generating the form elements and processing the client-submitted form data. There are as many ways of dividing up this work as there are web programmers. (Possibly more: every time I tackle this problem, I seem to come up with a different way of solving it.) The form in the Quixote widget demo is implemented in three parts, all of them in demo/widgets.ptl:

  • widgets() is the callable that handles the "/qdemo/widgets" URL. This template creates all the widget objects needed for the form and then calls either render_widgets() or process_widgets() as appropriate.
  • render_widgets() is called by widgets() when there is no form data to process, eg. on the first access to the "/qdemo/widgets" URL. It generates the form elements and returns an HTML document consisting of a table that lays them out in an attractive form.
  • process_widgets() is called by widgets() when there is form data to process, ie. when the form generated by render_widgets() is submitted by the user. It processes the form data, ie. it looks up the user-submitted form values and returns an HTML document listing those values.

This division of labour works well with Quixote's widget classes, since you need a collection of Widget objects whether you are generating the form elements or processing form data. For generating the form, we (1) create all the widget objects, and (2) generate an HTML document that includes the output of each widget object's render() method. (Laying out the form is the responsibility of render_widgets(), which is why it's littered with table tags.) For processing the form, we (1) create all the widget objects, and (2) generate an HTML document incorporating the user-submitted form values. In both cases, step (1) is handled by the widgets() template, which then calls either render_widgets() or process_widgets() for step (2).

Thus, there are three things you have to understand about widget objects: how to create them, how to render them, and how to use them to parse form values. Widget creation is the only step that's very interesting, since each widget class has different constructor arguments. For example, here's how we create the "name" widget in the pizza shop form:

widgets['name'] = widget.StringWidget('name', size=20)

When rendered, this widget will produce the following HTML:

<input size="20" name="name" type="text">

A more complex example is the "pizza size" widget:

widgets['size'] = widget.SingleSelectWidget(
    'size', value='medium',
    allowed_values=['tiny', 'small', 'medium', 'large', 'enormous'],
    descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")',
                  'Large (14")', 'Enormous (18")'],
    size=5)

which will generate the following HTML when rendered:

<select size="5" name="size">
  <option value="0">Tiny (4")
  <option value="1">Small (6")
  <option selected value="2">Medium (10")
  <option value="3">Large (14")
  <option value="4">Enormous (18")
</select>

Some things you might need to know about widget creation:

  • the standard widget classes are in the quixote.form.widget module; see widget.txt and/or the source code for the complete list
  • every widget class constructor has exactly one required argument: the widget name. This is used as the form element name in the generated HTML. (Things are a bit different for compound widgets, but I'm not covering them in this document.)
  • every widget class supports a number of keyword arguments that generally correspond to attributes of some HTML tag. The one argument common to all widget classes is value, the current value for this widget.

Rendering widgets is easy: just call the render() method, passing in the current HTTPRequest object. (It's currently not used by the standard widget classes, but could be used by derived or compound widget classes for context-sensitive widget rendering.)

Parsing form values is just as easy: call the parse() method, again passing in the current HTTPRequest object. The return value depends on the nature of the widget, eg.:

  • StringWidget and PasswordWidget return a string
  • CheckboxWidget returns a boolean
  • RadiobuttonsWidget, SingleSelectWidget, and MultipleSelectWidget return one of the values supplied in allowed_values -- in the demo these are all strings, but they can be any Python value. (If the client submits bogus data, the widget will return None.)

$Id: demo.txt 21292 2003-04-08 16:48:47Z akuchlin $

Quixote-1.2/doc/demo.txt0000664000225700100400000006771507644576557016060 0ustar naschememems00000000000000Running the Quixote Demo ======================== Quixote comes with a tiny demonstration application that you can install and run on your web server. In a few dozen lines of Python and PTL code, it demonstrates most of Quixote's basic capabilities. It's also an easy way to make sure that your Python installation and web server configuration are cooperating so that Quixote applications can work. Installation ------------ The demo is included in the quixote.demo package, which is installed along with the rest of Quixote when you run ``python setup.py install``. The driver script (demo.cgi) and associated configuration file (demo.conf) are *not* installed automatically -- you'll have to copy them from the demo/ subdirectory to your web server's CGI directory. Eg., if you happen to use the same web server tree as we do:: cp -p demo/demo.cgi demo/demo.conf /www/cgi-bin You'll almost certainly need to edit the ``#!`` line of demo.cgi to ensure that it points to the correct Python interpreter -- it should be the same interpreter that you used to run ``setup.py install``. Verifying the installation -------------------------- Before we try to access the demo via your web server, let's make sure that the quixote and quixote.demo packages are installed on your system:: $ python Python 2.1.1 (#2, Jul 30 2001, 12:04:51) [GCC 2.95.2 20000220 (Debian GNU/Linux)] on linux2 Type "copyright", "credits" or "license" for more information. >>> import quixote >>> quixote.enable_ptl() >>> import quixote.demo (Quixote requires Python 2.0 or greater; you might have to name an explicit Python interpreter, eg. ``/usr/local/bin/python2.1``. Make sure that the Python interpreter you use here is the same as you put in the ``#!`` line of demo.cgi, and the same that you used to install Quixote.) If this runs without errors, then Quixote (and its demo) are installed such that you can import them. It remains to be seen if the user that will run the driver script -- usually ``nobody`` -- can import them. Running the demo directly ------------------------- Assuming that * your web server is running on the current host * your web server is configured to handle requests to /cgi-bin/demo.cgi by running the demo.cgi script that you just installed (eg. to /www/cgi-bin/demo.cgi) then you should now be able to run the Quixote demo by directly referring to the demo.cgi script. Another option, if you have the Medusa package installed, is to run:: python server/medusa_http.py This will start a small pure-Python web server running on port 8080. (Medusa is available from http://www.amk.ca/python/code/medusa.html .) Start a web browser and load :: http://localhost/cgi-bin/demo.cgi/ or :: http://localhost:8080/ if you're using Medusa. You should see a page titled "Quixote Demo" with the headline "Hello, world!". Feel free to poke around; you can't break anything through the demo. (That's not to say you can't break things with Quixote in general; since Quixote gives you the full power of Python for your web applications, you have the power to create stupid security holes.) If you don't get the "Quixote Demo" page, go look in your web server's error log. Some things that might go wrong: * your web server is not configured to run CGI scripts, or it might use a different base URL for them. If you're running Apache, look for something like :: ScriptAlias /cgi-bin/ /www/cgi-bin/ in your httpd.conf (for some value of "/www/cgi-bin"). (This is not a problem with Quixote or the Quixote demo; this is a problem with your web server's configuration.) * your web server was unable to execute the script. Make sure its permissions are correct:: chmod 755 /www/cgi-bin/demo.cgi (This shouldn't happen if you installed demo.cgi with ``cp -p`` as illustrated above.) * demo.cgi started, but was unable to import the Quixote modules. In this case, there should be a short Python traceback in your web server's error log ending with a message like :: ImportError: No module named quixote Remember, just because you can "import quixote" in a Python interpreter doesn't mean the user that runs CGI scripts (usually "nobody") can. You might have installed Quixote in a non-standard location, in which case you should either install it in the standard location (your Python interpreter's "site-packages" directory) or instruct your web server to set the PYTHONPATH environment variable. Or you might be using the wrong Python interpreter -- check the ``#!`` line of demo.cgi. * demo.cgi started and imported Quixote, but was unable to read its config file. There should be a short Python traceback in your web server's error log ending with a message like :: IOError: [Errno 2] No such file or directory: 'demo.conf' in this case. Make sure you copied demo.conf to the same directory as demo.cgi, and make sure it is readable:: chmod 644 /www/cgi-bin/demo.conf (This shouldn't happen if you install demo.conf with ``cp -p`` as illustrated above.) Running the demo indirectly --------------------------- One of the main tenets of Quixote's design is that, in a web application, the URL is part of the user interface. We consider it undesirable to expose implementation details -- such as "/cgi-bin/demo.cgi" -- to users. That sort of thing should be tucked away out of sight. Depending on your web server, this should be easy to do with a simple tweak to its configuration. For example, say you want the "/qdemo" URL to be the location of the Quixote demo. If you're using Apache with the rewrite engine loaded and enabled, all you need to do is add this to your httpd.conf:: RewriteRule ^/qdemo(/.*) /www/cgi-bin/demo.cgi$1 [last] With this rule in effect (don't forget to restart your server!), accesses to "/qdemo/" are the same as accesses to "/cgi-bin/demo.cgi/" -- except they're a lot easier for the user to understand and don't expose implementation details of your application. Try it out. In your web browser, visit ``http://localhost/qdemo/``. You should get exactly the same page as you got visiting "/cgi-bin/demo.cgi/" earlier, and all the links should work exactly the same. You can use any URL prefix you like -- there's nothing special about "/qdemo". One small but important detail here is "/qdemo" versus "/qdemo/". In the above configuration, requests for "/qdemo" will fail, and requests for "/qdemo/" will succeed. See the "URL rewriting" section of web-server.txt for details and how to fix this. Understanding the demo ---------------------- Now that you've gotten the demo to run successfully, let's look under the hood and see how it works. Before we start following links in the demo (don't worry if you already have, you can't hurt anything), make sure you're watching all the relevant log files. As with any web application, log files are essential for debugging Quixote applications. Assuming that your web server's error log is in /www/log/error_log, and that you haven't changed the DEBUG_LOG and ERROR_LOG settings in demo.conf:: $ tail -f /www/log/error_log & \ tail -f /tmp/quixote-demo-debug.log & \ tail -f /tmp/quixote-demo-error.log (Note that recent versions of GNU tail let you tail multiple files with the same command. Cool!) Lesson 1: the top page ---------------------- Reload the top of the demo, presumably ``http://localhost/qdemo/``. You should see "debug message from the index page" in the debug log file. Where is this message coming from? To find out, we need to delve into the source code for the demo. Load up demo/__init__.py and let's take a look. In the process, we'll learn how to explore a Quixote application and find the source code that corresponds to a given URL. First, why are we loading demo/__init__.py? Because that's where some of the names in the "quixote.demo" namespace are defined, and it's where the list of names that may be "exported" by Quixote from this namespace to the web is given. Recall that under Quixote, every URL boils down to a callable Python object -- usually a function or method. The root of this application is a Python package ("quixote.demo"), which is just a special kind of module. But modules aren't callable -- so what does the "/qdemo/" URL boil down to? That's what ``_q_index()`` is for -- you can define a special function that is called by default when Quixote resolves a URL to a namespace rather than a callable. That is, "/qdemo/" resolves to the "quixote.demo" package; a package is a namespace, so it can't be called; therefore Quixote looks for a function called ``_q_index()`` in that namespace and calls it. In this case, ``_q_index()`` is not defined in demo/__init__.py -- but it is imported there from the quixote.demo.pages module. This is actually a PTL module -- demo/pages.py does not exist, but demo/pages.ptl does. So load it up and take a look:: def _q_index [plain] (request): print "debug message from the index page" """ Quixote Demo

Hello, world!

[...] """ A-ha! There's the PTL code that generates the "Quixote Demo" page. This ``_q_index()`` template is quite simple PTL -- it's mostly an HTML document with a single debug print thrown in to demonstrate Quixote's debug logging facility. Outcome of lesson 1: * a URL maps to either a namespace (package, module, class instance) or a callable (function, method, PTL template) * if a URL maps to a namespace, Quixote looks for a callable ``_q_index()`` in that namespace and calls it * ``_q_index()`` doesn't have to be explicitly exported by your namespace; if it exists, it will be used * anything your application prints to standard output goes to Quixote's debug log. (If you didn't specify a debug log in your config file, debug messages are discarded.) Lesson 2: a link to a simple document ------------------------------------- The first two links in the "Quixote Demo" page are quite simple. Each one is handled by a Python function defined in the "quixote.demo" namespace, i.e. in demo/__init__.py. For example, following the "simple" link is equivalent to calling the ``simple()`` function in "quixote.demo". Let's take a look at that function:: def simple (request): request.response.set_content_type("text/plain") return "This is the Python function 'quixote.demo.simple'.\n" Note that this could equivalently be coded in PTL:: def simple [plain] (request): request.response.set_content_type("text/plain") "This is the Python function 'quixote.demo.simple'.\n" ...but for such a simple document, why bother? Since this function doesn't generate an HTML document, it would be misleading for the HTTP response that Quixote generates to claim a "Content-type" of "text/html". That is the default for Quixote's HTTP responses, however, since most HTTP responses are indeed HTML documents. Therefore, if the content you're returning is anything other than an HTML document, you should set the "Content-type" header on the HTTP response. This brings up a larger issue: request and response objects. Quixote includes two classes, HTTPRequest and HTTPResponse, to encapsulate every HTTP request and its accompanying response. Whenever Quixote resolves a URL to a callable and calls it, it passes precisely one argument: an HTTPRequest object. The HTTPRequest object includes (almost) everything you might want to know about the HTTP request that caused Quixote to be invoked and to call a particular function, method, or PTL template. You have access to CGI environment variables, HTML form variables (parsed and ready-to-use), and HTTP cookies. Finally, the HTTPRequest object also includes an HTTPResponse object -- after all, every request implies a response. You can set the response status, set response headers, set cookies, or force a redirect using the HTTPResponse object. Note that it's not enough that the ``simple()`` function merely exists. If that were the case, then overly-curious users or attackers could craft URLs that point to any Python function in any module under your application's root namespace, potentially causing all sorts of havoc. You need to explicitly declare which names are exported from your application to the web, using the ``_q_exports`` variable. For example, demo/__init__.py has this export list:: _q_exports = ["simple", "error"] This means that only these two names are explicitly exported by the Quixote demo. (The empty string is implicitly exported from a namespace if a ``_q_index()`` callable exists there -- thus "/qdemo/" is handled by ``_q_index()`` in the "quixote.demo" namespace. Arbitrary names may be implicitly exported using a ``_q_lookup()`` function; see Lesson 4 below.) Lesson 3: error-handling ------------------------ The next link in the "Quixote Demo" page is to the "error" document, which is handled by the ``error()`` function in demo/__init__.py. All this function does is raise an exception:: def error (request): raise ValueError, "this is a Python exception" Follow the link, and you should see a Python traceback followed by a dump of the CGI environment for this request (along with other request data, such as a list of cookies). This is extremely useful when developing, testing, and debugging. In a production environment, though, it reveals way too much about your implementation to hapless users who should happen to hit an error, and it also reveals internal details to attackers who might use it to crack your site. (It's just as easy to write an insecure web application with Quixote as with any other tool.) Thus, Quixote offers the ``DISPLAY_EXCEPTIONS`` config variable. This is false by default, but the demo.conf file enables it. To see what happens with ``DISPLAY_EXCEPTIONS`` off, edit demo.conf and reload the "error" page. You should see a bland, generic error message that reveals very little about your implementation. (This error page is deliberately very similar, but not identical, to Apache's "Internal Server Error" page.) Unhandled exceptions raised by application code (aka "application bugs") are only one kind of error you're likely to encounter when developing a Quixote application. The other ones are: * driver script crashes or doesn't run (eg. can't import quixote modules, can't load config file). This is covered under "Running the demo directly" above * publishing errors, such as a request for "/simpel" that should have been "/simple", or a request for a resource that exists but is denied to the current user. Quixote has a family of exception classes for dealing with these; such exceptions may be raised by Quixote itself or by your application. They are usually handled by Quixote and turned into HTTP error responses (4xx status code), but it's possible for your application to define a special handler for such exceptions. * bugs in Quixote itself; hopefully this won't happen, but you never know. These usually look a lot like problems in the driver script: the script crashes and prints a traceback to stderr, which most likely winds up in your web server's error log. The length of the traceback is generally a clue as to whether there's a problem with your driver script or a bug in Quixote. Publishing errors result in a 4xx HTTP response code, and can be entirely handled by Quixote -- that is, your web server just returns the HTTP response that Quixote prepares. It's also possible to write a ``_q_exception_handler()`` method that will be called on triggering a publishing error. This method can then provide a friendlier response; for example, the page might provide a link to the site map, or the method might look at the problematic path and try to correct misspellings. The demo defines a ``_q_exception_handler()`` in demo/pages.ptl. Application bugs result in a 5xx HTTP response code, and are entirely handled by Quixote. Don't get confused by the fact that Quixote's and Apache's "Internal Server Error" pages are quite similar! Driver script crashes and Quixote bugs (which are essentially the same thing; the main difference is who to blame) are handled by your web server. (In the first case, Quixote doesn't even enter into it; in the second case, Quixote dies horribly and is no longer in control.) Under Apache, the Python traceback resulting from the crash is written to Apache's error log, and a 5xx response is returned to the client with Apache's "Internal Server Error" error page. Lesson 4: object publishing --------------------------- Publishing Python callables on the web -- i.e., translating URLs to Python functions/methods/PTL templates and calling them to determine the HTTP response -- is a very powerful way of writing web applications. However, Quixote has one more trick up its sleeve: object publishing. You can translate arbitrary names to arbitrary objects which are then published on the web, and you can create URLs that call methods on those objects. This is all accomplished with the ``_q_lookup()`` function. Every namespace that Quixote encounters may have a ``_q_lookup()``, just like it may have a ``_q_index()``. ``_q_index()`` is used to handle requests for the empty name -- as we saw in Lesson 1, a request for "/qdemo/", maps to the "quixote.demo" namespace; the empty string after the last slash means that Quixote will call ``_q_index()`` in this namespace to handle the request. ``_q_lookup()`` is for requests that aren't handled by a Python callable in the namespace. As seen in Lessons 2 and 3, requests for "/qdemo/simple" and "/qdemo/error" are handled by the ``simple()`` and ``error()`` functions in the "quixote.demo" namespace. What if someone requests "/qdemo/foo"? There's no function ``foo()`` in the "quixote.demo" namespace, so normally this would be an error. (Specifically, it would be a publishing error: Quixote would raise TraversalError, which is the error used for non-existent or non-exported names. Another part of Quixote then turns this into an HTTP 404 response.) However, this particular namespace also defines a ``_q_lookup()`` function. That means that the application wants a chance to handle unknown names before Quixote gives up entirely. Let's take a look at the implementation of ``_q_lookup()``:: from quixote.demo.integer_ui import IntegerUI [...] def _q_lookup(request, component): return IntegerUI(request, component) Pretty simple: we just construct an IntegerUI object and return it. So what is IntegerUI? Take a look in the demo/integer_ui.py file to see; it's just a web interface to integers. (Normally, you would write a wrapper class that provides a web interface to something more interesting than integers. This just demonstrates how simple an object published by Quixote can be.) So, what is an IntegerUI object? From Quixote's point of view, it's just another namespace to publish: like modules and packages, class instances have attributes, some of which (methods) are callable. In the case of IntegerUI, two of those attributes are ``_q_exports`` and ``_q_index`` -- every namespace published by Quixote must have an export list, and an index function is almost always advisable. What this means is that any name that the IntegerUI constructor accepts is a valid name to tack onto the "/qdemo/" URL. Take a look at the IntegerUI constructor; you'll see that it works fine when passed something that can be converted to an integer (eg. "12" or 1.0), and raises Quixote's TraversalError if not. As it happens, Quixote always passes in a string -- URLs are just strings, after all -- so we only have to worry about things like "12" or "foo". The error case is actually easier to understand, so try to access ``http://localhost/qdemo/foo/``. You should get an error page that complains about an "invalid literal for int()". Now let's build a real IntegerUI object and see the results. Follow the third link in the "Quixote Demo" page, or just go to ``http://localhost/qdemo/12/``. You should see a web page titled "The Number 12". This web page is generated by the ``_q_index()`` method of IntegerUI: after all, you've selected a namespace (the IntegerUI object corresponding to the number 12) with no explicit callable, so Quixote falls back on the ``_q_index()`` attribute of that namespace. IntegerUI only exports one interesting method, ``factorial()``. You can call this method by following the "factorial" link, or just by accessing ``http://localhost/qdemo/12/factorial``. Remember how I said the URL is part of the user interface? Here's a great example: edit the current URL to point to a different integer. A fun one to try is 2147483646. If you follow the "next" link, you'll get an OverflowError traceback (unless you're using a 64-bit Python!), because the web page for 2147483647 attempts to generate its own "next" link to the web page for 2147483648 -- but that fails because current versions of Python on 32-bit platforms can't handle regular integers larger than 2147483647. Now go back to the page for 2147483646 and hit the "factorial" link. Run "top" on the web server. Get yourself a coffee. Await the heat death of the universe. (Actually, your browser will probably timeout first.) This doesn't overflow, because the factorial() function uses Python long integers, which can handle any integer -- they just take a while to get there. However, it illustrates another interesting vulnerability: an attacker could use this to launch a denial-of-service attack on the server running the Quixote demo. (Hey, it's just a demo!) Rather than fix the DoS vulnerability, I decided to use it to illustrate another Quixote feature: if you write to stderr, the message winds up in the Quixote error log for this application (/tmp/quixote-demo-error.log by default). The IntegerUI.factorial() method uses this to log a warning of an apparent denial-of-service attack:: def factorial (self, request): if self.n > 10000: sys.stderr.write("warning: possible denial-of-service attack " "(request for factorial(%d))\n" % self.n) return "%d! = %d" % (self.n, fact(self.n)) Since the Quixote error log is where application tracebacks are recorded, you should be watching this log file regularly, so you would presumably notice these messages. In real life, you'd probably just deny such a ludicrous request. You could do this by raising a Quixote publishing error. For example:: def factorial (self, request): from quixote.errors import AccessError if self.n > 10000: raise AccessError("ridiculous request denied") return "%d! = %d" % (self.n, fact(self.n)) Lesson 5: widgets ----------------- You can't get very far writing web applications without writing forms, and the building blocks of web forms are generally called "form elements": string input, checkboxes, radiobuttons, select lists, and so forth. Quixote provides an abstraction for all of these form elements: the Widget class hierarchy. The widget classes are explained in detail in widget.txt; I'm going to give a brief description of the "Quixote Widget Demo" page and the code behind it here. If you follow the "widgets" link from the main page, you'll see a fairly ordinary-looking web form -- the sort of thing you might have to fill out to order a pizza on-line, with the oddity that this pizza shop is asking for your eye colour. (Hey, I had to demonstrate radiobuttons somehow!) This form demonstrates most of HTML's basic form capabilities: a simple string, a password, a checkbox, a set of radiobuttons, a single-select list, and a multiple-select list. Whenever you implement a web form, there are two things you have to worry about: generating the form elements and processing the client-submitted form data. There are as many ways of dividing up this work as there are web programmers. (Possibly more: every time I tackle this problem, I seem to come up with a different way of solving it.) The form in the Quixote widget demo is implemented in three parts, all of them in demo/widgets.ptl: * ``widgets()`` is the callable that handles the "/qdemo/widgets" URL. This template creates all the widget objects needed for the form and then calls either ``render_widgets()`` or ``process_widgets()`` as appropriate. * ``render_widgets()`` is called by ``widgets()`` when there is no form data to process, eg. on the first access to the "/qdemo/widgets" URL. It generates the form elements and returns an HTML document consisting of a table that lays them out in an attractive form. * ``process_widgets()`` is called by ``widgets()`` when there is form data to process, ie. when the form generated by ``render_widgets()`` is submitted by the user. It processes the form data, ie. it looks up the user-submitted form values and returns an HTML document listing those values. This division of labour works well with Quixote's widget classes, since you need a collection of Widget objects whether you are generating the form elements or processing form data. For generating the form, we (1) create all the widget objects, and (2) generate an HTML document that includes the output of each widget object's ``render()`` method. (Laying out the form is the responsibility of render_widgets(), which is why it's littered with table tags.) For processing the form, we (1) create all the widget objects, and (2) generate an HTML document incorporating the user-submitted form values. In both cases, step (1) is handled by the widgets() template, which then calls either render_widgets() or process_widgets() for step (2). Thus, there are three things you have to understand about widget objects: how to create them, how to render them, and how to use them to parse form values. Widget creation is the only step that's very interesting, since each widget class has different constructor arguments. For example, here's how we create the "name" widget in the pizza shop form:: widgets['name'] = widget.StringWidget('name', size=20) When rendered, this widget will produce the following HTML:: A more complex example is the "pizza size" widget:: widgets['size'] = widget.SingleSelectWidget( 'size', value='medium', allowed_values=['tiny', 'small', 'medium', 'large', 'enormous'], descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")', 'Large (14")', 'Enormous (18")'], size=5) which will generate the following HTML when rendered:: Some things you might need to know about widget creation: * the standard widget classes are in the quixote.form.widget module; see widget.txt and/or the source code for the complete list * every widget class constructor has exactly one required argument: the widget name. This is used as the form element name in the generated HTML. (Things are a bit different for compound widgets, but I'm not covering them in this document.) * every widget class supports a number of keyword arguments that generally correspond to attributes of some HTML tag. The one argument common to all widget classes is ``value``, the current value for this widget. Rendering widgets is easy: just call the render() method, passing in the current HTTPRequest object. (It's currently not used by the standard widget classes, but could be used by derived or compound widget classes for context-sensitive widget rendering.) Parsing form values is just as easy: call the parse() method, again passing in the current HTTPRequest object. The return value depends on the nature of the widget, eg.: * StringWidget and PasswordWidget return a string * CheckboxWidget returns a boolean * RadiobuttonsWidget, SingleSelectWidget, and MultipleSelectWidget return one of the values supplied in ``allowed_values`` -- in the demo these are all strings, but they can be any Python value. (If the client submits bogus data, the widget will return None.) $Id: demo.txt 21292 2003-04-08 16:48:47Z akuchlin $ Quixote-1.2/doc/form2conversion.html0000664000225700100400000004105510131012101020324 0ustar naschememems00000000000000 Converting form1 forms to use the form2 library

Converting form1 forms to use the form2 library

Introduction

These are some notes and examples for converting Quixote form1 forms, that is forms derived from quixote.form.Form, to the newer form2 forms.

Form2 forms are more flexible than their form1 counterparts in that they do not require you to use the Form class as a base to get form functionality as form1 forms did. Form2 forms can be instantiated directly and then manipulated as instances. You may also continue to use inheritance for your form2 classes to get form functionality, particularly if the structured separation of process, render, and action is desirable.

There are many ways to get from form1 code ported to form2. At one end of the spectrum is to rewrite the form class using a functional programing style. This method is arguably best since the functional style makes the flow of control clearer.

The other end of the spectrum and normally the easiest way to port form1 forms to form2 is to use the compatibility module provided in the form2 package. The compatibility module's Form class provides much of the same highly structured machinery (via a handle master method) that the form1 framework uses.

Converting form1 forms using using the compatibility module

Here's the short list of things to do to convert form1 forms to form2 using compatibility.

  1. Import the Form base class from quixote.form2.compatibility rather than from quixote.form.

  2. Getting and setting errors is slightly different. In your form's process method, where errors are typically set, form2 has a new interface for marking a widget as having an error.

    Form1 API:

    self.error['widget_name'] = 'the error message'
    

    Form2 API:

    self.set_error('widget_name', 'the error message') 
    

    If you want to find out if the form already has errors, change the form1 style of direct references to the self.errors dictionary to a call to the has_errors method.

    Form1 API:

    if not self.error:
       do some more error checking...
    

    Form2 API:

    if not self.has_errors():
       do some more error checking...
    
  3. Form2 select widgets no longer take allowed_values or descriptions arguments. If you are adding type of form2 select widget, you must provide the options argument instead. Options are the way you define the list of things that are selectable and what is returned when they are selected. the options list can be specified in in one of three ways:

        options: [objects:any]
    or
        options: [(object:any, description:any)]
    or
        options: [(object:any, description:any, key:any)]
    

    An easy way to construct options if you already have allowed_values and descriptions is to use the built-in function zip to define options:

    options=zip(allowed_values, descriptions)
    
Note, however, that often it is simpler to to construct the options list directly.
  1. You almost certainly want to include some kind of cascading style sheet (since form2 forms render with minimal markup). There is a basic set of CSS rules in quixote.form2.css.

Here's the longer list of things you may need to tweak in order for form2 compatibility forms to work with your form1 code.

  • widget_type widget class attribute is gone. This means when adding widgets other than widgets defined in quixote.form2.widget, you must import the widget class into your module and pass the widget class as the first argument to the add_widget method rather than using the widget_type string.

  • The action_url argument to the form's render method is now a keyword argument.

  • If you use OptionSelectWidget, there is no longer a get_current_option method. You can get the current value in the normal way.

  • ListWidget has been renamed to WidgetList.

  • There is no longer a CollapsibleListWidget class. If you need this functionality, consider writing a 'deletable composite widget' to wrap your WidgetList widgets in it:

    class DeletableWidget(CompositeWidget):
    
      def __init__(self, name, value=None,
                   element_type=StringWidget,
                   element_kwargs={}, **kwargs):
         CompositeWidget.__init__(self, name, value=value, **kwargs)
         self.add(HiddenWidget, 'deleted', value='0')
           if self.get('deleted') != '1':
              self.add(element_type, 'element', value=value,
                       **element_kwargs)
              self.add(SubmitWidget, 'delete', value='Delete')
              if self.get('delete'):
                 self.get_widget('deleted').set_value('1')
    
      def _parse(self, request):
         if self.get('deleted') == '1':
            self.value = None
         else:
            self.value = self.get('element')
    
      def render(self):
         if self.get('deleted') == '1':
            return self.get_widget('deleted').render()
         else:
            return CompositeWidget.render(self)
    

Congratulations, now that you've gotten your form1 forms working in form2, you may wish to simplify this code using some of the new features available in form2 forms. Here's a list of things you may wish to consider:

  • In your process method, you don't really need to get a form_data dictionary by calling Form.process to ensure your widgets are parsed. Instead, the parsed value of any widget is easy to obtain using the widget's get_value method or the form's __getitem__ method. So, instead of:

    form_data = Form.process(self, request)
    val = form_data['my_widget']
    

    You can use:

    val = self['my_widget']
    

    If the widget may or may not be in the form, you can use get:

    val = self.get('my_widget')
    
  • It's normally not necessary to provide the action_url argument to the form's render method.

  • You don't need to save references to your widgets in your form class. You may have a particular reason for wanting to do that, but any widget added to the form using add (or add_widget in the compatibility module) can be retrieved using the form's get_widget method.

Converting form1 forms to form2 by functional rewrite

The best way to get started on a functional version of a form2 rewrite is to look at a trivial example form first written using the form1 inheritance model followed by it's form2 functional equivalent.

First the form1 form:

class MyForm1Form(Form):
    def __init__(self, request, obj):
        Form.__init__(self)

        if obj is None:
            self.obj = Obj()
            self.add_submit_button('add', 'Add')
        else:
            self.obj = obj
            self.add_submit_button('update', 'Update')

        self.add_cancel_button('Cancel', request.get_path(1) + '/')

        self.add_widget('single_select', 'obj_type',
                        title='Object Type',
                        value=self.obj.get_type(),
                        allowed_values=list(obj.VALID_TYPES),
                        descriptions=['type1', 'type2', 'type3'])
        self.add_widget('float', 'cost',
                        title='Cost',
                        value=obj.get_cost())

    def render [html] (self, request, action_url):
        title = 'Obj %s: Edit Object' % self.obj
        header(title)
        Form.render(self, request, action_url)
        footer(title)

    def process(self, request):
        form_data = Form.process(self, request)

        if not self.error:
            if form_data['cost'] is None:
                self.error['cost'] = 'A cost is required.'
            elif form_data['cost'] < 0:
                self.error['cost'] = 'The amount must be positive'
        return form_data

    def action(self, request, submit, form_data):
        self.obj.set_type(form_data['obj_type'])
        self.obj.set_cost(form_data['cost'])
        if submit == 'add':
            db = get_database()
            db.add(self.obj)
        else:
            assert submit == 'update'
        return request.redirect(request.get_path(1) + '/')

Here's the same form using form2 where the function operates on a Form instance it keeps a reference to it as a local variable:

def obj_form(request, obj):
    form = Form() # quixote.form2.Form
    if obj is None:
        obj = Obj()
        form.add_submit('add', 'Add')
    else:
        form.add_submit('update', 'Update')
    form.add_submit('cancel', 'Cancel')

    form.add_single_select('obj_type',
                           title='Object Type',
                           value=obj.get_type(),
                           options=zip(obj.VALID_TYPES, 
                                       ['type1', 'type2', 'type3']))
    form.add_float('cost',
                   title='Cost',
                   value=obj.get_cost(),
                   required=1)

    def render [html] ():
        title = 'Obj %s: Edit Object' % obj
        header(title)
        form.render()
        footer(title)

    def process():
       if form['cost'] < 0:
          self.set_error('cost', 'The amount must be positive')

    def action(submit):
      obj.set_type(form['obj_type'])
      obj.set_cost(form['cost'])
      if submit == 'add':
          db = get_database()
          db.add(self.obj)
      else:
          assert submit == 'update'

    exit_path = request.get_path(1) + '/'
    submit = form.get_submit()
    if submit == 'cancel':
        return request.redirect(exit_path)

    if not form.is_submitted() or form.has_errors():
        return render()
    process()
    if form.has_errors():
        return render()
   
    action(submit)
    return request.redirect(exit_path)

As you can see in the example, the function still has all of the same parts of it's form1 equivalent.

  1. It determines if it's to create a new object or edit an existing one
  2. It adds submit buttons and widgets
  3. It has a function that knows how to render the form
  4. It has a function that knows how to do error processing on the form
  5. It has a function that knows how to register permanent changes to objects when the form is submitted successfully.

In the form2 example, we have used inner functions to separate out these parts. This, of course, is optional, but it does help readability once the form gets more complicated and has the additional advantage of mapping directly with it's form1 counterparts.

Form2 functional forms do not have the handle master-method that is called after the form is initialized. Instead, we deal with this functionality manually. Here are some things that the handle portion of your form might need to implement illustrated in the order that often makes sense.

  1. Get the value of any submit buttons using form.get_submit
  2. If the form has not been submitted yet, return render().
  3. See if the cancel button was pressed, if so return a redirect.
  4. Call your process inner function to do any widget-level error checks. The form may already have set some errors, so you may wish to check for that before trying additional error checks.
  5. See if the form was submitted by an unknown submit button. This will be the case if the form was submitted via a JavaScript action, which is the case when an option select widget is selected. The value of get_submit is True in this case and if it is, you want to clear any errors and re-render the form.
  6. If the form has not been submitted or if the form has errors, you simply want to render the form.
  7. Check for your named submit buttons which you expect for successful form posting e.g. add or update. If one of these is pressed, call you action inner function.
  8. Finally, return a redirect to the expected page following a form submission.

These steps are illustrated by the following snippet of code and to a large degree in the above functional form2 code example. Often this handle block of code can be simplified. For example, if you do not expect form submissions from unregistered submit buttons, you can eliminate the test for that. Similarly, if your form does not do any widget-specific error checking, there's no reason to have an error checking process function or the call to it:

exit_path = request.get_path(1) + '/'
submit = form.get_submit()
if not submit:
    return render()
if submit == 'cancel':
    return request.redirect(exit_path)
if submit == True:
    form.clear_errors()
    return render()
process()
if form.has_errors():
    return render()
action(submit)
return request.redirect(exit_path)
Quixote-1.2/doc/form2conversion.txt0000664000225700100400000003327610053152342020224 0ustar naschememems00000000000000Converting form1 forms to use the form2 library =============================================== Introduction ------------ These are some notes and examples for converting Quixote form1 forms, that is forms derived from ``quixote.form.Form``, to the newer form2 forms. Form2 forms are more flexible than their form1 counterparts in that they do not require you to use the ``Form`` class as a base to get form functionality as form1 forms did. Form2 forms can be instantiated directly and then manipulated as instances. You may also continue to use inheritance for your form2 classes to get form functionality, particularly if the structured separation of ``process``, ``render``, and ``action`` is desirable. There are many ways to get from form1 code ported to form2. At one end of the spectrum is to rewrite the form class using a functional programing style. This method is arguably best since the functional style makes the flow of control clearer. The other end of the spectrum and normally the easiest way to port form1 forms to form2 is to use the ``compatibility`` module provided in the form2 package. The compatibility module's Form class provides much of the same highly structured machinery (via a ``handle`` master method) that the form1 framework uses. Converting form1 forms using using the compatibility module ----------------------------------------------------------- Here's the short list of things to do to convert form1 forms to form2 using compatibility. 1. Import the Form base class from ``quixote.form2.compatibility`` rather than from quixote.form. 2. Getting and setting errors is slightly different. In your form's process method, where errors are typically set, form2 has a new interface for marking a widget as having an error. Form1 API:: self.error['widget_name'] = 'the error message' Form2 API:: self.set_error('widget_name', 'the error message') If you want to find out if the form already has errors, change the form1 style of direct references to the ``self.errors`` dictionary to a call to the ``has_errors`` method. Form1 API:: if not self.error: do some more error checking... Form2 API:: if not self.has_errors(): do some more error checking... 3. Form2 select widgets no longer take ``allowed_values`` or ``descriptions`` arguments. If you are adding type of form2 select widget, you must provide the ``options`` argument instead. Options are the way you define the list of things that are selectable and what is returned when they are selected. the options list can be specified in in one of three ways:: options: [objects:any] or options: [(object:any, description:any)] or options: [(object:any, description:any, key:any)] An easy way to construct options if you already have allowed_values and descriptions is to use the built-in function ``zip`` to define options:: options=zip(allowed_values, descriptions) Note, however, that often it is simpler to to construct the ``options`` list directly. 4. You almost certainly want to include some kind of cascading style sheet (since form2 forms render with minimal markup). There is a basic set of CSS rules in ``quixote.form2.css``. Here's the longer list of things you may need to tweak in order for form2 compatibility forms to work with your form1 code. * ``widget_type`` widget class attribute is gone. This means when adding widgets other than widgets defined in ``quixote.form2.widget``, you must import the widget class into your module and pass the widget class as the first argument to the ``add_widget`` method rather than using the ``widget_type`` string. * The ``action_url`` argument to the form's render method is now a keyword argument. * If you use ``OptionSelectWidget``, there is no longer a ``get_current_option`` method. You can get the current value in the normal way. * ``ListWidget`` has been renamed to ``WidgetList``. * There is no longer a ``CollapsibleListWidget`` class. If you need this functionality, consider writing a 'deletable composite widget' to wrap your ``WidgetList`` widgets in it:: class DeletableWidget(CompositeWidget): def __init__(self, name, value=None, element_type=StringWidget, element_kwargs={}, **kwargs): CompositeWidget.__init__(self, name, value=value, **kwargs) self.add(HiddenWidget, 'deleted', value='0') if self.get('deleted') != '1': self.add(element_type, 'element', value=value, **element_kwargs) self.add(SubmitWidget, 'delete', value='Delete') if self.get('delete'): self.get_widget('deleted').set_value('1') def _parse(self, request): if self.get('deleted') == '1': self.value = None else: self.value = self.get('element') def render(self): if self.get('deleted') == '1': return self.get_widget('deleted').render() else: return CompositeWidget.render(self) Congratulations, now that you've gotten your form1 forms working in form2, you may wish to simplify this code using some of the new features available in form2 forms. Here's a list of things you may wish to consider: * In your process method, you don't really need to get a ``form_data`` dictionary by calling ``Form.process`` to ensure your widgets are parsed. Instead, the parsed value of any widget is easy to obtain using the widget's ``get_value`` method or the form's ``__getitem__`` method. So, instead of:: form_data = Form.process(self, request) val = form_data['my_widget'] You can use:: val = self['my_widget'] If the widget may or may not be in the form, you can use ``get``:: val = self.get('my_widget') * It's normally not necessary to provide the ``action_url`` argument to the form's ``render`` method. * You don't need to save references to your widgets in your form class. You may have a particular reason for wanting to do that, but any widget added to the form using ``add`` (or ``add_widget`` in the compatibility module) can be retrieved using the form's ``get_widget`` method. Converting form1 forms to form2 by functional rewrite ----------------------------------------------------- The best way to get started on a functional version of a form2 rewrite is to look at a trivial example form first written using the form1 inheritance model followed by it's form2 functional equivalent. First the form1 form:: class MyForm1Form(Form): def __init__(self, request, obj): Form.__init__(self) if obj is None: self.obj = Obj() self.add_submit_button('add', 'Add') else: self.obj = obj self.add_submit_button('update', 'Update') self.add_cancel_button('Cancel', request.get_path(1) + '/') self.add_widget('single_select', 'obj_type', title='Object Type', value=self.obj.get_type(), allowed_values=list(obj.VALID_TYPES), descriptions=['type1', 'type2', 'type3']) self.add_widget('float', 'cost', title='Cost', value=obj.get_cost()) def render [html] (self, request, action_url): title = 'Obj %s: Edit Object' % self.obj header(title) Form.render(self, request, action_url) footer(title) def process(self, request): form_data = Form.process(self, request) if not self.error: if form_data['cost'] is None: self.error['cost'] = 'A cost is required.' elif form_data['cost'] < 0: self.error['cost'] = 'The amount must be positive' return form_data def action(self, request, submit, form_data): self.obj.set_type(form_data['obj_type']) self.obj.set_cost(form_data['cost']) if submit == 'add': db = get_database() db.add(self.obj) else: assert submit == 'update' return request.redirect(request.get_path(1) + '/') Here's the same form using form2 where the function operates on a Form instance it keeps a reference to it as a local variable:: def obj_form(request, obj): form = Form() # quixote.form2.Form if obj is None: obj = Obj() form.add_submit('add', 'Add') else: form.add_submit('update', 'Update') form.add_submit('cancel', 'Cancel') form.add_single_select('obj_type', title='Object Type', value=obj.get_type(), options=zip(obj.VALID_TYPES, ['type1', 'type2', 'type3'])) form.add_float('cost', title='Cost', value=obj.get_cost(), required=1) def render [html] (): title = 'Obj %s: Edit Object' % obj header(title) form.render() footer(title) def process(): if form['cost'] < 0: self.set_error('cost', 'The amount must be positive') def action(submit): obj.set_type(form['obj_type']) obj.set_cost(form['cost']) if submit == 'add': db = get_database() db.add(self.obj) else: assert submit == 'update' exit_path = request.get_path(1) + '/' submit = form.get_submit() if submit == 'cancel': return request.redirect(exit_path) if not form.is_submitted() or form.has_errors(): return render() process() if form.has_errors(): return render() action(submit) return request.redirect(exit_path) As you can see in the example, the function still has all of the same parts of it's form1 equivalent. 1. It determines if it's to create a new object or edit an existing one 2. It adds submit buttons and widgets 3. It has a function that knows how to render the form 4. It has a function that knows how to do error processing on the form 5. It has a function that knows how to register permanent changes to objects when the form is submitted successfully. In the form2 example, we have used inner functions to separate out these parts. This, of course, is optional, but it does help readability once the form gets more complicated and has the additional advantage of mapping directly with it's form1 counterparts. Form2 functional forms do not have the ``handle`` master-method that is called after the form is initialized. Instead, we deal with this functionality manually. Here are some things that the ``handle`` portion of your form might need to implement illustrated in the order that often makes sense. 1. Get the value of any submit buttons using ``form.get_submit`` 2. If the form has not been submitted yet, return ``render()``. 3. See if the cancel button was pressed, if so return a redirect. 4. Call your ``process`` inner function to do any widget-level error checks. The form may already have set some errors, so you may wish to check for that before trying additional error checks. 5. See if the form was submitted by an unknown submit button. This will be the case if the form was submitted via a JavaScript action, which is the case when an option select widget is selected. The value of ``get_submit`` is ``True`` in this case and if it is, you want to clear any errors and re-render the form. 6. If the form has not been submitted or if the form has errors, you simply want to render the form. 7. Check for your named submit buttons which you expect for successful form posting e.g. ``add`` or ``update``. If one of these is pressed, call you action inner function. 8. Finally, return a redirect to the expected page following a form submission. These steps are illustrated by the following snippet of code and to a large degree in the above functional form2 code example. Often this ``handle`` block of code can be simplified. For example, if you do not expect form submissions from unregistered submit buttons, you can eliminate the test for that. Similarly, if your form does not do any widget-specific error checking, there's no reason to have an error checking ``process`` function or the call to it:: exit_path = request.get_path(1) + '/' submit = form.get_submit() if not submit: return render() if submit == 'cancel': return request.redirect(exit_path) if submit == True: form.clear_errors() return render() process() if form.has_errors(): return render() action(submit) return request.redirect(exit_path) Quixote-1.2/doc/multi-threaded.html0000664000225700100400000000357110131012101020102 0ustar naschememems00000000000000 Multi-Threaded Quixote Applications

Multi-Threaded Quixote Applications

Starting with Quixote 0.6, it's possible to write multi-threaded Quixote applications. In previous versions, Quixote stored the current HTTPRequest object in a global variable, meaning that processing multiple requests in the same process simultaneously was impossible.

However, the Publisher class as shipped still can't handle multiple simultaneous requests; you'll need to subclass Publisher to make it re-entrant. Here's a starting point:

import thread
from quixote.publish import Publisher

[...]

class ThreadedPublisher (Publisher):
    def __init__ (self, root_namespace, config=None):
        Publisher.__init__(self, root_namespace, config)
        self._request_dict = {}

    def _set_request(self, request):
        self._request_dict[thread.get_ident()] = request

    def _clear_request(self):
        try:
            del self._request_dict[thread.get_ident()]
        except KeyError:
            pass

    def get_request(self):
        return self._request_dict.get(thread.get_ident())

Using ThreadedPublisher, you now have one current request per thread, rather than one for the entire process.

$Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $

Quixote-1.2/doc/multi-threaded.txt0000664000225700100400000000241207611615551020001 0ustar naschememems00000000000000Multi-Threaded Quixote Applications =================================== Starting with Quixote 0.6, it's possible to write multi-threaded Quixote applications. In previous versions, Quixote stored the current HTTPRequest object in a global variable, meaning that processing multiple requests in the same process simultaneously was impossible. However, the Publisher class as shipped still can't handle multiple simultaneous requests; you'll need to subclass Publisher to make it re-entrant. Here's a starting point:: import thread from quixote.publish import Publisher [...] class ThreadedPublisher (Publisher): def __init__ (self, root_namespace, config=None): Publisher.__init__(self, root_namespace, config) self._request_dict = {} def _set_request(self, request): self._request_dict[thread.get_ident()] = request def _clear_request(self): try: del self._request_dict[thread.get_ident()] except KeyError: pass def get_request(self): return self._request_dict.get(thread.get_ident()) Using ThreadedPublisher, you now have one current request per thread, rather than one for the entire process. $Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $ Quixote-1.2/doc/programming.html0000664000225700100400000006227510131012102017523 0ustar naschememems00000000000000 Quixote Programming Overview

Quixote Programming Overview

This document explains how a Quixote application is structured. Be sure you have read the "Understanding the demo" section of demo.txt first -- this explains a lot of Quixote fundamentals.

There are three components to a Quixote application:

  1. A driver script, usually a CGI or FastCGI script. This is the interface between your web server (eg., Apache) and the bulk of your application code.

    The driver script is responsible for creating a Quixote publisher customized for your application and invoking its publishing loop.

  2. A configuration file. This file specifies various features of the Publisher class, such as how errors are handled, the paths of various log files, and various other things. Read through quixote/config.py for the full list of configuration settings.

    The most important configuration parameters are:

    ERROR_EMAIL

    e-mail address to which errors will be mailed

    ERROR_LOG

    file to which errors will be logged

    For development/debugging, you should also set DISPLAY_EXCEPTIONS true and SECURE_ERRORS false; the defaults are the reverse, to favour security over convenience.

  3. Finally, the bulk of the code will be in a Python package or module, called the root namespace. The Quixote publisher will be set up to start traversing at the root namespace.

Driver script

The driver script is the interface between your web server and Quixote's "publishing loop", which in turn is the gateway to your application code. Thus, there are two things that your Quixote driver script must do:

  • create a Quixote publisher -- that is, an instance of the Publisher class provided by the quixote.publish module -- and customize it for your application
  • invoke the Quixote publishing loop by calling the 'publish_cgi()' method of the publisher

The publisher is responsible for translating URLs to Python objects and calling the appropriate function, method, or PTL template to retrieve the information and/or carry out the action requested by the URL.

The most important application-specific customization done by the driver script is to set the root namespace of your application. Broadly speaking, a namespace is any Python object with attributes. The most common namespaces are modules, packages, and class instances. The root namespace of a Quixote application is usually a Python package, although for a small application it could be a regular module.

The driver script can be very simple; for example, here is a trimmed-down version of demo.cgi, the driver script for the Quixote demo:

from quixote import enable_ptl, Publisher
enable_ptl()
app = Publisher("quixote.demo")
app.setup_logs()
app.publish_cgi()

(Whether you install this as demo.cgi, demo.fcgi, demo.py, or whatever is up to you and your web server.)

That's almost the simplest possible case -- there's no application-specific configuration info apart from the root namespace. (The only way to make this simpler would be to remove the enable_ptl() and setup_logs() calls. The former would remove the ability to import PTL modules, which is at least half the fun with Quixote; the latter would disable Quixote's debug and error logging, which is very useful.)

Here's a slightly more elaborate example, for a hypothetical database of books:

from quixote import enable_ptl, Publisher
from quixote.config import Config

# Install the PTL import hook, so we can use PTL modules in this app
enable_ptl()

# Create a Publisher instance with the default configuration.
pub = Publisher('books')

# Read a config file to override some default values.
pub.read_config('/www/conf/books.conf')

# Setup error and debug logging (do this after read_config(), so
# the settings in /www/conf/books.conf have an effect!).
pub.setup_logs()

# Enter the publishing main loop
pub.publish_cgi()

The application code is kept in a package named simply 'books' in this example, so its name is provided as the root namespace when creating the Publisher instance.

The SessionPublisher class in quixote.publish can also be used; it provides session tracking. The changes required to use SessionPublisher would be:

...
from quixote.publish import SessionPublisher
...
pub = SessionPublisher(PACKAGE_NAME)
...

For details on session management, see session-mgmt.txt.

Getting the driver script to actually run is between you and your web server. See the web-server.txt document for help, especially with Apache (which is the only web server we currently know anything about).

Configuration file

In the books.cgi driver script, configuration information is read from a file by this line:

pub.read_config('/www/conf/books.conf')

You should never edit the default values in quixote/config.py, because your edits will be lost if you upgrade to a newer Quixote version. You should certainly read it, though, to understand what all the configuration variables are.

The configuration file contains Python code, which is then evaluated using Python's built-in function execfile(). Since it's Python code, it's easy to set config variables:

ACCESS_LOG = "/www/log/access/books.log" 
DEBUG_LOG = "/www/log/books-debug.log"
ERROR_LOG = "/www/log/books-error.log"

You can also execute arbitrary Python code to figure out what the variables should be. The following example changes some settings to be more convenient for a developer when the WEB_MODE environment variable is the string DEVEL:

web_mode = os.environ["WEB_MODE"]
if web_mode == "DEVEL":
    DISPLAY_EXCEPTIONS = 1
    SECURE_ERRORS = 0
    RUN_ONCE = 1
elif web_mode in ("STAGING", "LIVE"):
    DISPLAY_EXCEPTIONS = 0
    SECURE_ERRORS = 1
    RUN_ONCE = 0
else:
    raise RuntimeError, "unknown server mode: %s" % web_mode

At the MEMS Exchange, we use this flexibility to display tracebacks in DEVEL mode, to redirect generated e-mails to a staging address in STAGING mode, and to enable all features in LIVE mode.

Logging

Every Quixote application can have up to three log files, each of which is selected by a different configuration variable:

  • access log (ACCESS_LOG)
  • error log (ERROR_LOG)
  • debug log (DEBUG_LOG)

If you want logging to work, you must call setup_logs() on your Publisher object after creating it and reading any application-specific config file. (This only applies for CGI/FastCGI driver scripts, where you are responsible for creating the Publisher object. With mod_python under Apache, it's taken care of for you.)

Quixote writes one (rather long) line to the access log for each request it handles; we have split that line up here to make it easier to read:

127.0.0.1 - 2001-10-15 09:48:43
  2504 "GET /catalog/ HTTP/1.1"
  200 'Opera/6.0 (Linux; U)' 0.10sec

This line consists of:

  • client IP address
  • current user (according to Quixote session management mechanism, so this will be "-" unless you're using a session manager that does authentication)
  • date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss
  • process ID of the process serving the request (eg. your CGI/FastCGI driver script)
  • the HTTP request line (request method, URI, and protocol)
  • response status code
  • HTTP user agent string (specifically, this is repr(os.environ.get('HTTP_USER_AGENT', '')))
  • time to complete the request

If no access log is configured (ie., ACCESS_LOG is None), then Quixote will not do any access logging.

The error log is used for two purposes:

  • all application output to standard error (sys.stderr) goes to Quixote's error log
  • all application tracebacks will be written to Quixote's error log

If no error log is configured (with ERROR_LOG), then both types of messages will be written to the stderr supplied to Quixote for this request by your web server. At least for CGI/FastCGI scripts under Apache, this winds up in Apache's error log.

The debug log is where any application output to stdout goes. Thus, you can just sprinkle print statements into your application for debugging; if you have configured a debug log, those print statements will wind up there. If you don't configure a debug log, they go to the bit bucket (/dev/null on Unix, NUL on Windows).

Application code

Finally, we reach the most complicated part of a Quixote application. However, thanks to Quixote's design, everything you've ever learned about designing and writing Python code is applicable, so there are no new hoops to jump through. The only new language to learn is PTL, which is simply Python with a novel way of generating function return values -- see PTL.txt for details.

An application's code lives in a Python package that contains both .py and .ptl files. Complicated logic should be in .py files, while .ptl files, ideally, should contain only the logic needed to render your Web interface and basic objects as HTML. As long as your driver script calls enable_ptl(), you can import PTL modules (.ptl files) just as if they were Python modules.

Quixote's publisher will start at the root of this package, and will treat the rest of the URL as a path into the package's contents. Here are some examples, assuming that the URL_PREFIX is "/q", your web server is setup to rewrite /q requests as calls to (eg.) /www/cgi-bin/books.cgi, and the root package for your application is 'books':

http://.../q/         call         books._q_index()
http://.../q/other    call         books.other(), if books.other
                                   is callable (eg. a function or
                                   method)
http://.../q/other    redirect to  /q/other/, if books.other is a
                                   namespace (eg. a module or sub-package)
http://.../q/other/   call         books.other._q_index(), if books.other
                                   is a namespace

One of Quixote's design principles is "Be explicit." Therefore there's no complicated rule for remembering which functions in a module are public; you just have to list them all in the _q_exports variable, which should be a list of strings naming the public functions. You don't need to list the _q_index() function as being public; that's assumed. Eg. if foo() is a function to be exported (via Quixote to the web) from your application's namespace, you should have this somewhere in that namespace (ie. at module level in a module or __init__.py file):

_q_exports = ['foo']

At times it is desirable for URLs to contain path components that are not valid Python identifiers. In these cases you can provide an explicit external to internal name mapping. For example:

_q_exports = ['foo', ('stylesheet.css', 'stylesheet_css')]

When a function is callable from the web, it must expect a single parameter, which will be an instance of the HTTPRequest class. This object contains everything Quixote could discover about the current HTTP request -- CGI environment variables, form data, cookies, etc. When using SessionPublisher, request.session is a Session object for the user agent making the request.

The function should return a string; all PTL templates return a string automatically. request.response is an HTTPResponse instance, which has methods for setting the content-type of the function's output, generating an HTTP redirect, specifying arbitrary HTTP response headers, and other common tasks. (Actually, the request object also has a method for generating a redirect. It's usually better to use this -- ie. code request.redirect(...) because generating a redirect correctly requires knowledge of the request, and only the request object has that knowledge. request.response.redirect(...) only works if you supply an absolute URL, eg. "http://www.example.com/foo/bar".)

Use

pydoc quixote.http_request
pydoc quixote.http_response

to view the documentation for the HTTPRequest and HTTPResponse classes, or consult the source code for all the gory details.

There are a few special functions that affect Quixote's traversal of a URL to determine how to handle it: _q_access(), _q_lookup(), and _q_resolve().

_q_access(request)

If this function is present in a module, it will be called before attempting to traverse any further. It can look at the contents of request and decide if the traversal can continue; if not, it should raise quixote.errors.AccessError (or a subclass), and Quixote will return a 403 ("forbidden") HTTP status code. The return value is ignored if _q_access() doesn't raise an exception.

For example, in the MEMS Exchange code, we have some sets of pages that are only accessible to signed-in users of a certain type. The _q_access() function looks like this:

def _q_access (request):
    if request.session.user is None:
        raise NotLoggedInError("You must be signed in.")
    if not (request.session.user.is_admin() or
            request.session.user.is_fab()):
        raise AccessError("You don't have access to the reports page.")

This is less error-prone than having to remember to add checks to every single public function.

_q_lookup(request, name)

This function translates an arbitrary string into an object that we continue traversing. This is very handy; it lets you put user-space objects into your URL-space, eliminating the need for digging ID strings out of a query, or checking PATH_INFO after Quixote's done with it. But it is a compromise with security: it opens up the traversal algorithm to arbitrary names not listed in _q_exports. (_q_lookup() is never called for names listed in _q_exports.) You should therefore be extremely paranoid about checking the value of name.

request is the request object, as it is everywhere else; name is a string containing the next component of the path. _q_lookup() should return either a string (a complete document that will be returned to the client) or some object that can be traversed further. Returning a string is useful in simple cases, eg. if you want the /user/joe URI to show everything about user "joe" in your database, you would define a _q_lookup() in the namespace that handles /user/ requests:

def _q_lookup [plain] (request, name):
    if not request.session.user.is_admin():
        raise AccessError("permission denied")
    user = get_database().get_user(name)
    if user is None:
        raise TraversalError("no such user: %r" % name)
    else:
        "<h1>User %s</h1>\n" % html_quote(name)
        "<table>\n"
        " <tr><th>real name</th><td>%s</td>\n" % user.real_name
        # ...

(This assumes that the namespace in question is a PTL module, not a Python module.)

To publish more complex objects, you'll want to use _q_lookup()'s ability to return a new namespace that Quixote continues traversing. The usual way to do this is to return an instance of a class that implements the web front-end to your object. That class must have a _q_exports attribute, and it will almost certainly have a _q_index() method. It might also have _q_access() and _q_lookup() (yes, _q_lookup() calls can nest arbitrarily deeply).

For example, you might want /user/joe/ to show a summary, /user/joe/history to show a login history, /user/joe/prefs to be a page where joe can edit his personal preferences, etc. The _q_lookup() function would then be

def _q_lookup (request, name):
    return UserUI(request, name)

and the UserUI class, which implements the web interface to user objects, might look like

class UserUI:
    _q_exports = ['history', 'prefs']

    def __init__ (self, request, name):
        if not request.session.user.is_admin():
            raise AccessError("permission denied")
        self.user = get_database().get_user(name)
        if self.user is None:
            raise TraversalError("no such user: %r" % name)

    def _q_index (self, request):
        # ... generate summary page ...

    def history (self, request):
        # ... generate history page ...

    def prefs (self, request):
        # ... generate prefs-editing page ...

_q_resolve(name)

_q_resolve() looks a bit like _q_lookup(), but is intended for a different purpose. Quixote applications can be slow to start up because they have to import a large number of Python and PTL modules. _q_resolve() is a hook that lets time-consuming imports be postponed until the code is actually needed

name is a string containing the next component of the path. _q_resolve() should do whatever imports are necessary and return a module that will be traversed further. (Nothing enforces that this function return a module, so you could also return other types, such as a class instance, a callable object, or even a string) if the last component of the path is being resolved. Given _q_resolve()'s memoization feature, though, returning a module is the most useful thing to do.)

_q_resolve() is only ever called for names that are in _q_exports and that don't already exist in the containing namespace. It is not passed the request object, so its return value can't depend on the client in any way. Calls are also memoized; after being called the object returned will be added to the containing namespace, so _q_resolve() will be called at most once for a given name.

Most commonly, _q_resolve() will look something like this:

_q_exports = [..., 'expensive', ...]

def _q_resolve(name):
    if name == 'expensive':
        from otherpackage import expensive
        return expensive

Let's say this function is in app.ui. The first time /expensive is accessed, _q_resolve('expensive') is called, the otherpackage.expensive module is returned and traversal continues. The imported module is also saved as app.ui.expensive, so future references to /expensive won't need to invoke the _q_resolve() hook.

_q_exception_handler(request, exception)

Quixote will display a default error page when a PublishError exception is raised. Before displaying the default page, Quixote will search back through the list of namespaces traversed looking for an object with a _q_exception_handler attribute. That attribute is expected to be a function and is called with the request and exception instance as arguments and should return the error page (e.g. a string). If the handler doesn't want to handle a particular error it can re-raise it and the next nearest handler will be found. If no _q_exception_handler is found, the default Quixote handler is used.

$Id: programming.txt 23893 2004-04-06 19:31:20Z nascheme $

Quixote-1.2/doc/programming.txt0000664000225700100400000004553410034602610017410 0ustar naschememems00000000000000Quixote Programming Overview ============================ This document explains how a Quixote application is structured. Be sure you have read the "Understanding the demo" section of demo.txt first -- this explains a lot of Quixote fundamentals. There are three components to a Quixote application: 1) A driver script, usually a CGI or FastCGI script. This is the interface between your web server (eg., Apache) and the bulk of your application code. The driver script is responsible for creating a Quixote publisher customized for your application and invoking its publishing loop. 2) A configuration file. This file specifies various features of the Publisher class, such as how errors are handled, the paths of various log files, and various other things. Read through quixote/config.py for the full list of configuration settings. The most important configuration parameters are: ``ERROR_EMAIL`` e-mail address to which errors will be mailed ``ERROR_LOG`` file to which errors will be logged For development/debugging, you should also set ``DISPLAY_EXCEPTIONS`` true and ``SECURE_ERRORS`` false; the defaults are the reverse, to favour security over convenience. 3) Finally, the bulk of the code will be in a Python package or module, called the root namespace. The Quixote publisher will be set up to start traversing at the root namespace. Driver script ------------- The driver script is the interface between your web server and Quixote's "publishing loop", which in turn is the gateway to your application code. Thus, there are two things that your Quixote driver script must do: * create a Quixote publisher -- that is, an instance of the Publisher class provided by the quixote.publish module -- and customize it for your application * invoke the Quixote publishing loop by calling the 'publish_cgi()' method of the publisher The publisher is responsible for translating URLs to Python objects and calling the appropriate function, method, or PTL template to retrieve the information and/or carry out the action requested by the URL. The most important application-specific customization done by the driver script is to set the root namespace of your application. Broadly speaking, a namespace is any Python object with attributes. The most common namespaces are modules, packages, and class instances. The root namespace of a Quixote application is usually a Python package, although for a small application it could be a regular module. The driver script can be very simple; for example, here is a trimmed-down version of demo.cgi, the driver script for the Quixote demo:: from quixote import enable_ptl, Publisher enable_ptl() app = Publisher("quixote.demo") app.setup_logs() app.publish_cgi() (Whether you install this as ``demo.cgi``, ``demo.fcgi``, ``demo.py``, or whatever is up to you and your web server.) That's almost the simplest possible case -- there's no application-specific configuration info apart from the root namespace. (The only way to make this simpler would be to remove the ``enable_ptl()`` and ``setup_logs()`` calls. The former would remove the ability to import PTL modules, which is at least half the fun with Quixote; the latter would disable Quixote's debug and error logging, which is very useful.) Here's a slightly more elaborate example, for a hypothetical database of books:: from quixote import enable_ptl, Publisher from quixote.config import Config # Install the PTL import hook, so we can use PTL modules in this app enable_ptl() # Create a Publisher instance with the default configuration. pub = Publisher('books') # Read a config file to override some default values. pub.read_config('/www/conf/books.conf') # Setup error and debug logging (do this after read_config(), so # the settings in /www/conf/books.conf have an effect!). pub.setup_logs() # Enter the publishing main loop pub.publish_cgi() The application code is kept in a package named simply 'books' in this example, so its name is provided as the root namespace when creating the Publisher instance. The SessionPublisher class in quixote.publish can also be used; it provides session tracking. The changes required to use SessionPublisher would be:: ... from quixote.publish import SessionPublisher ... pub = SessionPublisher(PACKAGE_NAME) ... For details on session management, see session-mgmt.txt. Getting the driver script to actually run is between you and your web server. See the web-server.txt document for help, especially with Apache (which is the only web server we currently know anything about). Configuration file ------------------ In the ``books.cgi`` driver script, configuration information is read from a file by this line:: pub.read_config('/www/conf/books.conf') You should never edit the default values in quixote/config.py, because your edits will be lost if you upgrade to a newer Quixote version. You should certainly read it, though, to understand what all the configuration variables are. The configuration file contains Python code, which is then evaluated using Python's built-in function ``execfile()``. Since it's Python code, it's easy to set config variables:: ACCESS_LOG = "/www/log/access/books.log" DEBUG_LOG = "/www/log/books-debug.log" ERROR_LOG = "/www/log/books-error.log" You can also execute arbitrary Python code to figure out what the variables should be. The following example changes some settings to be more convenient for a developer when the ``WEB_MODE`` environment variable is the string ``DEVEL``:: web_mode = os.environ["WEB_MODE"] if web_mode == "DEVEL": DISPLAY_EXCEPTIONS = 1 SECURE_ERRORS = 0 RUN_ONCE = 1 elif web_mode in ("STAGING", "LIVE"): DISPLAY_EXCEPTIONS = 0 SECURE_ERRORS = 1 RUN_ONCE = 0 else: raise RuntimeError, "unknown server mode: %s" % web_mode At the MEMS Exchange, we use this flexibility to display tracebacks in ``DEVEL`` mode, to redirect generated e-mails to a staging address in ``STAGING`` mode, and to enable all features in ``LIVE`` mode. Logging ------- Every Quixote application can have up to three log files, each of which is selected by a different configuration variable: * access log (``ACCESS_LOG``) * error log (``ERROR_LOG``) * debug log (``DEBUG_LOG``) If you want logging to work, you must call ``setup_logs()`` on your Publisher object after creating it and reading any application-specific config file. (This only applies for CGI/FastCGI driver scripts, where you are responsible for creating the Publisher object. With mod_python under Apache, it's taken care of for you.) Quixote writes one (rather long) line to the access log for each request it handles; we have split that line up here to make it easier to read:: 127.0.0.1 - 2001-10-15 09:48:43 2504 "GET /catalog/ HTTP/1.1" 200 'Opera/6.0 (Linux; U)' 0.10sec This line consists of: * client IP address * current user (according to Quixote session management mechanism, so this will be "-" unless you're using a session manager that does authentication) * date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss * process ID of the process serving the request (eg. your CGI/FastCGI driver script) * the HTTP request line (request method, URI, and protocol) * response status code * HTTP user agent string (specifically, this is ``repr(os.environ.get('HTTP_USER_AGENT', ''))``) * time to complete the request If no access log is configured (ie., ``ACCESS_LOG`` is ``None``), then Quixote will not do any access logging. The error log is used for two purposes: * all application output to standard error (``sys.stderr``) goes to Quixote's error log * all application tracebacks will be written to Quixote's error log If no error log is configured (with ``ERROR_LOG``), then both types of messages will be written to the stderr supplied to Quixote for this request by your web server. At least for CGI/FastCGI scripts under Apache, this winds up in Apache's error log. The debug log is where any application output to stdout goes. Thus, you can just sprinkle ``print`` statements into your application for debugging; if you have configured a debug log, those print statements will wind up there. If you don't configure a debug log, they go to the bit bucket (``/dev/null`` on Unix, ``NUL`` on Windows). Application code ---------------- Finally, we reach the most complicated part of a Quixote application. However, thanks to Quixote's design, everything you've ever learned about designing and writing Python code is applicable, so there are no new hoops to jump through. The only new language to learn is PTL, which is simply Python with a novel way of generating function return values -- see PTL.txt for details. An application's code lives in a Python package that contains both .py and .ptl files. Complicated logic should be in .py files, while .ptl files, ideally, should contain only the logic needed to render your Web interface and basic objects as HTML. As long as your driver script calls ``enable_ptl()``, you can import PTL modules (.ptl files) just as if they were Python modules. Quixote's publisher will start at the root of this package, and will treat the rest of the URL as a path into the package's contents. Here are some examples, assuming that the ``URL_PREFIX`` is ``"/q"``, your web server is setup to rewrite ``/q`` requests as calls to (eg.) ``/www/cgi-bin/books.cgi``, and the root package for your application is 'books':: http://.../q/ call books._q_index() http://.../q/other call books.other(), if books.other is callable (eg. a function or method) http://.../q/other redirect to /q/other/, if books.other is a namespace (eg. a module or sub-package) http://.../q/other/ call books.other._q_index(), if books.other is a namespace One of Quixote's design principles is "Be explicit." Therefore there's no complicated rule for remembering which functions in a module are public; you just have to list them all in the _q_exports variable, which should be a list of strings naming the public functions. You don't need to list the ``_q_index()`` function as being public; that's assumed. Eg. if ``foo()`` is a function to be exported (via Quixote to the web) from your application's namespace, you should have this somewhere in that namespace (ie. at module level in a module or __init__.py file):: _q_exports = ['foo'] At times it is desirable for URLs to contain path components that are not valid Python identifiers. In these cases you can provide an explicit external to internal name mapping. For example:: _q_exports = ['foo', ('stylesheet.css', 'stylesheet_css')] When a function is callable from the web, it must expect a single parameter, which will be an instance of the HTTPRequest class. This object contains everything Quixote could discover about the current HTTP request -- CGI environment variables, form data, cookies, etc. When using SessionPublisher, request.session is a Session object for the user agent making the request. The function should return a string; all PTL templates return a string automatically. ``request.response`` is an HTTPResponse instance, which has methods for setting the content-type of the function's output, generating an HTTP redirect, specifying arbitrary HTTP response headers, and other common tasks. (Actually, the request object also has a method for generating a redirect. It's usually better to use this -- ie. code ``request.redirect(...)`` because generating a redirect correctly requires knowledge of the request, and only the request object has that knowledge. ``request.response.redirect(...)`` only works if you supply an absolute URL, eg. ``"http://www.example.com/foo/bar"``.) Use :: pydoc quixote.http_request pydoc quixote.http_response to view the documentation for the HTTPRequest and HTTPResponse classes, or consult the source code for all the gory details. There are a few special functions that affect Quixote's traversal of a URL to determine how to handle it: ``_q_access()``, ``_q_lookup()``, and ``_q_resolve()``. ``_q_access(request)`` ---------------------- If this function is present in a module, it will be called before attempting to traverse any further. It can look at the contents of request and decide if the traversal can continue; if not, it should raise quixote.errors.AccessError (or a subclass), and Quixote will return a 403 ("forbidden") HTTP status code. The return value is ignored if ``_q_access()`` doesn't raise an exception. For example, in the MEMS Exchange code, we have some sets of pages that are only accessible to signed-in users of a certain type. The ``_q_access()`` function looks like this:: def _q_access (request): if request.session.user is None: raise NotLoggedInError("You must be signed in.") if not (request.session.user.is_admin() or request.session.user.is_fab()): raise AccessError("You don't have access to the reports page.") This is less error-prone than having to remember to add checks to every single public function. ``_q_lookup(request, name)`` ----------------------------- This function translates an arbitrary string into an object that we continue traversing. This is very handy; it lets you put user-space objects into your URL-space, eliminating the need for digging ID strings out of a query, or checking ``PATH_INFO`` after Quixote's done with it. But it is a compromise with security: it opens up the traversal algorithm to arbitrary names not listed in ``_q_exports``. (``_q_lookup()`` is never called for names listed in ``_q_exports``.) You should therefore be extremely paranoid about checking the value of ``name``. ``request`` is the request object, as it is everywhere else; ``name`` is a string containing the next component of the path. ``_q_lookup()`` should return either a string (a complete document that will be returned to the client) or some object that can be traversed further. Returning a string is useful in simple cases, eg. if you want the ``/user/joe`` URI to show everything about user "joe" in your database, you would define a ``_q_lookup()`` in the namespace that handles ``/user/`` requests:: def _q_lookup [plain] (request, name): if not request.session.user.is_admin(): raise AccessError("permission denied") user = get_database().get_user(name) if user is None: raise TraversalError("no such user: %r" % name) else: "

User %s

\n" % html_quote(name) "\n" " \n" % user.real_name # ... (This assumes that the namespace in question is a PTL module, not a Python module.) To publish more complex objects, you'll want to use ``_q_lookup()``'s ability to return a new namespace that Quixote continues traversing. The usual way to do this is to return an instance of a class that implements the web front-end to your object. That class must have a ``_q_exports`` attribute, and it will almost certainly have a ``_q_index()`` method. It might also have ``_q_access()`` and ``_q_lookup()`` (yes, ``_q_lookup()`` calls can nest arbitrarily deeply). For example, you might want ``/user/joe/`` to show a summary, ``/user/joe/history`` to show a login history, ``/user/joe/prefs`` to be a page where joe can edit his personal preferences, etc. The ``_q_lookup()`` function would then be :: def _q_lookup (request, name): return UserUI(request, name) and the UserUI class, which implements the web interface to user objects, might look like :: class UserUI: _q_exports = ['history', 'prefs'] def __init__ (self, request, name): if not request.session.user.is_admin(): raise AccessError("permission denied") self.user = get_database().get_user(name) if self.user is None: raise TraversalError("no such user: %r" % name) def _q_index (self, request): # ... generate summary page ... def history (self, request): # ... generate history page ... def prefs (self, request): # ... generate prefs-editing page ... ``_q_resolve(name)`` -------------------- ``_q_resolve()`` looks a bit like ``_q_lookup()``, but is intended for a different purpose. Quixote applications can be slow to start up because they have to import a large number of Python and PTL modules. ``_q_resolve()`` is a hook that lets time-consuming imports be postponed until the code is actually needed ``name`` is a string containing the next component of the path. ``_q_resolve()`` should do whatever imports are necessary and return a module that will be traversed further. (Nothing enforces that this function return a module, so you could also return other types, such as a class instance, a callable object, or even a string) if the last component of the path is being resolved. Given ``_q_resolve()``'s memoization feature, though, returning a module is the most useful thing to do.) ``_q_resolve()`` is only ever called for names that are in ``_q_exports`` and that don't already exist in the containing namespace. It is not passed the request object, so its return value can't depend on the client in any way. Calls are also memoized; after being called the object returned will be added to the containing namespace, so ``_q_resolve()`` will be called at most once for a given name. Most commonly, ``_q_resolve()`` will look something like this:: _q_exports = [..., 'expensive', ...] def _q_resolve(name): if name == 'expensive': from otherpackage import expensive return expensive Let's say this function is in ``app.ui``. The first time ``/expensive`` is accessed, ``_q_resolve('expensive')`` is called, the ``otherpackage.expensive`` module is returned and traversal continues. The imported module is also saved as ``app.ui.expensive``, so future references to ``/expensive`` won't need to invoke the ``_q_resolve()`` hook. ``_q_exception_handler(request, exception)`` -------------------------------------------- Quixote will display a default error page when a ``PublishError`` exception is raised. Before displaying the default page, Quixote will search back through the list of namespaces traversed looking for an object with a ``_q_exception_handler`` attribute. That attribute is expected to be a function and is called with the request and exception instance as arguments and should return the error page (e.g. a string). If the handler doesn't want to handle a particular error it can re-raise it and the next nearest handler will be found. If no ``_q_exception_handler`` is found, the default Quixote handler is used. $Id: programming.txt 23893 2004-04-06 19:31:20Z nascheme $ Quixote-1.2/doc/session-mgmt.html0000664000225700100400000004643210131012103017624 0ustar naschememems00000000000000Quixote Session Management

Quixote Session Management

HTTP was originally designed as a stateless protocol, meaning that every request for a document or image was conducted in a separate TCP connection, and that there was no way for a web server to tell if two separate requests actually come from the same user. It's no longer necessarily true that every request is conducted in a separate TCP connection, but HTTP is still fundamentally stateless. However, there are many applications where it is desirable or even essential to establish a "session" for each user, ie. where all requests performed by that user are somehow tied together on the server.

HTTP cookies were invented to address this requirement, and they are still the best solution for establishing sessions on top of HTTP. Thus, Quixote's session management mechanism is cookie-based. (The most common alternative is to generate long, complicated URLs with an embedded session identifier. Since Quixote views the URL as a fundamental part of the web user interface, a URL-based session management scheme would be un-Quixotic.)

For further reading: the standard for cookies that is approximately implemented by most current browsers is RFC 2109; the latest version of the standard is RFC 2965. Those RFCs can be found here:

ftp://ftp.isi.edu/in-notes/rfc2109.txt

ftp://ftp.isi.edu/in-notes/rfc2965.txt

In a nutshell, session management with Quixote works like this:

  • when a user-agent first requests a page from a Quixote application that implements session management, Quixote creates a Session object and generates a session ID (a random 64-bit number). The Session object is attached to the current HTTPRequest object, so that application code involved in processing this request has access to the Session object.

  • if, at the end of processing that request, the application code has stored any information in the Session object, Quixote saves the session in its SessionManager object for use by future requests and sends a session cookie, called QX_session by default, to the user. The session cookie contains the session ID encoded as a hexadecimal string, and is included in the response headers, eg.

    Set-Cookie: QX_session="928F82A9B8FA92FD"
    

    (You can instruct Quixote to specify the domain and path for URLs to which this cookie should be sent.)

  • the user agent stores this cookie for future requests

  • the next time the user agent requests a resource that matches the cookie's domain and path, it includes the QX_session cookie previously generated by Quixote in the request headers, eg.:

    Cookie: QX_session="928F82A9B8FA92FD"
    
  • while processing the request, Quixote decodes the session ID and looks up the corresponding Session object in its SessionManager. If there is no such session, the session cookie is bogus or out-of-date, so Quixote raises SessionError; ultimately the user gets an error page. Otherwise, the Session object is attached to the HTTPRequest object that is available to all application code used to process the request.

There are two caveats to keep in mind before proceeding, one major and one minor:

  • Quixote's standard Session and SessionManager class do not implement any sort of persistence, meaning that all sessions disappear when the process handling web requests terminates. Thus, session management is completely useless with a plain CGI driver script unless you add some persistence to the mix; see "Session persistence" below for information.
  • Quixote never expires sessions; if you want user sessions to be cleaned up after a period of inactivity, you will have to write code to do it yourself.

Session management demo

There's a simple demo of Quixote's session management in demo/session_demo.cgi and demo/session.ptl. The demo implements a simple session persistence scheme (each session is written to a separate pickle file in /tmp/quixote-session-demo), so running it through CGI is just fine.

I'll assume that you've added a rewrite rule so that requests for /qsdemo/ are handled by session_demo.cgi, similar to the rewriting for /qdemo/ described in web-server.txt. Once that's done, point your browser at

http://<hostname>/qsdemo/

and play around.

This particular application uses sessions to keep track of just two things: the user's identity and the number of requests made in this session. The first is addressed by Quixote's standard Session class -- every Session object has a user attribute, which you can use for anything you like. In the session demo, we simply store a string, the user's name, which is entered by the user.

Tracking the number of requests is a bit more interesting: from the DemoSession class in session_demo.cgi:

def __init__ (self, request, id):
    Session.__init__(self, request, id)
    self.num_requests = 0

def start_request (self, request):
    Session.start_request(self, request)
    self.num_requests += 1

When the session is created, we initialize the request counter; and when we start processing each request, we increment it.

Using the session information in the application code is simple. For example, here's the PTL code that checks if the user has logged in (identified herself) yet, and generates a login form if not:

session = request.session
if session.user is None:
    '''
    <p>You haven\'t introduced yourself yet.<br>
    Please tell me your name:
    '''
    login_form()

(The login_form() template just emits a simple HTML form -- see demo/session.ptl for full source.)

If the user has already identified herself, then she doesn't need to do so again -- so the other branch of that if statement simply prints a friendly greeting:

else:
    ('<p>Hello, %s.  Good to see you again.</p>\n' 
     % html_quote(session.user))

Note that we must quote the user's name, because they are free to enter anything they please, including special HTML characters like & or <.

Of course, session.user will never be set if we don't set it ourselves. The code that processes the login form is just this (from login() in demo/session.ptl):

if request.form:
    user = request.form.get("name")
    if not user:
        raise QueryError("no user name supplied")

    session.user = user

This is obviously a very simple application -- we're not doing any verification of the user's input. We have no user database, no passwords, and no limitations on what constitutes a "user name". A real application would have all of these, as well as a way for users to add themselves to the user database -- ie. register with your web site.

Writing the session class

You will almost certainly have to write a custom session class for your application by subclassing Quixote's standard Session class. Every custom session class has two essential responsibilities:

  • initialize the attributes that will be used by your application
  • override the has_info() method, so the session manager knows when it must save your session object

The first one is fairly obvious and just good practice. The second is essential, and not at all obvious. The has_info() method exists because SessionManager does not automatically hang on to all session objects; this is a defence against clients that ignore cookies, making your session manager create lots of session objects that are just used once. As long as those session objects are not saved, the burden imposed by these clients is not too bad -- at least they aren't sucking up your memory, or bogging down the database that you save session data to. Thus, the session manager uses has_info() to know if it should hang on to a session object or not: if a session has information that must be saved, the session manager saves it and sends a session cookie to the client.

For development/testing work, it's fine to say that your session objects should always be saved:

def has_info (self):
    return 1

The opposite extreme is to forget to override has_info() altogether, in which case session management most likely won't work: unless you tickle the Session object such that the base has_info() method returns true, the session manager won't save the sessions that it creates, and Quixote will never drop a session cookie on the client.

In a real application, you need to think carefully about what data to store in your sessions, and how has_info() should react to the presence of that data. If you try and track something about every single visitor to your site, sooner or later one of those a broken/malicious client that ignores cookies and robots.txt will come along and crawl your entire site, wreaking havoc on your Quixote application (or the database underlying it).

Session persistence

Keeping session data across requests is all very nice, but in the real world you want that data to survive across process termination. With CGI, this is essential, since each process serves exactly one request and then terminates. With other execution mechanisms, though, it's still important -- you don't want to lose all your session data just because your long-lived server process was restarted, or your server machine was rebooted.

However, every application is different, so Quixote doesn't provide any built-in mechanism for session persistence. Instead, it provides a number of hooks, most in the SessionManager class, that let you plug in your preferred persistence mechanism.

The first and most important hook is in the SessionManager constructor: you can provide an alternate mapping object that SessionManager will use to store session objects in. By default, SessionManager uses an ordinary dictionary; if you provide a mapping object that implements persistence, then your session data will automatically persist across processes. For example, you might use the standard 'shelve' module, which provides a mapping object on top of a DBM or Berkeley DB file:

import shelve
sessions = shelve.open("/tmp/quixote-sessions")
session_mgr = SessionManager(session_mapping=sessions)

For a persistent mapping implementation that doesn't require any external libraries, see the DirMapping class in demo/session_demo.cgi.

If you use one of these relatively simple persistent mapping types, you'll also need to override is_dirty() in your Session class. That's in addition to overriding has_info(), which determines if a session object is ever saved; is_dirty() is only called on sessions that have already been added to the session mapping, to see if they need to be "re-added". The default implementation always returns false, because once an object has been added to a normal dictionary, there's no need to add it again. However, with simple persistent mapping types like shelve and DirMapping, you need to store the object again each time it changes. Thus, is_dirty() should return true if the session object needs to be re-written. For a simple, naive, but inefficient implementation, making is_dirty an alias for has_info() will work -- that just means that once the session has been written once, it will be re-written on every request. (This is what DemoSession in demo/session_demo.cgi does.)

The third and final part of the persistence interface only applies if you are using a transactional persistence mechanism, such as ZODB or an industrial-strength relational database. In that case, you need a place to commit or abort the transaction that contains pending changes to the current session. SessionManager provides two methods for you to override: abort_changes() and commit_changes(). abort_changes() is called by SessionPublisher whenever a request crashes, ie. whenever your application raises an exception other than PublishError. commit_changes() is called for requests that complete successfully, or that raise a PublishError exception. They are defined as follows:

def abort_changes (self, session):
    """abort_changes(session : Session)"""

def commit_changes (self, session):
    """commit_changes(session : Session)"""

Obviously, you'll have to write your own SessionManager subclass if you need to take advantage of these hooks for transactional session persistence.

$Id: session-mgmt.txt 20217 2003-01-16 20:51:53Z akuchlin $

Quixote-1.2/doc/session-mgmt.txt0000664000225700100400000003570307611615551017527 0ustar naschememems00000000000000Quixote Session Management ========================== HTTP was originally designed as a stateless protocol, meaning that every request for a document or image was conducted in a separate TCP connection, and that there was no way for a web server to tell if two separate requests actually come from the same user. It's no longer necessarily true that every request is conducted in a separate TCP connection, but HTTP is still fundamentally stateless. However, there are many applications where it is desirable or even essential to establish a "session" for each user, ie. where all requests performed by that user are somehow tied together on the server. HTTP cookies were invented to address this requirement, and they are still the best solution for establishing sessions on top of HTTP. Thus, Quixote's session management mechanism is cookie-based. (The most common alternative is to generate long, complicated URLs with an embedded session identifier. Since Quixote views the URL as a fundamental part of the web user interface, a URL-based session management scheme would be un-Quixotic.) For further reading: the standard for cookies that is approximately implemented by most current browsers is RFC 2109; the latest version of the standard is RFC 2965. Those RFCs can be found here: ftp://ftp.isi.edu/in-notes/rfc2109.txt ftp://ftp.isi.edu/in-notes/rfc2965.txt In a nutshell, session management with Quixote works like this: * when a user-agent first requests a page from a Quixote application that implements session management, Quixote creates a Session object and generates a session ID (a random 64-bit number). The Session object is attached to the current HTTPRequest object, so that application code involved in processing this request has access to the Session object. * if, at the end of processing that request, the application code has stored any information in the Session object, Quixote saves the session in its SessionManager object for use by future requests and sends a session cookie, called ``QX_session`` by default, to the user. The session cookie contains the session ID encoded as a hexadecimal string, and is included in the response headers, eg. :: Set-Cookie: QX_session="928F82A9B8FA92FD" (You can instruct Quixote to specify the domain and path for URLs to which this cookie should be sent.) * the user agent stores this cookie for future requests * the next time the user agent requests a resource that matches the cookie's domain and path, it includes the ``QX_session`` cookie previously generated by Quixote in the request headers, eg.:: Cookie: QX_session="928F82A9B8FA92FD" * while processing the request, Quixote decodes the session ID and looks up the corresponding Session object in its SessionManager. If there is no such session, the session cookie is bogus or out-of-date, so Quixote raises SessionError; ultimately the user gets an error page. Otherwise, the Session object is attached to the HTTPRequest object that is available to all application code used to process the request. There are two caveats to keep in mind before proceeding, one major and one minor: * Quixote's standard Session and SessionManager class do not implement any sort of persistence, meaning that all sessions disappear when the process handling web requests terminates. Thus, session management is completely useless with a plain CGI driver script unless you add some persistence to the mix; see "Session persistence" below for information. * Quixote never expires sessions; if you want user sessions to be cleaned up after a period of inactivity, you will have to write code to do it yourself. Session management demo ----------------------- There's a simple demo of Quixote's session management in ``demo/session_demo.cgi`` and ``demo/session.ptl``. The demo implements a simple session persistence scheme (each session is written to a separate pickle file in ``/tmp/quixote-session-demo``), so running it through CGI is just fine. I'll assume that you've added a rewrite rule so that requests for ``/qsdemo/`` are handled by ``session_demo.cgi``, similar to the rewriting for ``/qdemo/`` described in web-server.txt. Once that's done, point your browser at :: http:///qsdemo/ and play around. This particular application uses sessions to keep track of just two things: the user's identity and the number of requests made in this session. The first is addressed by Quixote's standard Session class -- every Session object has a ``user`` attribute, which you can use for anything you like. In the session demo, we simply store a string, the user's name, which is entered by the user. Tracking the number of requests is a bit more interesting: from the DemoSession class in session_demo.cgi:: def __init__ (self, request, id): Session.__init__(self, request, id) self.num_requests = 0 def start_request (self, request): Session.start_request(self, request) self.num_requests += 1 When the session is created, we initialize the request counter; and when we start processing each request, we increment it. Using the session information in the application code is simple. For example, here's the PTL code that checks if the user has logged in (identified herself) yet, and generates a login form if not:: session = request.session if session.user is None: '''

You haven\'t introduced yourself yet.
Please tell me your name: ''' login_form() (The ``login_form()`` template just emits a simple HTML form -- see ``demo/session.ptl`` for full source.) If the user has already identified herself, then she doesn't need to do so again -- so the other branch of that ``if`` statement simply prints a friendly greeting:: else: ('

Hello, %s. Good to see you again.

\n' % html_quote(session.user)) Note that we must quote the user's name, because they are free to enter anything they please, including special HTML characters like ``&`` or ``<``. Of course, ``session.user`` will never be set if we don't set it ourselves. The code that processes the login form is just this (from ``login()`` in ``demo/session.ptl``):: if request.form: user = request.form.get("name") if not user: raise QueryError("no user name supplied") session.user = user This is obviously a very simple application -- we're not doing any verification of the user's input. We have no user database, no passwords, and no limitations on what constitutes a "user name". A real application would have all of these, as well as a way for users to add themselves to the user database -- ie. register with your web site. Configuring the session cookie ------------------------------ Quixote allows you to configure several aspects of the session cookie that it exchanges with clients. First, you can set the name of the cookie; this is important if you have multiple independent Quixote applications running on the same server. For example, the config file for the first application might have :: SESSION_COOKIE_NAME = "foo_session" and the second application might have :: SESSION_COOKIE_NAME = "bar_session" Next, you can use ``SESSION_COOKIE_DOMAIN`` and ``SESSION_COOKIE_PATH`` to set the cookie attributes that control which requests the cookie is included with. By default, these are both ``None``, which instructs Quixote to send the cookie without ``Domain`` or ``Path`` qualifiers. For example, if the client requests ``/foo/bar/`` from www.example.com, and Quixote decides that it must set the session cookie in the response to that request, then the server would send :: Set-Cookie: QX_session="928F82A9B8FA92FD" in the response headers. Since no domain or path were specified with that cookie, the browser will only include the cookie with requests to www.example.com for URIs that start with ``/foo/bar/``. If you want to ensure that your session cookie is included with all requests to www.example.com, you should set ``SESSION_COOKIE_PATH`` in your config file:: SESSION_COOKIE_PATH = "/" which will cause Quixote to set the cookie like this:: Set-Cookie: QX_session="928F82A9B8FA92FD"; Path="/" which will instruct the browser to include that cookie with *all* requests to www.example.com. However, think carefully about what you set ``SESSION_COOKIE_PATH`` to -- eg. if you set it to "/", but all of your Quixote code is under "/q/" in your server's URL-space, then your user's session cookies could be unnecessarily exposed. On shared servers where you don't control all of the code, this is especially dangerous; be sure to use (eg.) :: SESSION_COOKIE_PATH = "/q/" on such servers. The trailing slash is important; without it, your session cookies will be sent to URIs like ``/qux`` and ``/qix``, even if you don't control those URIs. If you want to share the cookie across servers in your domain, eg. www1.example.com and www2.example.com, you'll also need to set ``SESSION_COOKIE_DOMAIN``: SESSION_COOKIE_DOMAIN = ".example.com" Finally, note that the ``SESSION_COOKIE_*`` configuration variables *only* affect Quixote's session cookie; if you set your own cookies using the ``HTTPResponse.set_cookie()`` method, then the cookie sent to the client is completely determined by that ``set_cookie()`` call. See RFCs 2109 and 2965 for more information on the rules browsers are supposed to follow for including cookies with HTTP requests. Writing the session class ------------------------- You will almost certainly have to write a custom session class for your application by subclassing Quixote's standard Session class. Every custom session class has two essential responsibilities: * initialize the attributes that will be used by your application * override the ``has_info()`` method, so the session manager knows when it must save your session object The first one is fairly obvious and just good practice. The second is essential, and not at all obvious. The has_info() method exists because SessionManager does not automatically hang on to all session objects; this is a defence against clients that ignore cookies, making your session manager create lots of session objects that are just used once. As long as those session objects are not saved, the burden imposed by these clients is not too bad -- at least they aren't sucking up your memory, or bogging down the database that you save session data to. Thus, the session manager uses has_info() to know if it should hang on to a session object or not: if a session has information that must be saved, the session manager saves it and sends a session cookie to the client. For development/testing work, it's fine to say that your session objects should always be saved:: def has_info (self): return 1 The opposite extreme is to forget to override ``has_info()`` altogether, in which case session management most likely won't work: unless you tickle the Session object such that the base ``has_info()`` method returns true, the session manager won't save the sessions that it creates, and Quixote will never drop a session cookie on the client. In a real application, you need to think carefully about what data to store in your sessions, and how ``has_info()`` should react to the presence of that data. If you try and track something about every single visitor to your site, sooner or later one of those a broken/malicious client that ignores cookies and ``robots.txt`` will come along and crawl your entire site, wreaking havoc on your Quixote application (or the database underlying it). Session persistence ------------------- Keeping session data across requests is all very nice, but in the real world you want that data to survive across process termination. With CGI, this is essential, since each process serves exactly one request and then terminates. With other execution mechanisms, though, it's still important -- you don't want to lose all your session data just because your long-lived server process was restarted, or your server machine was rebooted. However, every application is different, so Quixote doesn't provide any built-in mechanism for session persistence. Instead, it provides a number of hooks, most in the SessionManager class, that let you plug in your preferred persistence mechanism. The first and most important hook is in the SessionManager constructor: you can provide an alternate mapping object that SessionManager will use to store session objects in. By default, SessionManager uses an ordinary dictionary; if you provide a mapping object that implements persistence, then your session data will automatically persist across processes. For example, you might use the standard 'shelve' module, which provides a mapping object on top of a DBM or Berkeley DB file:: import shelve sessions = shelve.open("/tmp/quixote-sessions") session_mgr = SessionManager(session_mapping=sessions) For a persistent mapping implementation that doesn't require any external libraries, see the DirMapping class in ``demo/session_demo.cgi``. If you use one of these relatively simple persistent mapping types, you'll also need to override ``is_dirty()`` in your Session class. That's in addition to overriding ``has_info()``, which determines if a session object is *ever* saved; ``is_dirty()`` is only called on sessions that have already been added to the session mapping, to see if they need to be "re-added". The default implementation always returns false, because once an object has been added to a normal dictionary, there's no need to add it again. However, with simple persistent mapping types like shelve and DirMapping, you need to store the object again each time it changes. Thus, ``is_dirty()`` should return true if the session object needs to be re-written. For a simple, naive, but inefficient implementation, making is_dirty an alias for ``has_info()`` will work -- that just means that once the session has been written once, it will be re-written on every request. (This is what DemoSession in ``demo/session_demo.cgi`` does.) The third and final part of the persistence interface only applies if you are using a transactional persistence mechanism, such as ZODB or an industrial-strength relational database. In that case, you need a place to commit or abort the transaction that contains pending changes to the current session. SessionManager provides two methods for you to override: ``abort_changes()`` and ``commit_changes()``. ``abort_changes()`` is called by SessionPublisher whenever a request crashes, ie. whenever your application raises an exception other than PublishError. ``commit_changes()`` is called for requests that complete successfully, or that raise a PublishError exception. They are defined as follows:: def abort_changes (self, session): """abort_changes(session : Session)""" def commit_changes (self, session): """commit_changes(session : Session)""" Obviously, you'll have to write your own SessionManager subclass if you need to take advantage of these hooks for transactional session persistence. $Id: session-mgmt.txt 20217 2003-01-16 20:51:53Z akuchlin $ Quixote-1.2/doc/static-files.html0000664000225700100400000000600310131012104017555 0ustar naschememems00000000000000 Examples of serving static files

Examples of serving static files

The quixote.util module includes classes for making files and directories available as Quixote resources. Here are some examples.

Publishing a Single File

The StaticFile class makes an individual filesystem file (possibly a symbolic link) available. You can also specify the MIME type and encoding of the file; if you don't specify this, the MIME type will be guessed using the standard Python mimetypes.guess_type() function. The default action is to not follow symbolic links, but this behaviour can be changed using the follow_symlinks parameter.

The following example publishes a file with the URL .../stylesheet_css:

# 'stylesheet_css' must be in the _q_exports list
_q_exports = [ ..., 'stylesheet_css', ...]

stylesheet_css = StaticFile(
        "/htdocs/legacy_app/stylesheet.css",
        follow_symlinks=1, mime_type="text/css")

If you want the URL of the file to have a .css extension, you use the external to internal name mapping feature of _q_exports. For example:

_q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...]

Publishing a Directory

Publishing a directory is similar. The StaticDirectory class makes a complete filesystem directory available. Again, the default behaviour is to not follow symlinks. You can also request that the StaticDirectory object cache information about the files in memory so that it doesn't try to guess the MIME type on every hit.

This example publishes the notes/ directory:

_q_exports = [ ..., 'notes', ...]

notes = StaticDirectory("/htdocs/legacy_app/notes")
Quixote-1.2/doc/static-files.txt0000664000225700100400000000331010034602610017437 0ustar naschememems00000000000000Examples of serving static files ================================ The ``quixote.util`` module includes classes for making files and directories available as Quixote resources. Here are some examples. Publishing a Single File ------------------------ The ``StaticFile`` class makes an individual filesystem file (possibly a symbolic link) available. You can also specify the MIME type and encoding of the file; if you don't specify this, the MIME type will be guessed using the standard Python ``mimetypes.guess_type()`` function. The default action is to not follow symbolic links, but this behaviour can be changed using the ``follow_symlinks`` parameter. The following example publishes a file with the URL ``.../stylesheet_css``:: # 'stylesheet_css' must be in the _q_exports list _q_exports = [ ..., 'stylesheet_css', ...] stylesheet_css = StaticFile( "/htdocs/legacy_app/stylesheet.css", follow_symlinks=1, mime_type="text/css") If you want the URL of the file to have a ``.css`` extension, you use the external to internal name mapping feature of ``_q_exports``. For example:: _q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...] Publishing a Directory ---------------------- Publishing a directory is similar. The ``StaticDirectory`` class makes a complete filesystem directory available. Again, the default behaviour is to not follow symlinks. You can also request that the ``StaticDirectory`` object cache information about the files in memory so that it doesn't try to guess the MIME type on every hit. This example publishes the ``notes/`` directory:: _q_exports = [ ..., 'notes', ...] notes = StaticDirectory("/htdocs/legacy_app/notes") Quixote-1.2/doc/upgrading.html0000664000225700100400000003470610131012104017161 0ustar naschememems00000000000000 Upgrading code from older versions of Quixote

Upgrading code from older versions of Quixote

This document lists backward-incompatible changes in Quixote, and explains how to update application code to work with the newer version.

Changes from 0.6.1 to 1.0

Sessions

A leading underscore was removed from the Session attributes __remote_address, __creation_time, and __access_time. If you have pickled Session objects you will need to upgrade them somehow. Our preferred method is to write a script that unpickles each object, renames the attributes and then re-pickles it.

Changes from 0.6 to 0.6.1

_q_exception_handler now called if exception while traversing

_q_exception_handler hooks will now be called if an exception is raised during the traversal process. Quixote 0.6 had a bug that caused _q_exception_handler hooks to only be called if an exception was raised after the traversal completed.

Changes from 0.5 to 0.6

_q_getname renamed to _q_lookup

The _q_getname special function was renamed to _q_lookup, because that name gives a clearer impression of the function's purpose. In 0.6, _q_getname still works but will trigger a warning.

Form Framework Changes

The quixote.form.form module was changed from a .ptl file to a .py file. You should delete or move the existing quixote/ directory in site-packages before running setup.py, or at least delete the old form.ptl and form.ptlc files.

The widget and form classes in the quixote.form package now return htmltext instances. Applications that use forms and widgets will likely have to be changed to use the [html] template type to avoid over-escaping of HTML special characters.

Also, the constructor arguments to SelectWidget and its subclasses have changed. This only affects applications that use the form framework located in the quixote.form package.

In Quixote 0.5, the SelectWidget constructor had this signature:

def __init__ (self, name, value=None,
              allowed_values=None,
              descriptions=None,
              size=None,
              sort=0):

allowed_values was the list of objects that the user could choose, and descriptions was a list of strings that would actually be shown to the user in the generated HTML.

In Quixote 0.6, the signature has changed slightly:

def __init__ (self, name, value=None,
              allowed_values=None,
              descriptions=None,
              options=None,
              size=None,
              sort=0):

The quote argument is gone, and the options argument has been added. If an options argument is provided, allowed_values and descriptions must not be supplied.

The options argument, if present, must be a list of tuples with 1,2, or 3 elements, of the form (value:any, description:any, key:string).

  • value is the object that will be returned if the user chooses this item, and must always be supplied.
  • description is a string or htmltext instance which will be shown to the user in the generated HTML. It will be passed through the htmlescape() functions, so for an ordinary string special characters such as '&' will be converted to '&amp;'. htmltext instances will be left as they are.
  • If supplied, key will be used in the value attribute of the option element (<option value="...">). If not supplied, keys will be generated; value is checked for a _p_oid attribute and if present, that string is used; otherwise the description is used.

In the common case, most applications won't have to change anything, though the ordering of selection items may change due to the difference in how keys are generated.

File Upload Changes

Quixote 0.6 introduces new support for HTTP upload requests. Any HTTP request with a Content-Type of "multipart/form-data" -- which is generally only used for uploads -- is now represented by HTTPUploadRequest, a subclass of HTTPRequest, and the uploaded files themselves are represented by Upload objects.

Whenever an HTTP request has a Content-Type of "multipart/form-data", an instance of HTTPUploadRequest is created instead of HTTPRequest. Some of the fields in the request are presumably uploaded files and might be quite large, so HTTPUploadRequest will read all of the fields supplied in the request body and write them out to temporary files; the temporary files are written in the directory specified by the UPLOAD_DIR configuration variable.

Once the temporary files have been written, the HTTPUploadRequest object is passed to a function or PTL template, just like an ordinary request. The difference between HTTPRequest and HTTPUploadRequest is that all of the form variables are represented as Upload objects. Upload objects have three attributes:

orig_filename
the filename supplied by the browser.
base_filename
a stripped-down version of orig_filename with unsafe characters removed. This could be used when writing uploaded data to a permanent location.
tmp_filename
the path of the temporary file containing the uploaded data for this field.

Consult upload.txt for more information about handling file uploads.

Refactored Publisher Class

Various methods in the Publisher class were rearranged. If your application subclasses Publisher, you may need to change your code accordingly.

  • parse_request() no longer creates the HTTPRequest object; instead a new method, create_request(), handles this, and can be overridden as required.

    As a result, the method signature has changed from parse_request(stdin, env) to parse_request(request).

  • The Publisher.publish() method now catches exceptions raised by parse_request().

Changes from 0.4 to 0.5

Session Management Changes

The Quixote session management interface underwent lots of change and cleanup with Quixote 0.5. It was previously undocumented (apart from docstrings in the code), so we thought that this was a good opportunity to clean up the interface. Nevertheless, those brave souls who got session management working just by reading the code are in for a bit of suffering; this brief note should help clarify things. The definitive documentation for session management is session-mgmt.txt -- you should start there.

Attribute renamings and pickled objects

Most attributes of the standard Session class were made private in order to reduce collisions with subclasses. The downside is that pickled Session objects will break. You might want to (temporarily) modify session.py and add this method to Session:

def __setstate__ (self, dict):
    # Update for attribute renamings made in rev. 1.51.2.3
    # (between Quixote 0.4.7 and 0.5).
    self.__dict__.update(dict)
    if hasattr(self, 'remote_address'):
        self.__remote_address = self.remote_address
        del self.remote_address
    if hasattr(self, 'creation_time'):
        self.__creation_time = self.creation_time
        del self.creation_time
    if hasattr(self, 'access_time'):
        self.__access_time = self.access_time
        del self.access_time
    if hasattr(self, 'form_tokens'):
        self._form_tokens = self.form_tokens
        del self.form_tokens

However, if your sessions were pickled via ZODB, this may not work. (It didn't work for us.) In that case, you'll have to add something like this to your class that inherits from both ZODB's Persistent and Quixote's Session:

def __setstate__ (self, dict):
    # Blechhh!  This doesn't work if I put it in Quixote's
    # session.py, so I have to second-guess how Python
    # treats "__" attribute names.
    self.__dict__.update(dict)
    if hasattr(self, 'remote_address'):
        self._Session__remote_address = self.remote_address
        del self.remote_address
    if hasattr(self, 'creation_time'):
        self._Session__creation_time = self.creation_time
        del self.creation_time
    if hasattr(self, 'access_time'):
        self._Session__access_time = self.access_time
        del self.access_time
    if hasattr(self, 'form_tokens'):
        self._form_tokens = self.form_tokens
        del self.form_tokens

It's not pretty, but it worked for us.

Quixote-1.2/doc/upgrading.txt0000664000225700100400000002325310036535412017047 0ustar naschememems00000000000000Upgrading code from older versions of Quixote ============================================= This document lists backward-incompatible changes in Quixote, and explains how to update application code to work with the newer version. Changes from 0.6.1 to 1.0 ------------------------- Sessions ******** A leading underscore was removed from the ``Session`` attributes ``__remote_address``, ``__creation_time``, and ``__access_time``. If you have pickled ``Session`` objects you will need to upgrade them somehow. Our preferred method is to write a script that unpickles each object, renames the attributes and then re-pickles it. Changes from 0.6 to 0.6.1 ------------------------- ``_q_exception_handler`` now called if exception while traversing ***************************************************************** ``_q_exception_handler`` hooks will now be called if an exception is raised during the traversal process. Quixote 0.6 had a bug that caused ``_q_exception_handler`` hooks to only be called if an exception was raised after the traversal completed. Changes from 0.5 to 0.6 ----------------------- ``_q_getname`` renamed to ``_q_lookup`` *************************************** The ``_q_getname`` special function was renamed to ``_q_lookup``, because that name gives a clearer impression of the function's purpose. In 0.6, ``_q_getname`` still works but will trigger a warning. Form Framework Changes ********************** The ``quixote.form.form`` module was changed from a .ptl file to a .py file. You should delete or move the existing ``quixote/`` directory in ``site-packages`` before running ``setup.py``, or at least delete the old ``form.ptl`` and ``form.ptlc`` files. The widget and form classes in the ``quixote.form`` package now return ``htmltext`` instances. Applications that use forms and widgets will likely have to be changed to use the ``[html]`` template type to avoid over-escaping of HTML special characters. Also, the constructor arguments to ``SelectWidget`` and its subclasses have changed. This only affects applications that use the form framework located in the ``quixote.form`` package. In Quixote 0.5, the ``SelectWidget`` constructor had this signature:: def __init__ (self, name, value=None, allowed_values=None, descriptions=None, size=None, sort=0): ``allowed_values`` was the list of objects that the user could choose, and ``descriptions`` was a list of strings that would actually be shown to the user in the generated HTML. In Quixote 0.6, the signature has changed slightly:: def __init__ (self, name, value=None, allowed_values=None, descriptions=None, options=None, size=None, sort=0): The ``quote`` argument is gone, and the ``options`` argument has been added. If an ``options`` argument is provided, ``allowed_values`` and ``descriptions`` must not be supplied. The ``options`` argument, if present, must be a list of tuples with 1,2, or 3 elements, of the form ``(value:any, description:any, key:string)``. * ``value`` is the object that will be returned if the user chooses this item, and must always be supplied. * ``description`` is a string or htmltext instance which will be shown to the user in the generated HTML. It will be passed through the htmlescape() functions, so for an ordinary string special characters such as '&' will be converted to '&'. htmltext instances will be left as they are. * If supplied, ``key`` will be used in the value attribute of the option element (``
real name%s
XML-RPC Type Python Type or Class
<int> int
<double> float
<string> string
<array> list
<struct> dict
<boolean> xmlrpclib.Boolean
<base64> xmlrpclib.Binary
<dateTime> xmlrpclib.DateTime

Making XML-RPC Calls

Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server lives at a particular URL, so the first step is to create an xmlrpclib.ServerProxy object pointing at that URL.

>>> import xmlrpclib
>>> s = xmlrpclib.ServerProxy(
             'http://www.stuffeddog.com/speller/speller-rpc.cgi')

Now you can simply make a call to the spell-checking service offered by this server:

>>> s.speller.spellCheck('my speling isnt gud', {})
[{'word': 'speling', 'suggestions': ['apeling', 'spelding',
  'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4},
{'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}]
>>> 

This call results in the following XML being sent:

<?xml version='1.0'?>
<methodCall>
     <methodName>speller.spellCheck</methodName>
     <params>
         <param>
                <value><string>my speling isnt gud</string></value>
         </param>
         <param>
                 <value><struct></struct></value>
         </param>
     </params>
</methodCall>

Writing a Quixote Service

In the quixote.util module, Quixote provides a function, xmlrpc(request, func), that processes the body of an XML-RPC request. request is the HTTPRequest object that Quixote passes to every function it invokes. func is a user-supplied function that receives the name of the XML-RPC method being called and a tuple containing the method's parameters. If there's a bug in the function you supply and it raises an exception, the xmlrpc() function will catch the exception and return a Fault to the remote caller.

Here's an example of implementing a simple XML-RPC handler with a single method, get_time(), that simply returns the current time. The first task is to expose a URL for accessing the service.

from quixote.util import xmlrpc

_q_exports = ['rpc']

def rpc (request):
    return xmlrpc(request, rpc_process)

def rpc_process (meth, params):
    ...

When the above code is placed in the __init__.py file for the Python package corresponding to your Quixote application, it exposes the URL http://<hostname>/rpc as the access point for the XML-RPC service.

Next, we need to fill in the contents of the rpc_process() function:

import time

def rpc_process (meth, params):
    if meth == 'get_time':
        # params is ignored
        now = time.gmtime(time.time())
        return xmlrpclib.DateTime(now)
    else:
        raise RuntimeError, "Unknown XML-RPC method: %r" % meth

rpc_process() receives the method name and the parameters, and its job is to run the right code for the method, returning a result that will be marshalled into XML-RPC. The body of rpc_process() will therefore usually be an if statement that checks the name of the method, and calls another function to do the actual work. In this case, get_time() is very simple so the two lines of code it requires are simply included in the body of rpc_process().

If the method name doesn't belong to a supported method, execution will fall through to the else clause, which will raise a RuntimeError exception. Quixote's xmlrpc() will catch this exception and report it to the caller as an XML-RPC fault, with the error code set to 1.

As you add additional XML-RPC services, the if statement in rpc_process() will grow more branches. You might be tempted to pass the method name to getattr() to select a method from a module or class. That would work, too, and avoids having a continually growing set of branches, but you should be careful with this and be sure that there are no private methods that a remote caller could access. I generally prefer to have the if... elif... elif... else blocks, for three reasons: 1) adding another branch isn't much work, 2) it's explicit about the supported method names, and 3) there won't be any security holes in doing so.

An alternative approach is to have a dictionary mapping method names to the corresponding functions and restrict the legal method names to the keys of this dictionary:

def echo (*params):
    # Just returns the parameters it's passed
    return params

def get_time ():
    now = time.gmtime(time.time())
    return xmlrpclib.DateTime(now)

methods = {'echo' : echo, 
           'get_time' : get_time}

def rpc_process (meth, params):
    func = methods.get[meth]
    if methods.has_key(meth):
        # params is ignored
        now = time.gmtime(time.time())
        return xmlrpclib.DateTime(now)
    else:
        raise RuntimeError, "Unknown XML-RPC method: %r" % meth

This approach works nicely when there are many methods and the if...elif...else statement would be unworkably long.

$Id: web-services.txt 21603 2003-05-09 19:17:04Z akuchlin $

Quixote-1.2/doc/web-services.txt0000664000225700100400000001325107656777260017511 0ustar naschememems00000000000000Implementing Web Services with Quixote ====================================== This document will show you how to implement Web services using Quixote. An XML-RPC Service ------------------ XML-RPC is the simplest protocol commonly used to expose a Web service. In XML-RPC, there are a few basic data types such as integers, floats, strings, and dates, and a few aggregate types such as arrays and structs. The xmlrpclib module, part of the Python 2.2 standard library and available separately from http://www.pythonware.com/products/xmlrpc/, converts between Python's standard data types and the XML-RPC data types. ============== ===================== XML-RPC Type Python Type or Class -------------- --------------------- int float string list dict xmlrpclib.Boolean xmlrpclib.Binary xmlrpclib.DateTime ============== ===================== Making XML-RPC Calls -------------------- Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server lives at a particular URL, so the first step is to create an xmlrpclib.ServerProxy object pointing at that URL. :: >>> import xmlrpclib >>> s = xmlrpclib.ServerProxy( 'http://www.stuffeddog.com/speller/speller-rpc.cgi') Now you can simply make a call to the spell-checking service offered by this server:: >>> s.speller.spellCheck('my speling isnt gud', {}) [{'word': 'speling', 'suggestions': ['apeling', 'spelding', 'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4}, {'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}] >>> This call results in the following XML being sent:: speller.spellCheck my speling isnt gud Writing a Quixote Service ------------------------- In the quixote.util module, Quixote provides a function, ``xmlrpc(request, func)``, that processes the body of an XML-RPC request. ``request`` is the HTTPRequest object that Quixote passes to every function it invokes. ``func`` is a user-supplied function that receives the name of the XML-RPC method being called and a tuple containing the method's parameters. If there's a bug in the function you supply and it raises an exception, the ``xmlrpc()`` function will catch the exception and return a ``Fault`` to the remote caller. Here's an example of implementing a simple XML-RPC handler with a single method, ``get_time()``, that simply returns the current time. The first task is to expose a URL for accessing the service. :: from quixote.util import xmlrpc _q_exports = ['rpc'] def rpc (request): return xmlrpc(request, rpc_process) def rpc_process (meth, params): ... When the above code is placed in the __init__.py file for the Python package corresponding to your Quixote application, it exposes the URL ``http:///rpc`` as the access point for the XML-RPC service. Next, we need to fill in the contents of the ``rpc_process()`` function:: import time def rpc_process (meth, params): if meth == 'get_time': # params is ignored now = time.gmtime(time.time()) return xmlrpclib.DateTime(now) else: raise RuntimeError, "Unknown XML-RPC method: %r" % meth ``rpc_process()`` receives the method name and the parameters, and its job is to run the right code for the method, returning a result that will be marshalled into XML-RPC. The body of ``rpc_process()`` will therefore usually be an ``if`` statement that checks the name of the method, and calls another function to do the actual work. In this case, ``get_time()`` is very simple so the two lines of code it requires are simply included in the body of ``rpc_process()``. If the method name doesn't belong to a supported method, execution will fall through to the ``else`` clause, which will raise a RuntimeError exception. Quixote's ``xmlrpc()`` will catch this exception and report it to the caller as an XML-RPC fault, with the error code set to 1. As you add additional XML-RPC services, the ``if`` statement in ``rpc_process()`` will grow more branches. You might be tempted to pass the method name to ``getattr()`` to select a method from a module or class. That would work, too, and avoids having a continually growing set of branches, but you should be careful with this and be sure that there are no private methods that a remote caller could access. I generally prefer to have the ``if... elif... elif... else`` blocks, for three reasons: 1) adding another branch isn't much work, 2) it's explicit about the supported method names, and 3) there won't be any security holes in doing so. An alternative approach is to have a dictionary mapping method names to the corresponding functions and restrict the legal method names to the keys of this dictionary:: def echo (*params): # Just returns the parameters it's passed return params def get_time (): now = time.gmtime(time.time()) return xmlrpclib.DateTime(now) methods = {'echo' : echo, 'get_time' : get_time} def rpc_process (meth, params): func = methods.get[meth] if methods.has_key(meth): # params is ignored now = time.gmtime(time.time()) return xmlrpclib.DateTime(now) else: raise RuntimeError, "Unknown XML-RPC method: %r" % meth This approach works nicely when there are many methods and the ``if...elif...else`` statement would be unworkably long. $Id: web-services.txt 21603 2003-05-09 19:17:04Z akuchlin $ Quixote-1.2/doc/widgets.html0000664000225700100400000006604410131012107016652 0ustar naschememems00000000000000 Quixote Widget Classes

Quixote Widget Classes

[This is reference documentation. If you haven't yet read "Lesson 5: widgets" of demo.txt, you should go and do so now. This document also assumes you have a good understanding of HTML forms and form elements. If not, you could do worse than pick up a copy of HTML: The Definitive Guide by Chuck Musciano & Bill Kennedy (O'Reilly). I usually keep it within arm's reach.]

Web forms are built out of form elements: string input, select lists, checkboxes, submit buttons, and so forth. Quixote provides a family of classes for handling these form elements, or widgets, in the quixote.form.widget module. The class hierarchy is:

Widget [A]
|
+--StringWidget
|  |
|  +--PasswordWidget
|  |
|  +--NumberWidget [*] [A]
|     |
|     +-FloatWidget [*]
|     +-IntWidget [*]
|     
+--TextWidget
| 
+--CheckboxWidget
| 
+--SelectWidget [A]
|  |
|  +--SingleSelectWidget
|  |  |
|  |  +-RadiobuttonsWidget
|  |  |
|  |  +-OptionSelectWidget [*]
|  |    
|  +--MultipleSelectWidget
|    
+--SubmitButtonWidget
| 
+--HiddenWidget
|
+--ListWidget [*]

[*] Widget classes that do not correspond exactly with a particular
    HTML form element
[A] Abstract classes 

Widget: the base class

Widget is the abstract base class for the widget hierarchy. It provides the following facilities:

  • widget name (name attribute, set_name() method)
  • widget value (value attribute, set_value() and clear() methods)
  • __str__() and __repr__() methods
  • some facilities for writing composite widget classes

The Widget constructor signature is:

Widget(name : string, value : any = None)
name
the name of the widget. For non-compound widgets (ie. everything in the above class hierarchy), this will be used as the "name" attribute for the main HTML tag that defines the form element.
value
the current value of the form element. The type of 'value' depends on the widget class. Most widget classes support only a single type, eg. StringWidget always deals with strings and IntWidget with integers. The SelectWidget classes are different; see the descriptions below for details.

Common widget methods

The Widget base class also provides a couple of useful methods:

set_name(name:string)
use this to change the widget name supplied to the constructor. Unless you know what you're doing, you should do this before rendering or parsing the widget.
set_value(value:any)
use this to set the widget value; this is the same as supplying a value to the constructor (and the same type rules apply, ie. the type of 'value' depends on the widget class).
clear()
clear the widget's current value. Equivalent to widget.set_value(None).

The following two methods will be used on every widget object you create; if you write your own widget classes, you will almost certainly have to define both of these:

render(request:HTTPRequest) : string
return a chunk of HTML that implements the form element corresponding to this widget.
parse(request:HTTPRequest) : any
extract the form value for this widget from request.form, parse it according to the rules for this widget class, and return the resulting value. The return value depends on the widget class, and will be of the same type as the value passed to the constructor and/or set_value().

StringWidget

Used for short, single-line string input with no validation (ie. any string will be accepted.) Generates an <input type="text"> form element.

Constructor

StringWidget(name : string,
             value : string = None,
             size : int = None,
             maxlength : int = None)
size
used as the size attribute of the generated <input> tag; controls the physical size of the input field.
maxlength
used as the maxlength attribute; controls the maximum amount of input.

Examples

>>> StringWidget("foo", value="hello").render(request)
'<input name="foo" type="text" value="hello">'

>>> StringWidget("foo", size=10, maxlength=20).render(request)
'<input name="foo" type="text" size="10" maxlength="20">'

PasswordWidget

PasswordWidget is identical to StringWidget except for the type of the HTML form element: password instead of text.

TextWidget

Used for multi-line text input. The value is a single string with newlines right where the browser supplied them. (\r\n, if present, is converted to \n.) Generates a <textarea> form element.

Constructor

TextWidget(name : string,
           value : string = None,
           cols : int = None,
           rows : int = None,
           wrap : string = "physical")
cols, rows
number of columns/rows in the textarea
wrap
controls how the browser wraps text and includes newlines in the submitted form value; consult an HTML book for details.

CheckboxWidget

Used for single boolean (on/off) value. The value you supply can be anything, since Python has a boolean interpretation for all values; the value returned by parse() will always be 0 or 1 (but you shouldn't rely on that!). Generates an <input type="checkbox"> form element.

Constructor

CheckboxWidget(name : string,
               value : boolean = false)

Examples

>>> CheckboxWidget("foo", value=0).render(request)
'<input name="foo" type="checkbox" value="yes">'

>>> CheckboxWidget("foo", value="you bet").render(request)
'<input name="foo" type="checkbox" value="yes" checked>'

RadiobuttonsWidget

Used for a set of related radiobuttons, ie. several <input type="radio"> tags with the same name and different values. The set of values are supplied to the constructor as allowed_values, which may be a list of any Python objects (not just strings). The current value must be either None (the default) or one of the values in allowed_values; if you supply a value not in allowed_values, it will be ignored. parse() will return either None or one of the values in allowed_values.

Constructor

RadiobuttonsWidget(name : string,
                   value : any = None,
                   allowed_values : [any] = None,
                   descriptions : [string] = map(str, allowed_values),
                   quote : boolean = true,
                   delim : string = "\n")
allowed_values

specifies how many <input type="radio"> tags to generate and the values for each. Eg. allowed_values=["foo", "bar"] will result in (roughly):

<input type="radio" value="foo">
<input type="radio" value="bar">
descriptions
the text that will actually be shown to the user in the web page that includes this widget. Handy when the elements of allowed_values are too terse, or don't have a meaningful str(), or you want to add some additional cues for the user. If not supplied, map(str, allowed_values) is used, with the exception that None in allowed_values becomes "" (the empty string) in descriptions. If supplied, descriptions must be the same length as allowed_values.
quote
if true (the default), the elements of 'descriptions' will be HTML-quoted (using quixote.html.html_quote()) when the widget is rendered. This is essential if you might have characters like & or < in your descriptions. However, you'll want to set quote to false if you are deliberately including HTML markup in your descriptions.
delim
the delimiter to separate the radiobuttons with when rendering the whole widget. The default ensures that your HTML is readable (by putting each <input> tag on a separate line), and that there is horizontal whitespace between each radiobutton.

Examples

>>> colours = ["red", "green", "blue", "pink"]
>>> widget = RadiobuttonsWidget("foo", allowed_values=colours)
>>> print widget.render(request)
<input name="foo" type="radio" value="0">red</input>
<input name="foo" type="radio" value="1">green</input>
<input name="foo" type="radio" value="2">blue</input>
<input name="foo" type="radio" value="3">pink</input>

(Note that the actual form values, ie. what the browser returns to the server, are always stringified indices into the 'allowed_values' list. This is irrelevant to you, since SingleSelectWidget takes care of converting "1" to 1 and looking up allowed_values[1].)

>>> values = [val1, val2, val3]
>>> descs = ["thing <b>1</b>",
             "thing <b>2</b>",
             "thing <b>3</b>"]
>>> widget = RadiobuttonsWidget("bar",
                 allowed_values=values,
                 descriptions=descs,
                 value=val3,
                 delim="<br>\n",
                 quote=0)
>>> print widget.render(request)
<input name="bar" type="radio" value="0">thing <b>1</b></input><br>
<input name="bar" type="radio" value="1">thing <b>2</b></input><br>
<input name="bar" type="radio" value="2" checked="checked">thing <b>3</b></input>

SingleSelectWidget

Used to select a single value from a list that's too long or ungainly for a set of radiobuttons. (Most browsers implement this as a scrolling list; UNIX versions of Netscape 4.x and earlier used a pop-up menu.) The value can be any Python object; parse() will return either None or one of the values you supply to the constructor as allowed_values. Generates a <select>...</select> tag, with one <option> tag for each element of allowed_values.

Constructor

SingleSelectWidget(name : string,
                   value : any = None,
                   allowed_values : [any] = None,
                   descriptions : [string] = map(str, allowed_values),
                   quote : boolean = true,
                   size : int = None)
allowed_values
determines the set of <option> tags that will go inside the <select> tag; these can be any Python values (not just strings). parse() will return either one of the allowed_values or None. If you supply a value that is not in allowed_values, it will be ignored.
descriptions
(same as RadiobuttonsWidget above)
quote
(same as RadiobuttonsWidget above)
size
corresponds to the size attribute of the <select> tag: ask the browser to show a select list with size items visible. Not always respected by the browser; consult an HTML book.

Examples

>>> widget = SingleSelectWidget("foo",
                                allowed_values=["abc", 123, 5.5])
>>> print widget.render(request)
<select name="foo">
<option value="0">abc
<option value="1">123
<option value="2">5.5
</select>

>>> widget = SingleSelectWidget("bar",
                                value=val2,  
                                allowed_values=[val1, val2, val3],
                                descriptions=["foo", "bar", "foo & bar"],
                                size=3)
>>> print widget.render(request)
<select name="bar" size="3">
<option value="0">foo
<option selected value="1">bar
<option value="2">foo &amp; bar
</select>

MultipleSelectWidget

Used to select multiple values from a list. Everything is just like SingleSelectWidget, except that value can be a list of objects selected from allowed_values (in which case every object in value will initially be selected). Generates a <select multiple>...</select> tag, with one <option> tag for each element of allowed_values.

Constructor

MultipleSelectWidget(name : string,
                     value : any | [any] = None,
                     allowed_values : [any] = None,
                     descriptions : [string] = map(str, allowed_values),
                     quote : boolean = true,
                     size : int = None)

SubmitButtonWidget

Used for generating submit buttons. Note that HTML submit buttons are rather weird, and Quixote preserves this weirdness -- the Widget classes are meant to be a fairly thin wrapper around HTML form elements, after all.

In particular, the widget value for a submit button controls two things: what the user sees in their browser (the text in the button) and what the browser returns as the value for that form element. You can't control the two separately, as you can with radiobuttons or selection widgets.

Also, SubmitButtonWidget is the only widget with an optional name. In many simple forms, all you care about is the fact that the form was submitted -- which submit button the user used doesn't matter.

Constructor

SubmitButtonWidget(name : string = None,
                   value : string = None)
value
the text that will be shown in the user's browser, and the value that will be returned for this form element (widget) if the user selects this submit button.

Examples

>>> SubmitButtonWidget(value="Submit Form").render(request)
'<input type="submit" value="Submit Form">'

HiddenWidget

Used to generate HTML hidden widgets, which can be useful for carrying around non-sensitive application state. (The Quixote form framework uses hidden widgets for form tokens as a measure against cross-site request forgery [CSRF] attacks. So by "sensitive" I mean "information which should not be revealed", rather than "security-related". If you wouldn't put it in a cookie or in email, don't put it in a hidden form element.)

Constructor

HiddenWidget(name : string,
             value : string)

Examples

>>> HiddenWidget("form_id", "2452345135").render(request)
'<input type="hidden" name="form_id" value="2452345135">'

IntWidget

The first derived widget class: this is a subclass of StringWidget specifically for entering integer values. As such, this is the first widget class we've covered that can reject certain user input. (The selection widgets all have to validate their input in case of broken or malicious clients, but they just drop bogus values.) If the user enters a string that Python's built-in int() can't convert to an integer, IntWidget's parse() method raises FormValueError (also defined in the quixote.form.widget module). This exception is handled by Quixote's form framework, but if you're using widget objects on their own, you'll have to handle it yourself.

IntWidget.parse() always returns an integer or None.

Constructor

IntWidget(name : string,
          value : int = None,
          size : int = None,
          maxlength : int = None)

Constructor arguments are as for StringWidget, except that value must be an integer (or None). Note that size and maxlength have exactly the same meaning: they control the size of the input widget and the maximum number of characters of input.

[Examples]

>>> IntWidget("num", value=37, size=5).render(request)
'<input type="string" name="num" value="37" size="5">'

FloatWidget

FloatWidget is identical to IntWidget, except:

  • value must be a float
  • parse() returns a float or None
  • parse() raises FormValueError if the string entered by the user cannot be converted by Python's built-in float() function

OptionSelectWidget

OptionSelectWidget is simply a SingleSelectWidget that uses a bit of Javascript to automatically submit the current form as soon as the user selects a value. This is useful for very simple one-element forms where you don't want to bother with a submit button, or for very complex forms where you need to revamp the user interface based on a user's selection. Your form-processing code could then detect that style of form submission, and regenerate a slightly different form for the user. (Or you could treat it as a full-blown form submission, if the only widget of interest is the OptionSelectWidget.)

For example, if you're asking a user for their address, some of the details will vary depending on which country they're in. You might make the country widget an OptionSelectWidget: if the user selects "Canada", you'll ask them for a province and a postal code; if they select "United States", you ask for a state and a zip code; and so forth. (I don't really recommend a user interface that works this way: you'll spend way too much time getting the details right ["How many states does Australia have again?"], and you're bound to get something wrong -- there are over 200 countries in the world, after all.)

Be warned that since OptionSelectWidget relies on Javascript to work, using it makes immediately makes your application less portable and more fragile. One thing to avoid: form elements with a name of submit, since that masks the Javascript function called by OptionSelectWidget.

$Id: widgets.txt 20217 2003-01-16 20:51:53Z akuchlin $

Quixote-1.2/doc/widgets.txt0000664000225700100400000004224707611615551016551 0ustar naschememems00000000000000Quixote Widget Classes ====================== [This is reference documentation. If you haven't yet read "Lesson 5: widgets" of demo.txt, you should go and do so now. This document also assumes you have a good understanding of HTML forms and form elements. If not, you could do worse than pick up a copy of *HTML: The Definitive Guide* by Chuck Musciano & Bill Kennedy (O'Reilly). I usually keep it within arm's reach.] Web forms are built out of form elements: string input, select lists, checkboxes, submit buttons, and so forth. Quixote provides a family of classes for handling these form elements, or widgets, in the quixote.form.widget module. The class hierarchy is:: Widget [A] | +--StringWidget | | | +--PasswordWidget | | | +--NumberWidget [*] [A] | | | +-FloatWidget [*] | +-IntWidget [*] | +--TextWidget | +--CheckboxWidget | +--SelectWidget [A] | | | +--SingleSelectWidget | | | | | +-RadiobuttonsWidget | | | | | +-OptionSelectWidget [*] | | | +--MultipleSelectWidget | +--SubmitButtonWidget | +--HiddenWidget | +--ListWidget [*] [*] Widget classes that do not correspond exactly with a particular HTML form element [A] Abstract classes Widget: the base class ---------------------- Widget is the abstract base class for the widget hierarchy. It provides the following facilities: * widget name (``name`` attribute, ``set_name()`` method) * widget value (``value`` attribute, ``set_value()`` and ``clear()`` methods) * ``__str__()`` and ``__repr__()`` methods * some facilities for writing composite widget classes The Widget constructor signature is:: Widget(name : string, value : any = None) ``name`` the name of the widget. For non-compound widgets (ie. everything in the above class hierarchy), this will be used as the "name" attribute for the main HTML tag that defines the form element. ``value`` the current value of the form element. The type of 'value' depends on the widget class. Most widget classes support only a single type, eg. StringWidget always deals with strings and IntWidget with integers. The SelectWidget classes are different; see the descriptions below for details. Common widget methods --------------------- The Widget base class also provides a couple of useful methods: ``set_name(name:string)`` use this to change the widget name supplied to the constructor. Unless you know what you're doing, you should do this before rendering or parsing the widget. ``set_value(value:any)`` use this to set the widget value; this is the same as supplying a value to the constructor (and the same type rules apply, ie. the type of 'value' depends on the widget class). ``clear()`` clear the widget's current value. Equivalent to ``widget.set_value(None)``. The following two methods will be used on every widget object you create; if you write your own widget classes, you will almost certainly have to define both of these: ``render(request:HTTPRequest)`` : ``string`` return a chunk of HTML that implements the form element corresponding to this widget. ``parse(request:HTTPRequest)`` : ``any`` extract the form value for this widget from ``request.form``, parse it according to the rules for this widget class, and return the resulting value. The return value depends on the widget class, and will be of the same type as the value passed to the constructor and/or ``set_value()``. StringWidget ------------ Used for short, single-line string input with no validation (ie. any string will be accepted.) Generates an ```` form element. Constructor ~~~~~~~~~~~ :: StringWidget(name : string, value : string = None, size : int = None, maxlength : int = None) ``size`` used as the ``size`` attribute of the generated ```` tag; controls the physical size of the input field. ``maxlength`` used as the ``maxlength`` attribute; controls the maximum amount of input. Examples ~~~~~~~~ :: >>> StringWidget("foo", value="hello").render(request) '' >>> StringWidget("foo", size=10, maxlength=20).render(request) '' PasswordWidget -------------- PasswordWidget is identical to StringWidget except for the type of the HTML form element: ``password`` instead of ``text``. TextWidget ---------- Used for multi-line text input. The value is a single string with newlines right where the browser supplied them. (``\r\n``, if present, is converted to ``\n``.) Generates a ``