Import Quixote 2.4.

This commit is contained in:
Neil Schemenauer 2006-03-15 17:58:21 -07:00
commit d6b73c5768
86 changed files with 18359 additions and 0 deletions

53
ACKS Normal file
View File

@ -0,0 +1,53 @@
Acknowledgements
================
The Quixote developer team would like to thank everybody who
contributed in any way, with code, hints, bug reports, ideas, moral
support, endorsement, or even complaints. Listed in alphabetical
order:
David Ascher
Anton Benard
David Binger
Titus Brown
Oleg Broytmann
Shalabh Chaturvedi
David M. Cooke
Jonathan Corbet
David Creemer
Herman Cuppens
Michael Davidson
Toby Dickenson
Ray Drew
Jim Dukarm
Quinn Dunkan
Robin Dunn
Jon Dyte
David Edwards
Graham Fawcett
Jim Fulton
David Goodger
Neal M. Holtz
Kaweh Kazemi
Shahms E. King
Alexander J. Kozlovsky
A.M. Kuchling (Quixote originator)
Erno Kuusela
Nicola Larosa
Hamish Lawson
Dryice Liu
Roger Masse
Patrick K. O'Brien
Brendan T O'Connor
Ed Overly
Matt Patterson
Paul Richardson
Jeff Rush
Neil Schemenauer (Quixote originator and BD)
Jason Sibre
Gregory P. Smith
Mikhail Sobolev
Daniele Varrazzo
Johann Visagie
Greg Ward (Quixote originator)
The whole gang at the Zope Corporation

1165
CHANGES Normal file

File diff suppressed because it is too large Load Diff

67
LICENSE Normal file
View File

@ -0,0 +1,67 @@
CNRI OPEN SOURCE LICENSE AGREEMENT FOR QUIXOTE-2.4
IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY. BY COPYING,
INSTALLING OR OTHERWISE USING QUIXOTE-2.4 SOFTWARE, YOU ARE DEEMED TO
HAVE AGREED TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS LICENSE
AGREEMENT.
1. This LICENSE AGREEMENT is between Corporation for National Research
Initiatives, having an office at 1895 Preston White Drive, Reston, VA
20191 ("CNRI"), and the Individual or Organization ("Licensee")
copying, installing or otherwise using Quixote-2.4 software in source
or binary form and its associated documentation ("Quixote-2.4").
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Quixote-2.4
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright ©
2005 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Quixote-2.4 alone or in any derivative
version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Quixote-2.4, or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Quixote-2.4.
4. CNRI is making Quixote-2.4 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF QUIXOTE-2.4 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF
QUIXOTE-2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
QUIXOTE-2.4, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE
POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law
provisions. Notwithstanding the foregoing, with regard to derivative
works based on Quixote-2.4 that incorporate non-separable material
that was previously distributed under the GNU General Public License
(GPL), the law of the Commonwealth of Virginia shall govern this
License Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By copying, installing or otherwise using Quixote-2.4, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
global-include *.py *.ptl
include README LICENSE MANIFEST.in MANIFEST CHANGES TODO ACKS
include doc/*.txt doc/*.css doc/Makefile
recursive-include doc *.html
include demo/*.cgi demo/*.conf demo/*.sh
include src/*.c src/Makefile

58
README Normal file
View File

@ -0,0 +1,58 @@
Quixote
=======
Quixote is a framework for developing Web applications in Python.
The target is web applications that are developed and maintained
by Python programmers.
Quixote requires Python 2.3 or greater to run. For installation
instructions, see the doc/INSTALL.txt file.
Quixote includes PTL, the Python Template Language for producing
HTML with Python code. The use of PTL is not required in Quixote
applications, but we recommend it. Details about PTL are provided
in doc/PTL.txt.
If you're switching to a newer version of Quixote from an older
version, please refer to doc/upgrading.txt for explanations of any
backward-incompatible changes.
Installation
=============
See doc/INSTALL.txt.
Documentation
=============
Look in the doc/ directory.
Authors, copyright, and license
===============================
Quixote was originally written by Andrew Kuchling, Neil Schemenauer, and
Greg Ward.
A list of contributors appears in the ACKS file.
Copyright (c) 2000-2005 CNRI.
Quixote is distributed under the CNRI Open Source License Agreement.
See LICENSE for details.
Availability, home page, and mailing lists
==========================================
The Quixote home page is:
http://www.mems-exchange.org/software/quixote/
Discussion of Quixote occurs on the quixote-users mailing list:
http://mail.mems-exchange.org/mailman/listinfo/quixote-users/
To follow development at the most detailed level by seeing every
checkin, join the quixote-checkins mailing list:
http://mail.mems-exchange.org/mailman/listinfo/quixote-checkins/

17
TODO Normal file
View File

@ -0,0 +1,17 @@
* Extend HTTPRequest to support single/multiple-valued fields.
* Make bare return statements inside of PTL templates work as expected.
* Allow __init__.ptl files to be used as package markers. It looks like
something is wrong with the way ihooks handles __init__ modules.
* Logging doesn't work with CGI scripts (something about our
log-opening code depends on how fastcgi.py fiddles stdout).
* For OpenBSD: fcgi.py should catch SIGTERM and, umm, do something.
(Terminate the process?) Otherwise, the FastCGI process can no longer
accept() on its socket. (Reported by Robin Wöhler
<rw@robinwoehler.de>, 2002/08/02.)
* For Mac OS X: _startup() in fcgi.py doesn't work for some reason on
OS X. Figure out why and fix it (or kludge around it).

30
__init__.py Normal file
View File

@ -0,0 +1,30 @@
"""quixote
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/__init__.py $
$Id: __init__.py 27720 2005-12-12 21:13:41Z dbinger $
A highly Pythonic web application framework.
"""
__version__ = '2.4'
# These are frequently needed by Quixote applications.
from quixote.publish import \
get_publisher, get_request, get_response, get_path, redirect, \
get_session, get_session_manager, get_user, get_field, get_cookie
# This is the default charset used by the HTTPRequest, HTTPResponse,
# DefaultLogger, and sendmail components.
DEFAULT_CHARSET = 'iso-8859-1'
def enable_ptl():
"""
Installs the import hooks needed to import PTL modules. This must
be done explicitly because not all Quixote applications need to use
PTL, and import hooks are deep magic that can cause all sorts of
mischief and deeply confuse innocent bystanders. Thus, we avoid
invoking them behind the programmer's back. One known problem is
that, if you use ZODB, you must import ZODB before calling this
function.
"""
import quixote.ptl.install

163
config.py Normal file
View File

@ -0,0 +1,163 @@
"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/config.py $
$Id: config.py 27190 2005-08-11 15:12:15Z dbinger $
Quixote configuration information. This module provides both the
default configuration values, and some code that Quixote uses for
dealing with configuration info. You should not edit the configuration
values in this file, since your edits will be lost if you upgrade to a
newer Quixote version in the future. However, this is the canonical
source of information about Quixote configuration variables, and editing
the defaults here is harmless if you're just playing around and don't
care what happens in the future.
"""
# Note that the default values here are geared towards a production
# environment, preferring security and performance over verbosity and
# debug-ability. If you just want to get a Quixote application
# up-and-running in a production environment, these settings are mostly
# right; all you really need to customize are ERROR_EMAIL, and ERROR_LOG.
# If you need to test/debug/develop a Quixote application, though, you'll
# probably want to also change DISPLAY_EXCEPTIONS.
# Again, you shouldn't edit this file unless you don't care what happens
# in the future (in particular, an upgrade to Quixote would clobber your
# edits).
# E-mail address to send application errors to; None to send no mail at
# all. This should probably be the email address of your web
# administrator.
ERROR_EMAIL = None
#ERROR_EMAIL = 'webmaster@example.com'
# Filename for writing the Quixote access log; None for no access log.
ACCESS_LOG = None
#ACCESS_LOG = "/www/log/quixote-access.log"
# Filename for logging error messages and debugging output; if None,
# everything will be sent to standard error (normally ending up in the
# Web server's error log file.
ERROR_LOG = None
# Controls what's done when uncaught exceptions occur. If set to
# 'plain', the traceback will be returned to the browser in addition
# to being logged, If set to 'html' and the cgitb module is installed,
# a more elaborate display will be returned to the browser, showing
# the local variables and a few lines of context for each level of the
# traceback. If set to None, a generic error display, containing no
# information about the traceback, will be used.
DISPLAY_EXCEPTIONS = None
# Compress large pages using gzip if the client accepts that encoding.
COMPRESS_PAGES = False
# If true, then a cryptographically secure token will be inserted into forms
# as a hidden field. The token will be checked when the form is submitted.
# This prevents cross-site request forgeries (CSRF). It is off by default
# since it doesn't work if sessions are not persistent across requests.
FORM_TOKENS = False
# Session-related variables
# =========================
# Name of the cookie that will hold the session ID string.
SESSION_COOKIE_NAME = "QX_session"
# Domain and path to which the session cookie is restricted. Leaving
# these undefined is fine. Quixote does not have a default "domain"
# option, meaning the session cookie will only be sent to the
# originating server. If you don't set the cookie path, Quixote will
# use your application's root URL (ie. SCRIPT_NAME in a CGI-like
# environment), meaning the session cookie will be sent to all URLs
# controlled by your application, but no other.
SESSION_COOKIE_DOMAIN = None # eg. ".example.com"
SESSION_COOKIE_PATH = None # eg. "/"
# Mail-related variables
# ======================
# These are only used by the quixote.sendmail module, which is
# provided for use by Quixote applications that need to send
# e-mail. This is a common task for web apps, but by no means
# universal.
#
# E-mail addresses can be specified either as a lone string
# containing a bare e-mail address ("addr-spec" in the RFC 822
# grammar), or as an (address, real_name) tuple.
# MAIL_FROM is used as the default for the "From" header and the SMTP
# sender for all outgoing e-mail. If you don't set it, your application
# will crash the first time it tries to send e-mail without an explicit
# "From" address.
MAIL_FROM = None # eg. "webmaster@example.com"
# or ("webmaster@example.com", "Example Webmaster")
# E-mail is sent by connecting to an SMTP server on MAIL_SERVER. This
# server must be configured to relay outgoing e-mail from the current
# host (ie., the host where your Quixote application runs, most likely
# your web server) to anywhere on the Internet. If you don't know what
# this means, talk to your system administrator.
MAIL_SERVER = "localhost"
# If MAIL_DEBUG_ADDR is set, then all e-mail will actually be sent to
# this address rather than the intended recipients. This should be a
# single, bare e-mail address.
MAIL_DEBUG_ADDR = None # eg. "developers@example.com"
# -- End config variables ----------------------------------------------
# (no user serviceable parts after this point)
class Config:
"""Holds all Quixote configuration variables -- see above for
documentation of them. The naming convention is simple:
downcase the above variables to get the names of instance
attributes of this class.
"""
config_vars = [
'error_email',
'access_log',
'display_exceptions',
'error_log',
'compress_pages',
'form_tokens',
'session_cookie_domain',
'session_cookie_name',
'session_cookie_path',
'mail_from',
'mail_server',
'mail_debug_addr',
]
def __init__(self, **kwargs):
self.set_from_dict(globals()) # set defaults
for name, value in kwargs.items():
if name not in self.config_vars:
raise ValueError('unknown config variable %r' % name)
setattr(self, name, value)
def set_from_dict(self, config_vars):
for name, value in config_vars.items():
if name.isupper():
name = name.lower()
if name not in self.config_vars:
raise ValueError('unknown config variable %r' % name)
setattr(self, name, value)
def read_file(self, filename):
"""Read configuration from a file. Any variables already
defined in this Config instance, but not in the file, are
unchanged, so you can use this to build up a configuration
by accumulating data from several config files.
"""
# The config file is Python code -- makes life easy.
config_vars = {}
try:
execfile(filename, config_vars)
except IOError, exc:
if exc.filename is None: # arg! execfile() loses filename
exc.filename = filename
raise exc
self.set_from_dict(config_vars)

10
demo/__init__.py Normal file
View File

@ -0,0 +1,10 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/__init__.py $
$Id: __init__.py 25575 2004-11-11 16:56:44Z nascheme $
"""
from quixote import enable_ptl
from quixote.publish import Publisher
enable_ptl()
def create_publisher():
from quixote.demo.root import RootDirectory
return Publisher(RootDirectory(), display_exceptions='plain')

205
demo/altdemo.py Normal file
View File

@ -0,0 +1,205 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/altdemo.py $
$Id: altdemo.py 26377 2005-03-16 23:32:15Z dbinger $
An alternative Quixote demo. This version is contained in a single module
and does not use PTL. The easiest way to run this demo is to use the
simple HTTP server included with Quixote. For example:
$ server/simple_server.py --factory quixote.demo.altdemo.create_publisher
The server listens on localhost:8080 by default. Debug and error output
will be sent to the terminal.
If you have installed durus, you can run the same demo, except with
persistent sessions stored in a durus database, by running:
$ server/simple_server.py --factory quixote.demo.altdemo.create_durus_publisher
"""
from quixote import get_user, get_session, get_session_manager, get_field
from quixote.directory import Directory
from quixote.html import href, htmltext
from quixote.publish import Publisher
from quixote.session import Session, SessionManager
from quixote.util import dump_request
def format_page(title, content):
request = htmltext(
'<div style="font-size: smaller;background:#eee">'
'<h1>Request:</h1>%s</div>') % dump_request()
return htmltext(
'<html><head><title>%(title)s</title>'
'<style type="text/css">\n'
'body { border: thick solid green; padding: 2em; }\n'
'h1 { font-size: larger; }\n'
'th { background: #aaa; text-align:left; font-size: smaller; }\n'
'td { background: #ccc; font-size: smaller; }\n'
'</style>'
'</head><body>%(content)s%(request)s</body></html>') % locals()
def format_request():
return format_page('Request', dump_request())
def format_link_list(targets):
return htmltext('<ul>%s</ul>') % htmltext('').join([
htmltext('<li>%s</li>') % href(target, target) for target in targets])
class RootDirectory(Directory):
_q_exports = ['', 'login', 'logout']
def _q_index(self):
content = htmltext('')
if not get_user():
content += htmltext('<p>%s</p>' % href('login', 'login'))
else:
content += htmltext(
'<p>Hello, %s.</p>') % get_user()
content += htmltext('<p>%s</p>' % href('logout', 'logout'))
sessions = get_session_manager().items()
if sessions:
sessions.sort()
content += htmltext('<table><tr>'
'<th></th>'
'<th>Session</th>'
'<th>User</th>'
'<th>Number of Requests</th>'
'</tr>')
this_session = get_session()
for index, (id, session) in enumerate(sessions):
if session is this_session:
formatted_id = htmltext(
'<span style="font-weight:bold">%s</span>' % id)
else:
formatted_id = id
content += htmltext(
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%d</td>' % (
index,
formatted_id,
session.user or htmltext("<em>None</em>"),
session.num_requests))
content += htmltext('</table>')
return format_page("Quixote Session Management Demo", content)
def login(self):
content = htmltext('')
if get_field("name"):
session = get_session()
session.set_user(get_field("name")) # This is the important part.
content += htmltext(
'<p>Welcome, %s! Thank you for logging in.</p>') % get_user()
content += href("..", "go back")
else:
content += htmltext(
'<p>Please enter your name here:</p>\n'
'<form method="POST" action="login">'
'<input name="name" />'
'<input type="submit" />'
'</form>')
return format_page("Quixote Session Demo: Login", content)
def logout(self):
if get_user():
content = htmltext('<p>Goodbye, %s.</p>') % get_user()
else:
content = htmltext('<p>That would be redundant.</p>')
content += href("..", "start over")
get_session_manager().expire_session() # This is the important part.
return format_page("Quixote Session Demo: Logout", content)
class DemoSession(Session):
def __init__(self, id):
Session.__init__(self, id)
self.num_requests = 0
def start_request(self):
"""
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)
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
def create_publisher():
return Publisher(RootDirectory(),
session_manager=SessionManager(session_class=DemoSession),
display_exceptions='plain')
try:
# If durus is installed, define a create_durus_publisher() that
# uses a durus database to store persistent sessions.
import os, tempfile
from durus.persistent import Persistent
from durus.persistent_dict import PersistentDict
from durus.file_storage import FileStorage
from durus.connection import Connection
connection = None # set in create_durus_publisher()
class PersistentSession(DemoSession, Persistent):
pass
class PersistentSessionManager(SessionManager, Persistent):
def __init__(self):
sessions = PersistentDict()
SessionManager.__init__(self,
session_class=PersistentSession,
session_mapping=sessions)
def forget_changes(self, session):
print 'abort changes', get_session()
connection.abort()
def commit_changes(self, session):
print 'commit changes', get_session()
connection.commit()
def create_durus_publisher():
global connection
filename = os.path.join(tempfile.gettempdir(), 'quixote-demo.durus')
print 'Opening %r as a Durus database.' % filename
connection = Connection(FileStorage(filename))
root = connection.get_root()
session_manager = root.get('session_manager', None)
if session_manager is None:
session_manager = PersistentSessionManager()
connection.get_root()['session_manager'] = session_manager
connection.commit()
return Publisher(RootDirectory(),
session_manager=session_manager,
display_exceptions='plain')
except ImportError:
pass # durus not installed.

51
demo/extras.ptl Normal file
View File

@ -0,0 +1,51 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/extras.ptl $
$Id: extras.ptl 25683 2004-11-30 19:48:13Z dbinger $
"""
import os
from quixote.directory import Directory, Resolving
from quixote.util import StaticDirectory
from quixote.demo.integers import IntegerUI
class ExtraDirectory(Resolving, Directory):
_q_exports = ["", "form", "src"]
def _q_index [html] (self):
"""
<html>
<head><title>Quixote Demo Extras</title></head>
<body>
<h1>Extras</h1>
<p>
Here are some more features of this demo:
<ul>
<li><a href="12/">12/</a>:
A Python object published through <code>_q_lookup()</code>.
<li><a href="12/factorial">12/factorial</a>:
A method on a published Python object.
<li><a href="form">form</a>:
A Quixote form in action.
<li><a href="src/">src/</a>:
A static directory published through Quixote.
</ul>
"""
def _q_resolve(self, 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':
from quixote.demo.forms import form_demo
return form_demo
def _q_lookup(self, component):
return IntegerUI(component)
def upload(self):
return 'upload demo unfinished'
import quixote
src = StaticDirectory(os.path.dirname(quixote.__file__),
list_directory=True)

120
demo/forms.ptl Normal file
View File

@ -0,0 +1,120 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/forms.ptl $
$Id: forms.ptl 26192 2005-02-18 11:24:43Z dbinger $
Demonstrate the Quixote form class.
"""
import time
from quixote.form import Form, StringWidget, PasswordWidget, \
RadiobuttonsWidget, SingleSelectWidget, MultipleSelectWidget, \
CheckboxWidget, FileWidget
from quixote.form.css import BASIC_FORM_CSS
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)]
def form_demo():
# build form
form = Form(enctype="multipart/form-data") # enctype for file upload
form.add(StringWidget, "name", title="Your Name",
size=20, required=True)
form.add(PasswordWidget, "password", title="Password",
size=20, maxlength=20, required=True)
form.add(CheckboxWidget, "confirm",
title="Are you sure?")
form.add(RadiobuttonsWidget, "color", title="Eye color",
options=['green', 'blue', 'brown', 'other'])
form.add(SingleSelectWidget, "size", title="Size of pizza",
value='medium',
options=[('tiny', 'Tiny (4")'),
('small', 'Small (6")'),
('medium', 'Medium (10")'),
('large', 'Large (14")'),
('enormous', 'Enormous (18")')],
size=1)
# select widgets can use any type of object, no just strings
form.add(MultipleSelectWidget, "toppings", title="Pizza Toppings",
value=[TOPPINGS[0]],
options=TOPPINGS,
size=5)
form.add(FileWidget, "file", title="Your Pizza Specification")
form.add_hidden('time', value=time.time())
form.add_submit("go", "Go!")
def render [html] ():
"""
<html>
<head><title>Quixote Form Demo</title>
<style type="text/css">
%s
</style>
</head>
<body>
<h1>Quixote Form Demo</h1>
""" % BASIC_FORM_CSS
form.render()
"""
</body>
</html>
"""
if not form.is_submitted() or form.has_errors():
return render()
# Could to more error checking, set errors and return render().
# The data has been submitted and verified. Do something interesting
# with it (save it in DB, send email, etc.). We'll just display it.
def success [html] ():
"""
<html>
<head><title>Quixote Form Demo</title></head>
<body>
<h2>Form data:</h2>
<table>
<tr>
<th align=left>Name</th>
<th align=left>Type</th>
<th align=left>Value</th>
</tr>
"""
for widget in form.get_all_widgets():
value = widget.parse()
'<tr>'
' <td>%s</td>' % widget.get_name()
' <td>%s</td>' % getattr(value, str('__class__'),
type(value)).__name__
'<td>'
if value is None:
"<i>None</i>"
elif isinstance(widget, FileWidget):
repr(value)
' (%s bytes)' % len(value.fp.read())
else:
repr(value)
'</td>'
'</tr>'
"""
</table>
</body>
</html>
"""
return success()

61
demo/integers.ptl Normal file
View File

@ -0,0 +1,61 @@
import sys
from quixote import get_response, redirect
from quixote.directory import Directory
from quixote.errors import TraversalError
def fact(n):
f = 1L
while n > 1:
f *= n
n -= 1
return f
class IntegerUI(Directory):
_q_exports = ["", "factorial", "prev", "next"]
def __init__(self, component):
try:
self.n = int(component)
except ValueError, exc:
raise TraversalError(str(exc))
def factorial(self):
if self.n > 10000:
sys.stderr.write("warning: possible denial-of-service attack "
"(request for factorial(%d))\n" % self.n)
get_response().set_content_type("text/plain")
return "%d! = %d\n" % (self.n, fact(self.n))
def _q_index(self):
return """\
<html>
<head><title>The Number %d</title></head>
<body>
You have selected the integer %d.<p>
You can compute its <a href="factorial">factorial</a> (%d!)<p>
Or, you can visit the web page for the
<a href="../%d/">previous</a> or
<a href="../%d/">next</a> integer.<p>
Or, you can use redirects to visit the
<a href="prev">previous</a> or
<a href="next">next</a> 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 <code>prev()</code>
and <code>next()</code> methods for this integer.<p>
</body>
</html>
""" % (self.n, self.n, self.n, self.n-1, self.n+1)
def prev(self):
return redirect("../%d/" % (self.n-1))
def next(self):
return redirect("../%d/" % (self.n+1))

39
demo/mini_demo.py Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
"""
A minimal Quixote demo. If you have the 'quixote' package in your Python
path, you can run it like this:
$ python demo/mini_demo.py
The server listens on localhost:8080 by default. Debug and error output
will be sent to the terminal.
"""
from quixote.publish import Publisher
from quixote.directory import Directory
class RootDirectory(Directory):
_q_exports = ['', 'hello']
def _q_index(self):
return '''<html>
<body>Welcome to the Quixote demo. Here is a
<a href="hello">link</a>.
</body>
</html>
'''
def hello(self):
return '<html><body>Hello world!</body></html>'
def create_publisher():
return Publisher(RootDirectory(),
display_exceptions='plain')
if __name__ == '__main__':
from quixote.server.simple_server import run
print 'creating demo listening on http://localhost:8080/'
run(create_publisher, host='localhost', port=8080)

103
demo/root.ptl Normal file
View File

@ -0,0 +1,103 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/root.ptl $
$Id: root.ptl 25577 2004-11-11 18:58:30Z dbinger $
The root directory for the Quixote demo.
"""
from quixote import get_response
from quixote.directory import Directory
from quixote.errors import PublishError
from quixote.util import dump_request
from quixote.demo.extras import ExtraDirectory
class RootDirectory(Directory):
_q_exports = ["", "simple", "plain", "error", "publish_error", "css",
"dumpreq", "extras", ("favicon.ico", "favicon_ico")]
def _q_index [html] (self):
print "debug message from the index page"
"""
<html>
<head>
<title>Quixote Demo</title>
<link rel="stylesheet" href="css" type="text/css" />
</head>
<body>
<h1>Hello, world!</h1>
<p>To understand what's going on here, be sure to read the
<code>doc/demo.txt</code> file included with Quixote.</p>
<p>
Here are some features of this demo:
<ul>
<li><a href="simple">simple</a>:
A Python function that generates a very simple document.
<li><a href="plain">plain</a>:
A Python function that generates a plain text document.
<li><a href="error">error</a>:
A Python function that raises an exception.
<li><a href="publish_error">publish_error</a>:
A Python function that raises
a <code>PublishError</code> exception. This exception
will be caught by a <code>_q_exception_handler</code> method.
<li><a href="dumpreq">dumpreq</a>:
Print out the contents of the HTTPRequest object.
<li><a href="css">css</a>:
The stylesheet for this document.
<li><a href="extras/">extras/</a>:
Demos of some of Quixote's more advanced features.
</ul>
</p>
</body>
</html>
"""
def simple [html] (self):
'<html><body>Hello!</body></html>'
def plain(self):
get_response().set_content_type("text/plain")
return "This is a plain text document."
def error(self):
raise ValueError, "this is a Python exception"
def publish_error(self):
raise PublishError("Publishing error raised by publish_error")
def dumpreq [html] (self):
"""
<html>
<head><title>HTTPRequest Object</title></head>
<body>
<h1>HTTPRequest Object</h1>
"""
dump_request()
"""
</body>
</html>
"""
def css(self):
get_response().set_content_type("text/css")
# on a real site we would also set the expires header
return 'body { border: thick solid green; padding: 2em; }'
def favicon_ico(self):
response = get_response()
response.set_content_type("image/x-icon")
response.set_expires(days=1)
return FAVICON
extras = ExtraDirectory()
FAVICON = """\
AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAADJZmEA4KilAMJQSwDZko8Aujo0AOi9uwDRfHgA9+npAP///wDw1NIAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAiIiIiIiIiIiIiIiIiIiIiIiIiIiSQDiIiIiIiGRYSIiIiIYkRFiIiIiFQlhk
RYiIiIBAeGRAiIiIFEE2aUQYiIhkSHV4RGiIiGRIiIhEaIiIZEiIiERoiIiUSYiJRJiIiIZDiING
iIiIh2RlEmeIiIiIiBYYiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
""".decode('base64')

112
directory.py Normal file
View File

@ -0,0 +1,112 @@
"""$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/directory.py $
$Id: directory.py 26606 2005-04-18 21:55:37Z rmasse $
Logic for traversing directory objects and generating output.
"""
import quixote
from quixote.errors import TraversalError
class Directory(object):
"""
Instance attributes: none
"""
# A list containing strings or 2-tuples of strings that map external
# names to internal names. Note that the empty string will be
# implicitly mapped to '_q_index'.
_q_exports = []
def _q_translate(self, component):
"""(component : string) -> string | None
Translate a path component into a Python identifier. Returning
None signifies that the component does not exist.
"""
if component in self._q_exports:
if component == '':
return '_q_index' # implicit mapping
else:
return component
else:
# check for an explicit external to internal mapping
for value in self._q_exports:
if isinstance(value, tuple):
if value[0] == component:
return value[1]
else:
return None
def _q_lookup(self, component):
"""(component : string) -> object
Lookup a path component and return the corresponding object (usually
a Directory, a method or a string). Returning None signals that the
component does not exist.
"""
return None
def _q_traverse(self, path):
"""(path: [string]) -> object
Traverse a path and return the result.
"""
assert len(path) > 0
component = path[0]
path = path[1:]
name = self._q_translate(component)
if name is not None:
obj = getattr(self, name)
else:
obj = self._q_lookup(component)
if obj is None:
raise TraversalError(private_msg=('directory %r has no component '
'%r' % (self, component)))
if path:
if hasattr(obj, '_q_traverse'):
return obj._q_traverse(path)
else:
raise TraversalError
elif callable(obj):
return obj()
else:
return obj
def __call__(self):
if "" in self._q_exports and not quixote.get_request().form:
# Fix missing trailing slash.
path = quixote.get_path()
print "Adding slash to: %r " % path
return quixote.redirect(path + "/", permanent=True)
else:
raise TraversalError(private_msg=('directory %r is not '
'callable' % self))
class AccessControlled(object):
"""
A mix-in class that calls the _q_access() method before traversing
into the directory.
"""
def _q_access(self):
pass
def _q_traverse(self, path):
self._q_access()
return super(AccessControlled, self)._q_traverse(path)
class Resolving(object):
"""
A mix-in class that provides the _q_resolve() method. _q_resolve()
is called if a component name appears in the _q_exports list but is
not an instance attribute. _q_resolve is expected to return the
component object.
"""
def _q_resolve(self, name):
return None
def _q_translate(self, component):
name = super(Resolving, self)._q_translate(component)
if name is not None and not hasattr(self, name):
obj = self._q_resolve(name)
setattr(self, name, obj)
return name

37
doc/INSTALL.html Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document">
<div class="section" id="installing-quixote">
<h1><a name="installing-quixote">Installing Quixote</a></h1>
<p>Quixote requires Python 2.3 or later.</p>
<p>If you have a previously installed quixote, we strongly recommend that
you remove it before installing a new one.
First, find out where your old Quixote installation is:</p>
<blockquote>
python -c &quot;import os, quixote; print os.path.dirname(quixote.__file__)&quot;</blockquote>
<p>and then remove away the reported directory. (If the import fails, then
you don't have an existing Quixote installation.)</p>
<p>Now install the new version by running (in the distribution directory),</p>
<blockquote>
python setup.py install</blockquote>
<p>and you're done.</p>
</div>
<div class="section" id="quick-start">
<h1><a name="quick-start">Quick start</a></h1>
<p>In a terminal window, run server/simple_server.py.
In a browser, open <a class="reference" href="http://localhost:8080">http://localhost:8080</a></p>
</div>
<div class="section" id="upgrading-a-quixote-1-application-to-quixote-2">
<h1><a name="upgrading-a-quixote-1-application-to-quixote-2">Upgrading a Quixote 1 application to Quixote 2.</a></h1>
<p>See upgrading.txt for details.</p>
</div>
</div>
</body>
</html>

32
doc/INSTALL.txt Normal file
View File

@ -0,0 +1,32 @@
Installing Quixote
==================
Quixote requires Python 2.3 or later.
If you have a previously installed quixote, we strongly recommend that
you remove it 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.)
Now install the new version by running (in the distribution directory),
python setup.py install
and you're done.
Quick start
===========
In a terminal window, run server/simple_server.py.
In a browser, open http://localhost:8080
Upgrading a Quixote 1 application to Quixote 2.
===============================================
See upgrading.txt for details.

31
doc/Makefile Normal file
View File

@ -0,0 +1,31 @@
#
# 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) .

255
doc/PTL.html Normal file
View File

@ -0,0 +1,255 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>PTL: Python Template Language</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="ptl-python-template-language">
<h1 class="title">PTL: Python Template Language</h1>
<div class="section" id="introduction">
<h1><a name="introduction">Introduction</a></h1>
<p>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.</p>
<p>Specifically, a PTL template is designated by inserting a <tt class="literal"><span class="pre">[plain]</span></tt>
or <tt class="literal"><span class="pre">[html]</span></tt> modifier after the function name. The value of
expressions inside templates are kept, not discarded. If the type is
<tt class="literal"><span class="pre">[html]</span></tt> then non-literal strings are passed through a function that
escapes HTML special characters.</p>
</div>
<div class="section" id="plain-text-templates">
<h1><a name="plain-text-templates">Plain text templates</a></h1>
<p>Here's a sample plain text template:</p>
<pre class="literal-block">
def foo [plain] (x, y = 5):
&quot;This is a chunk of static text.&quot;
greeting = &quot;hello world&quot; # statement, no PTL output
print 'Input values:', x, y
z = x + y
&quot;&quot;&quot;You can plug in variables like x (%s)
in a variety of ways.&quot;&quot;&quot; % x
&quot;\n\n&quot;
&quot;Whitespace is important in generated text.\n&quot;
&quot;z = &quot;; z
&quot;, but y is &quot;
y
&quot;.&quot;
</pre>
<p>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.</p>
<p>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:</p>
<pre class="literal-block">
def foo [plain] (x, y = 5):
&quot;This is a chunk of static text.&quot;
greeting = &quot;hello world&quot; # statement, no PTL output
print 'Input values:', x, y
z = x + y
&quot;&quot;&quot;You can plug in variables like x (%s)
in a variety of ways.&quot;&quot;&quot; % x
</pre>
<p>Calling this template with <tt class="literal"><span class="pre">foo(1,</span> <span class="pre">2)</span></tt> results in the following
string:</p>
<pre class="literal-block">
This is a chunk of static text.You can plug in variables like x (1)
in a variety of ways.
</pre>
<p>Normally when Python evaluates expressions inside functions, it just
discards their values, but in a <tt class="literal"><span class="pre">[plain]</span></tt> PTL template the value is
converted to a string using <tt class="literal"><span class="pre">str()</span></tt> and appended to the template's
return value. There's a single exception to this rule: <tt class="literal"><span class="pre">None</span></tt> 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 <tt class="literal"><span class="pre">None</span></tt>
would require assigning their value to a variable. You'd have to write
<tt class="literal"><span class="pre">dummy</span> <span class="pre">=</span> <span class="pre">list.sort()</span></tt> in PTL code, which would be strange and
confusing.)</p>
<p>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 <tt class="literal"><span class="pre">...text.You</span> <span class="pre">can</span> <span class="pre">...</span></tt> from the example. You'd
have to add an extra space to one of the string literals to correct
this.</p>
<p>The assignment to the <tt class="literal"><span class="pre">greeting</span></tt> local variable is a statement, not an
expression, so it doesn't return a value and produces no output. The
output from the <tt class="literal"><span class="pre">print</span></tt> 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. <tt class="literal"><span class="pre">print</span></tt> should never be used
to generate output returned to the browser, only for adding debugging
traces to a template.</p>
<p>Inside templates, you can use all of Python's control-flow statements:</p>
<pre class="literal-block">
def numbers [plain] (n):
for i in range(n):
i
&quot; &quot; # PTL does not add any whitespace
</pre>
<p>Calling <tt class="literal"><span class="pre">numbers(5)</span></tt> will return the string <tt class="literal"><span class="pre">&quot;1</span> <span class="pre">2</span> <span class="pre">3</span> <span class="pre">4</span> <span class="pre">5</span> <span class="pre">&quot;</span></tt>. You can
also have conditional logic or exception blocks:</p>
<pre class="literal-block">
def international_hello [plain] (language):
if language == &quot;english&quot;:
&quot;hello&quot;
elif language == &quot;french&quot;:
&quot;bonjour&quot;
else:
raise ValueError, &quot;I don't speak %s&quot; % language
</pre>
</div>
<div class="section" id="html-templates">
<h1><a name="html-templates">HTML templates</a></h1>
<p>Since PTL is usually used to generate HTML documents, an <tt class="literal"><span class="pre">[html]</span></tt>
template type has been provided to make generating HTML easier.</p>
<p>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 '&lt;' and '&amp;'. This leads to a class of
security bugs called &quot;cross-site scripting&quot; 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).</p>
<p>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).</p>
<p>Here's how this feature works. PTL defines a class called
<tt class="literal"><span class="pre">htmltext</span></tt> that represents a string that's already been HTML-escaped
and can be safely sent to the client. The function <tt class="literal"><span class="pre">htmlescape(string)</span></tt>
is used to escape data, and it always returns an <tt class="literal"><span class="pre">htmltext</span></tt>
instance. It does nothing if the argument is already <tt class="literal"><span class="pre">htmltext</span></tt>.</p>
<p>If a template function is declared <tt class="literal"><span class="pre">[html]</span></tt> instead of <tt class="literal"><span class="pre">[text]</span></tt>
then two things happen. First, all literal strings in the function
become instances of <tt class="literal"><span class="pre">htmltext</span></tt> instead of Python's <tt class="literal"><span class="pre">str</span></tt>. Second,
the values of expressions are passed through <tt class="literal"><span class="pre">htmlescape()</span></tt> instead
of <tt class="literal"><span class="pre">str()</span></tt>.</p>
<p><tt class="literal"><span class="pre">htmltext</span></tt> type is like the <tt class="literal"><span class="pre">str</span></tt> type except that operations
combining strings and <tt class="literal"><span class="pre">htmltext</span></tt> instances will result in the string
being passed through <tt class="literal"><span class="pre">htmlescape()</span></tt>. For example:</p>
<pre class="literal-block">
&gt;&gt;&gt; from quixote.html import htmltext
&gt;&gt;&gt; htmltext('a') + 'b'
&lt;htmltext 'ab'&gt;
&gt;&gt;&gt; 'a' + htmltext('b')
&lt;htmltext 'ab'&gt;
&gt;&gt;&gt; htmltext('a%s') % 'b'
&lt;htmltext 'ab'&gt;
&gt;&gt;&gt; response = 'green eggs &amp; ham'
&gt;&gt;&gt; htmltext('The response was: %s') % response
&lt;htmltext 'The response was: green eggs &amp;amp; ham'&gt;
</pre>
<p>Note that calling <tt class="literal"><span class="pre">str()</span></tt> strips the <tt class="literal"><span class="pre">htmltext</span></tt> type and should be
avoided since it usually results in characters being escaped more than
once. While <tt class="literal"><span class="pre">htmltext</span></tt> behaves much like a regular string, it is
sometimes necessary to insert a <tt class="literal"><span class="pre">str()</span></tt> inside a template in order
to obtain a genuine string. For example, the <tt class="literal"><span class="pre">re</span></tt> module requires
genuine strings. We have found that explicit calls to <tt class="literal"><span class="pre">str()</span></tt> can
often be avoided by splitting some code out of the template into a
helper function written in regular Python.</p>
<p>It is also recommended that the <tt class="literal"><span class="pre">htmltext</span></tt> constructor be used as
sparingly as possible. The reason is that when using the htmltext
feature of PTL, explicit calls to <tt class="literal"><span class="pre">htmltext</span></tt> become the most likely
source of cross-site scripting holes. Calling <tt class="literal"><span class="pre">htmltext</span></tt> is like
saying &quot;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.&quot;</p>
<p>Note that literal strings in template functions declared with
<tt class="literal"><span class="pre">[html]</span></tt> are htmltext instances, and therefore won't be escaped.
You'll only need to use <tt class="literal"><span class="pre">htmltext</span></tt> when HTML markup comes from
outside the template. For example, if you want to include a file
containing HTML:</p>
<pre class="literal-block">
def output_file [html] ():
'&lt;html&gt;&lt;body&gt;' # does not get escaped
htmltext(open(&quot;myfile.html&quot;).read())
'&lt;/body&gt;&lt;/html&gt;'
</pre>
<p>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 <tt class="literal"><span class="pre">HEAD</span></tt> element:</p>
<pre class="literal-block">
def meta_tags [html] (title, description):
'&lt;title&gt;%s&lt;/title&gt;' % title
'&lt;meta name=&quot;description&quot; content=&quot;%s&quot;&gt;\n' % description
</pre>
<p>There are no calls to <tt class="literal"><span class="pre">htmlescape()</span></tt> at all, but string literals
such as <tt class="literal"><span class="pre">&lt;title&gt;%s&lt;/title&gt;</span></tt> have all be turned into <tt class="literal"><span class="pre">htmltext</span></tt>
instances, so the string variables will be automatically escaped:</p>
<pre class="literal-block">
&gt;&gt;&gt; t.meta_tags('Catalog', 'A catalog of our cool products')
&lt;htmltext '&lt;title&gt;Catalog&lt;/title&gt;
&lt;meta name=&quot;description&quot; content=&quot;A catalog of our cool products&quot;&gt;\n'&gt;
&gt;&gt;&gt; t.meta_tags('Dissertation on &lt;HEAD&gt;',
... 'Discusses the &quot;LINK&quot; and &quot;META&quot; tags')
&lt;htmltext '&lt;title&gt;Dissertation on &amp;lt;HEAD&amp;gt;&lt;/title&gt;
&lt;meta name=&quot;description&quot;
content=&quot;Discusses the &amp;quot;LINK&amp;quot; and &amp;quot;META&amp;quot; tags&quot;&gt;\n'&gt;
&gt;&gt;&gt;
</pre>
<p>Note how the title and description have had HTML-escaping applied to them.
(The output has been manually pretty-printed to be more readable.)</p>
<p>Once you start using <tt class="literal"><span class="pre">htmltext</span></tt> in one of your templates, mixing
plain and HTML templates is tricky because of <tt class="literal"><span class="pre">htmltext</span></tt>'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 <tt class="literal"><span class="pre">str()</span></tt> to convert
<tt class="literal"><span class="pre">htmltext</span></tt> instances to regular Python strings; just be sure the
resulting string isn't HTML-escaped again.</p>
<p>Two implementations of <tt class="literal"><span class="pre">htmltext</span></tt> are provided, one written in pure
Python and a second one implemented as a C extension. Both versions
have seen production use.</p>
</div>
<div class="section" id="ptl-modules">
<h1><a name="ptl-modules">PTL modules</a></h1>
<p>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 <tt class="literal"><span class="pre">.pyc</span></tt>. 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 <tt class="literal"><span class="pre">enable_ptl()</span></tt> function:</p>
<pre class="literal-block">
from quixote import enable_ptl
enable_ptl()
</pre>
<p>(Note: if you're using ZODB, always import ZODB <em>before</em> 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. A similar problem has been reported for
BioPython and win32com.client imports.)</p>
<p>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 <tt class="literal"><span class="pre">foo.ptl</span></tt>, you could then write Python code that did
this:</p>
<pre class="literal-block">
from foo import numbers
def f():
return numbers(10)
</pre>
<p>You may want to keep this little function in your <tt class="literal"><span class="pre">PYTHONSTARTUP</span></tt>
file:</p>
<pre class="literal-block">
def ptl():
try:
import ZODB
except ImportError:
pass
from quixote import enable_ptl
enable_ptl()
</pre>
<p>This is useful if you want to interactively play with a PTL module.</p>
</div>
</div>
</body>
</html>

264
doc/PTL.txt Normal file
View File

@ -0,0 +1,264 @@
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``.
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 ``.pyc``. 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. A similar problem has been reported for
BioPython and win32com.client imports.)
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.

16
doc/default.css Normal file
View File

@ -0,0 +1,16 @@
/*
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 }

207
doc/demo.html Normal file
View File

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Running the Quixote Demos</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="running-the-quixote-demos">
<h1 class="title">Running the Quixote Demos</h1>
<p>Quixote comes with some demonstration applications in the demo directory.
After quixote is installed (see INSTALL.txt for instructions),
you can run the demos using the scripts located in the server directory.</p>
<p>Each server script is written for a specific method of connecting a
quixote publisher to a web server, and you will ultimately want to
choose the one that matches your needs. More information about the
different server scripts may be found in the scripts themselves and in
web-server.txt. To start, though, the easiest way to view the demos
is as follows: in a terminal window, run server/simple_server.py, and
in a browser, open <a class="reference" href="http://localhost:8080">http://localhost:8080</a>.</p>
<p>The simple_server.py script prints a usage message if you run it with
a '--help' command line argument. You can run different demos by
using the '--factory' option to identify a callable that creates the
publisher you want to use. In particular, you might try these demos:</p>
<blockquote>
simple_server.py --factory quixote.demo.mini_demo.create_publisher</blockquote>
<p>or</p>
<blockquote>
simple_server.py --factory quixote.demo.altdemo.create_publisher</blockquote>
<div class="section" id="understanding-the-mini-demo">
<h1><a name="understanding-the-mini-demo">Understanding the mini_demo</a></h1>
<dl>
<dt>Start the mini demo by running the command:</dt>
<dd>simple_server.py --factory quixote.demo.mini_demo.create_publisher</dd>
</dl>
<p>In a browser, load <a class="reference" href="http://localhost:8080">http://localhost:8080</a>. In your browser, you should
see &quot;Welcome ...&quot; page. In your terminal window, you will see a
&quot;localhost - - ...&quot; line for each request. These are access log
messages from the web server.</p>
<p>Look at the source code in demo/mini_demo.py. Near the bottom you
will find the create_publisher() function. The create_publisher()
function creates a Publisher instance whose root directory is an
instance of the RootDirectory class defined just above. When a
request arrives, the Publisher calls the _q_traverse() method on the
root directory. In this case, the RootDirectory is using the standard
_q_traverse() implementation, inherited from Directory.</p>
<p>Look, preferably in another window, at the source code for
_q_traverse() in directory.py. The path argument provided to
_q_traverse() is a list of string components of the path part of the
URL, obtained by splitting the request location at each '/' and
dropping the first element (which is always '') For example, if the
path part of the URL is '/', the path argument to _q_traverse() is
['']. If the path part of the URL is '/a', the path argument to
_q_traverse() is ['a']. If the path part of the URL is '/a/', the
path argument to _q_traverse() is ['a', ''].</p>
<p>Looking at the code of _q_traverse(), observe that it starts by
splitting off the first component of the path and calling
_q_translate() to see if there is a designated attribute name
corresponding to this component. For the '/' page, the component is
'', and _q_translate() returns the attribute name '_q_index'. The
_q_traverse() function goes on to lookup the _q_index method and
return the result of calling it.</p>
<p>Looking back at mini_demo.py, you can see that the RootDirectory class
includes a _q_index() method, and this method does return the HTML for
<a class="reference" href="http://localhost:8080/">http://localhost:8080/</a></p>
<p>As mentioned above, the _q_translate() identifies a &quot;designated&quot;
attribute name for a given component. The default implementation uses
self._q_exports to define this designation. In particular, if the
component is in self._q_exports, then it is returned as the attribute
name, except in the special case of '', which is translated to the
special attribute name '_q_index'.</p>
<p>When you click on the link on the top page, you get
<a class="reference" href="http://localhost:8080/hello">http://localhost:8080/hello</a>. In this case, the path argument to the
_q_traverse() call is ['hello'], and the return value is the result of
calling the hello() method.</p>
<p>Feeling bold? (Just kidding, this won't hurt at all.) Try opening
<a class="reference" href="http://localhost:8080/bogus">http://localhost:8080/bogus</a>. This is what happens when _q_traverse()
raises a TraversalError. A TraversalError is no big deal, but how
does quixote handle more exceptional exceptions? To see, you can
introduce one by editing mini_demo.py. Try inserting the line &quot;raise
'ouch'&quot; into the hello() method. Kill the demo server (Control-c) and
start a new one with the same command as before. Now load the
<a class="reference" href="http://localhost:8080/hello">http://localhost:8080/hello</a> page. You should see a plain text python
traceback followed by some information extracted from the HTTP
request. This information is always printed to the error log on an
exception. Here, it is also displayed in the browser because the
create_publisher() function made a publisher using the 'plain' value
for the display_exceptions keyword argument. If you omit that keyword
argument from the Publisher constructor, the browser will get an
&quot;Internal Server Error&quot; message instead of the full traceback. If you
provide the value 'html', the browser displays a prettier version of
the traceback.</p>
<p>One more thing to try here. Replace your 'raise &quot;ouch&quot;' line in the hello() method with 'print &quot;ouch&quot;'. If you restart the server and load the /hello page,
you will see that print statements go the the error log (in this case, your
terminal window). This can be useful.</p>
</div>
<div class="section" id="understanding-the-root-demo">
<h1><a name="understanding-the-root-demo">Understanding the root demo</a></h1>
<dl>
<dt>Start the root demo by running the command:</dt>
<dd>simple_server.py --factory quixote.demo.create_publisher</dd>
</dl>
<p>In a browser, open <a class="reference" href="http://localhost:8080">http://localhost:8080</a> as before.
Click around at will.</p>
<p>This is the default demo, but it is more complicated than the
mini_demo described above. The create_publisher() function in
quixote.demo.__init__.py creates a publisher whose root directory is
an instance of quixote.demo.root.RootDirectory. Note that the source
code is a file named &quot;root.ptl&quot;. The suffix of &quot;ptl&quot; indicates that
it is a PTL file, and the import must follow a call to
quixote.enable_ptl() or else the source file will not be found or
compiled. The quixote.demo.__init__.py file takes care of that.</p>
<p>Take a look at the source code in root.ptl. You will see code that
looks like regular python, except that some function definitions have
&quot;[html]&quot; between the function name and the parameter list. These
functions are ptl templates. For details about PTL, see the PTL.txt
file.</p>
<p>This RootDirectory class is similar to the one in mini_demo.py, in
that it has a _q_index() method and '' appears in the _q_exports list.
One new feature here is the presence of a tuple in the _q_exports
list. Most of the time, the elements of the _q_exports lists are just
strings that name attributes that should be available as URL
components. This pattern does not work, however, when the particular
URL component you want to use includes characters (like '.') that
can't appear in Python attribute names. To work around these cases,
the _q_exports list may contain tuples such as (&quot;favicon.ico&quot;,
&quot;favicon_ico&quot;) to designate &quot;favicon_ico&quot; as the attribute name
corresponding the the &quot;favicon.ico&quot; URL component.</p>
<p>Looking at the RootDirectoryMethods, including plain(), css() and
favon_ico(), you will see examples where, in addition to returning a
string containing the body of the HTTP response, the function also
makes side-effect modifications to the response object itself, to set
the content type and the expiration time for the response.
Most of the time, these direct modifications to the response are
not needed. When they are, though, the get_response() function
gives you direct access to the response instance.</p>
<p>The RootDirectory here also sets an 'extras' attribute to be an
instance of ExtraDirectory, imported from the quixote.demo.extras
module. Note that 'extras' also appears in the _q_exports list. This
is the ordinary way to extend your URL space through another '/'.
For example, the URL path '/extras/' will result in a call to
the ExtraDirectory instance's _q_index() method.</p>
</div>
<div class="section" id="the-q-lookup-method">
<h1><a name="the-q-lookup-method">The _q_lookup() method</a></h1>
<p>Now take a look at the ExtraDirectory class in extras.ptl. This class
exhibits some more advanced publishing features. If you look back at
the default _q_traverse() implementation (in directory.py), you will
see that the _q_traverse does not give up if _q_translate() returns
None, indicating that the path component has no designated
corresponding attribute name. In this case, _q_traverse() tries
calling self._q_lookup() to see if the object of interest can be found
in a different way. Note that _q_lookup() takes the component as an
argument and must return either (if there is more path to traverse) a
Directory instance, or else (if the component is the last in the path)
a callable or a string.</p>
<p>In this particular case, the ExtrasDirectory._q_lookup() call returns
an instance of IntegerUI (a subclass of Directory). The interest
here, unlike the ExtrasDirectory() instance itself, is created
on-the-fly during the traversal, especially for this particular
component. Try loading <a class="reference" href="http://localhost:8080/extras/12/">http://localhost:8080/extras/12/</a> to see how
this behaves.</p>
<p>Note that the correct URL to get to the IntegerUI(12)._q_index() call
ends with a '/'. This can sometimes be confusing to people who expect
<a class="reference" href="http://localhost:8080/extras/12">http://localhost:8080/extras/12</a> to yield the same page as
<a class="reference" href="http://localhost:8080/extras/12/">http://localhost:8080/extras/12/</a>. If given the path ['extras', '12'],
the default _q_traverse() ends up <em>calling</em> the instance of IntegerUI.
The Directory.__call__() (see directory.py) determines the result: if
no form values were submitted and adding a slash would produce a page,
the call returns the result of calling quixote.redirect(). The
redirect() call here causes the server to issue a permanent redirect
response to the path with the slash added. When this automatic
redirect is used, a message is printed to the error log. If the
conditions for a redirect are not met, the call falls back to raising
a TraversalError. [Note, if you don't like this redirect behavior,
override, replace, or delete Directory.__call__]</p>
<p>The _q_lookup() pattern is useful when you want to allow URL
components that you either don't know or don't want to list in
_q_exports ahead of time.</p>
</div>
<div class="section" id="the-q-resolve-method">
<h1><a name="the-q-resolve-method">The _q_resolve() method</a></h1>
<p>Note that the ExtraDirectory class inherits from Resolving (in
addition to Directory). The Resolving mixin modifies the
_q_traverse() so that, when a component has an attribute name
designated by _q_translate(), but the Directory instance does not
actually <em>have</em> that attribute, the _q_resolve() method is called to
&quot;resolve&quot; the trouble. Typically, the _q_resolve() imports or
constructs what <em>should</em> be the value of the designated attribute.
The modified _q_translate() sets the attribute value so that the
_q_resolve() won't be called again for the same attribute. The
_q_resolve() pattern is useful when you want to delay the work of
constructing the values for exported attributes.</p>
</div>
<div class="section" id="forms">
<h1><a name="forms">Forms</a></h1>
<p>You can't get very far writing web applications without writing forms.
The root demo includes, at <a class="reference" href="http://localhost:8080/extras/form">http://localhost:8080/extras/form</a>, a page
that demonstrates basic usage of the Form class and widgets defined in
the quixote.form package.</p>
<p>$Id: demo.txt 25695 2004-11-30 20:53:44Z dbinger $</p>
</div>
</div>
</body>
</html>

221
doc/demo.txt Normal file
View File

@ -0,0 +1,221 @@
Running the Quixote Demos
=========================
Quixote comes with some demonstration applications in the demo directory.
After quixote is installed (see INSTALL.txt for instructions),
you can run the demos using the scripts located in the server directory.
Each server script is written for a specific method of connecting a
quixote publisher to a web server, and you will ultimately want to
choose the one that matches your needs. More information about the
different server scripts may be found in the scripts themselves and in
web-server.txt. To start, though, the easiest way to view the demos
is as follows: in a terminal window, run server/simple_server.py, and
in a browser, open http://localhost:8080.
The simple_server.py script prints a usage message if you run it with
a '--help' command line argument. You can run different demos by
using the '--factory' option to identify a callable that creates the
publisher you want to use. In particular, you might try these demos:
simple_server.py --factory quixote.demo.mini_demo.create_publisher
or
simple_server.py --factory quixote.demo.altdemo.create_publisher
Understanding the mini_demo
---------------------------
Start the mini demo by running the command:
simple_server.py --factory quixote.demo.mini_demo.create_publisher
In a browser, load http://localhost:8080. In your browser, you should
see "Welcome ..." page. In your terminal window, you will see a
"localhost - - ..." line for each request. These are access log
messages from the web server.
Look at the source code in demo/mini_demo.py. Near the bottom you
will find the create_publisher() function. The create_publisher()
function creates a Publisher instance whose root directory is an
instance of the RootDirectory class defined just above. When a
request arrives, the Publisher calls the _q_traverse() method on the
root directory. In this case, the RootDirectory is using the standard
_q_traverse() implementation, inherited from Directory.
Look, preferably in another window, at the source code for
_q_traverse() in directory.py. The path argument provided to
_q_traverse() is a list of string components of the path part of the
URL, obtained by splitting the request location at each '/' and
dropping the first element (which is always '') For example, if the
path part of the URL is '/', the path argument to _q_traverse() is
['']. If the path part of the URL is '/a', the path argument to
_q_traverse() is ['a']. If the path part of the URL is '/a/', the
path argument to _q_traverse() is ['a', ''].
Looking at the code of _q_traverse(), observe that it starts by
splitting off the first component of the path and calling
_q_translate() to see if there is a designated attribute name
corresponding to this component. For the '/' page, the component is
'', and _q_translate() returns the attribute name '_q_index'. The
_q_traverse() function goes on to lookup the _q_index method and
return the result of calling it.
Looking back at mini_demo.py, you can see that the RootDirectory class
includes a _q_index() method, and this method does return the HTML for
http://localhost:8080/
As mentioned above, the _q_translate() identifies a "designated"
attribute name for a given component. The default implementation uses
self._q_exports to define this designation. In particular, if the
component is in self._q_exports, then it is returned as the attribute
name, except in the special case of '', which is translated to the
special attribute name '_q_index'.
When you click on the link on the top page, you get
http://localhost:8080/hello. In this case, the path argument to the
_q_traverse() call is ['hello'], and the return value is the result of
calling the hello() method.
Feeling bold? (Just kidding, this won't hurt at all.) Try opening
http://localhost:8080/bogus. This is what happens when _q_traverse()
raises a TraversalError. A TraversalError is no big deal, but how
does quixote handle more exceptional exceptions? To see, you can
introduce one by editing mini_demo.py. Try inserting the line "raise
'ouch'" into the hello() method. Kill the demo server (Control-c) and
start a new one with the same command as before. Now load the
http://localhost:8080/hello page. You should see a plain text python
traceback followed by some information extracted from the HTTP
request. This information is always printed to the error log on an
exception. Here, it is also displayed in the browser because the
create_publisher() function made a publisher using the 'plain' value
for the display_exceptions keyword argument. If you omit that keyword
argument from the Publisher constructor, the browser will get an
"Internal Server Error" message instead of the full traceback. If you
provide the value 'html', the browser displays a prettier version of
the traceback.
One more thing to try here. Replace your 'raise "ouch"' line in the hello() method with 'print "ouch"'. If you restart the server and load the /hello page,
you will see that print statements go the the error log (in this case, your
terminal window). This can be useful.
Understanding the root demo
---------------------------
Start the root demo by running the command:
simple_server.py --factory quixote.demo.create_publisher
In a browser, open http://localhost:8080 as before.
Click around at will.
This is the default demo, but it is more complicated than the
mini_demo described above. The create_publisher() function in
quixote.demo.__init__.py creates a publisher whose root directory is
an instance of quixote.demo.root.RootDirectory. Note that the source
code is a file named "root.ptl". The suffix of "ptl" indicates that
it is a PTL file, and the import must follow a call to
quixote.enable_ptl() or else the source file will not be found or
compiled. The quixote.demo.__init__.py file takes care of that.
Take a look at the source code in root.ptl. You will see code that
looks like regular python, except that some function definitions have
"[html]" between the function name and the parameter list. These
functions are ptl templates. For details about PTL, see the PTL.txt
file.
This RootDirectory class is similar to the one in mini_demo.py, in
that it has a _q_index() method and '' appears in the _q_exports list.
One new feature here is the presence of a tuple in the _q_exports
list. Most of the time, the elements of the _q_exports lists are just
strings that name attributes that should be available as URL
components. This pattern does not work, however, when the particular
URL component you want to use includes characters (like '.') that
can't appear in Python attribute names. To work around these cases,
the _q_exports list may contain tuples such as ("favicon.ico",
"favicon_ico") to designate "favicon_ico" as the attribute name
corresponding the the "favicon.ico" URL component.
Looking at the RootDirectoryMethods, including plain(), css() and
favon_ico(), you will see examples where, in addition to returning a
string containing the body of the HTTP response, the function also
makes side-effect modifications to the response object itself, to set
the content type and the expiration time for the response.
Most of the time, these direct modifications to the response are
not needed. When they are, though, the get_response() function
gives you direct access to the response instance.
The RootDirectory here also sets an 'extras' attribute to be an
instance of ExtraDirectory, imported from the quixote.demo.extras
module. Note that 'extras' also appears in the _q_exports list. This
is the ordinary way to extend your URL space through another '/'.
For example, the URL path '/extras/' will result in a call to
the ExtraDirectory instance's _q_index() method.
The _q_lookup() method
----------------------
Now take a look at the ExtraDirectory class in extras.ptl. This class
exhibits some more advanced publishing features. If you look back at
the default _q_traverse() implementation (in directory.py), you will
see that the _q_traverse does not give up if _q_translate() returns
None, indicating that the path component has no designated
corresponding attribute name. In this case, _q_traverse() tries
calling self._q_lookup() to see if the object of interest can be found
in a different way. Note that _q_lookup() takes the component as an
argument and must return either (if there is more path to traverse) a
Directory instance, or else (if the component is the last in the path)
a callable or a string.
In this particular case, the ExtrasDirectory._q_lookup() call returns
an instance of IntegerUI (a subclass of Directory). The interest
here, unlike the ExtrasDirectory() instance itself, is created
on-the-fly during the traversal, especially for this particular
component. Try loading http://localhost:8080/extras/12/ to see how
this behaves.
Note that the correct URL to get to the IntegerUI(12)._q_index() call
ends with a '/'. This can sometimes be confusing to people who expect
http://localhost:8080/extras/12 to yield the same page as
http://localhost:8080/extras/12/. If given the path ['extras', '12'],
the default _q_traverse() ends up *calling* the instance of IntegerUI.
The Directory.__call__() (see directory.py) determines the result: if
no form values were submitted and adding a slash would produce a page,
the call returns the result of calling quixote.redirect(). The
redirect() call here causes the server to issue a permanent redirect
response to the path with the slash added. When this automatic
redirect is used, a message is printed to the error log. If the
conditions for a redirect are not met, the call falls back to raising
a TraversalError. [Note, if you don't like this redirect behavior,
override, replace, or delete Directory.__call__]
The _q_lookup() pattern is useful when you want to allow URL
components that you either don't know or don't want to list in
_q_exports ahead of time.
The _q_resolve() method
-----------------------
Note that the ExtraDirectory class inherits from Resolving (in
addition to Directory). The Resolving mixin modifies the
_q_traverse() so that, when a component has an attribute name
designated by _q_translate(), but the Directory instance does not
actually *have* that attribute, the _q_resolve() method is called to
"resolve" the trouble. Typically, the _q_resolve() imports or
constructs what *should* be the value of the designated attribute.
The modified _q_translate() sets the attribute value so that the
_q_resolve() won't be called again for the same attribute. The
_q_resolve() pattern is useful when you want to delay the work of
constructing the values for exported attributes.
Forms
-----
You can't get very far writing web applications without writing forms.
The root demo includes, at http://localhost:8080/extras/form, a page
that demonstrates basic usage of the Form class and widgets defined in
the quixote.form package.
$Id: demo.txt 25695 2004-11-30 20:53:44Z dbinger $

377
doc/form2conversion.html Normal file
View File

@ -0,0 +1,377 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Converting form1 forms to use the form2 library</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="converting-form1-forms-to-use-the-form2-library">
<h1 class="title">Converting form1 forms to use the form2 library</h1>
<div class="section" id="note">
<h1><a name="note">Note:</a></h1>
<p>The packages names have changed in Quixote 2.</p>
<p>Quixote form1 forms are now in the package quixote.form1.
(In Quixote 1, they were in quixote.form.)</p>
<p>Quixote form2 forms are now in the package quixote.form.
(In Quixote 1, they were in quixote.form2.)</p>
</div>
<div class="section" id="introduction">
<h1><a name="introduction">Introduction</a></h1>
<p>These are some notes and examples for converting Quixote form1 forms,
that is forms derived from <tt class="literal"><span class="pre">quixote.form1.Form</span></tt>, to the newer form2
forms.</p>
<p>Form2 forms are more flexible than their form1 counterparts in that they
do not require you to use the <tt class="literal"><span class="pre">Form</span></tt> 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 <tt class="literal"><span class="pre">process</span></tt>, <tt class="literal"><span class="pre">render</span></tt>,
and <tt class="literal"><span class="pre">action</span></tt> is desirable.</p>
<p>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.</p>
<p>The other end of the spectrum and normally the easiest way to port
form1 forms to form2 is to use the <tt class="literal"><span class="pre">compatibility</span></tt> module provided
in the form2 package. The compatibility module's Form class provides
much of the same highly structured machinery (via a <tt class="literal"><span class="pre">handle</span></tt> master
method) that the form1 framework uses.</p>
</div>
<div class="section" id="converting-form1-forms-using-using-the-compatibility-module">
<h1><a name="converting-form1-forms-using-using-the-compatibility-module">Converting form1 forms using using the compatibility module</a></h1>
<p>Here's the short list of things to do to convert form1 forms to
form2 using compatibility.</p>
<blockquote>
<ol class="arabic">
<li><p class="first">Import the Form base class from <tt class="literal"><span class="pre">quixote.form.compatibility</span></tt>
rather than from quixote.form1.</p>
</li>
<li><p class="first">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.</p>
<blockquote>
<p>Form1 API:</p>
<pre class="literal-block">
self.error['widget_name'] = 'the error message'
</pre>
<p>Form2 API:</p>
<pre class="literal-block">
self.set_error('widget_name', 'the error message')
</pre>
</blockquote>
<p>If you want to find out if the form already has errors, change
the form1 style of direct references to the <tt class="literal"><span class="pre">self.errors</span></tt>
dictionary to a call to the <tt class="literal"><span class="pre">has_errors</span></tt> method.</p>
<blockquote>
<p>Form1 API:</p>
<pre class="literal-block">
if not self.error:
do some more error checking...
</pre>
<p>Form2 API:</p>
<pre class="literal-block">
if not self.has_errors():
do some more error checking...
</pre>
</blockquote>
</li>
<li><p class="first">Form2 select widgets no longer take <tt class="literal"><span class="pre">allowed_values</span></tt> or
<tt class="literal"><span class="pre">descriptions</span></tt> arguments. If you are adding type of form2 select
widget, you must provide the <tt class="literal"><span class="pre">options</span></tt> 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:</p>
<pre class="literal-block">
options: [objects:any]
or
options: [(object:any, description:any)]
or
options: [(object:any, description:any, key:any)]
</pre>
<p>An easy way to construct options if you already have
allowed_values and descriptions is to use the built-in function
<tt class="literal"><span class="pre">zip</span></tt> to define options:</p>
<pre class="literal-block">
options=zip(allowed_values, descriptions)
</pre>
</li>
</ol>
<blockquote>
Note, however, that often it is simpler to to construct the
<tt class="literal"><span class="pre">options</span></tt> list directly.</blockquote>
<ol class="arabic simple" start="4">
<li>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 <tt class="literal"><span class="pre">quixote.form.css</span></tt>.</li>
</ol>
</blockquote>
<p>Here's the longer list of things you may need to tweak in order for
form2 compatibility forms to work with your form1 code.</p>
<blockquote>
<ul>
<li><p class="first"><tt class="literal"><span class="pre">widget_type</span></tt> widget class attribute is gone. This means when
adding widgets other than widgets defined in <tt class="literal"><span class="pre">quixote.form.widget</span></tt>,
you must import the widget class into your module and pass the
widget class as the first argument to the <tt class="literal"><span class="pre">add_widget</span></tt> method
rather than using the <tt class="literal"><span class="pre">widget_type</span></tt> string.</p>
</li>
<li><p class="first">The <tt class="literal"><span class="pre">action_url</span></tt> argument to the form's render method is now
a keyword argument.</p>
</li>
<li><p class="first">If you use <tt class="literal"><span class="pre">OptionSelectWidget</span></tt>, there is no longer a
<tt class="literal"><span class="pre">get_current_option</span></tt> method. You can get the current value
in the normal way.</p>
</li>
<li><p class="first"><tt class="literal"><span class="pre">ListWidget</span></tt> has been renamed to <tt class="literal"><span class="pre">WidgetList</span></tt>.</p>
</li>
<li><p class="first">There is no longer a <tt class="literal"><span class="pre">CollapsibleListWidget</span></tt> class. If you need
this functionality, consider writing a 'deletable composite widget'
to wrap your <tt class="literal"><span class="pre">WidgetList</span></tt> widgets in it:</p>
<pre class="literal-block">
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)
</pre>
</li>
</ul>
</blockquote>
<p>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:</p>
<blockquote>
<ul>
<li><p class="first">In your process method, you don't really need to get a <tt class="literal"><span class="pre">form_data</span></tt>
dictionary by calling <tt class="literal"><span class="pre">Form.process</span></tt> to ensure your widgets are
parsed. Instead, the parsed value of any widget is easy to obtain
using the widget's <tt class="literal"><span class="pre">get_value</span></tt> method or the form's
<tt class="literal"><span class="pre">__getitem__</span></tt> method. So, instead of:</p>
<pre class="literal-block">
form_data = Form.process(self, request)
val = form_data['my_widget']
</pre>
<p>You can use:</p>
<pre class="literal-block">
val = self['my_widget']
</pre>
<p>If the widget may or may not be in the form, you can use <tt class="literal"><span class="pre">get</span></tt>:</p>
<pre class="literal-block">
val = self.get('my_widget')
</pre>
</li>
<li><p class="first">It's normally not necessary to provide the <tt class="literal"><span class="pre">action_url</span></tt> argument
to the form's <tt class="literal"><span class="pre">render</span></tt> method.</p>
</li>
<li><p class="first">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 <tt class="literal"><span class="pre">add</span></tt> (or <tt class="literal"><span class="pre">add_widget</span></tt> in
the compatibility module) can be retrieved using the form's
<tt class="literal"><span class="pre">get_widget</span></tt> method.</p>
</li>
</ul>
</blockquote>
</div>
<div class="section" id="converting-form1-forms-to-form2-by-functional-rewrite">
<h1><a name="converting-form1-forms-to-form2-by-functional-rewrite">Converting form1 forms to form2 by functional rewrite</a></h1>
<p>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.</p>
<p>First the form1 form:</p>
<pre class="literal-block">
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'] &lt; 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) + '/')
</pre>
<p>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:</p>
<pre class="literal-block">
def obj_form(request, obj):
form = Form() # quixote.form.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'] &lt; 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)
</pre>
<p>As you can see in the example, the function still has all of the same
parts of it's form1 equivalent.</p>
<blockquote>
<ol class="arabic simple">
<li>It determines if it's to create a new object or edit an existing one</li>
<li>It adds submit buttons and widgets</li>
<li>It has a function that knows how to render the form</li>
<li>It has a function that knows how to do error processing on the form</li>
<li>It has a function that knows how to register permanent changes to
objects when the form is submitted successfully.</li>
</ol>
</blockquote>
<p>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.</p>
<p>Form2 functional forms do not have the <tt class="literal"><span class="pre">handle</span></tt> master-method that
is called after the form is initialized. Instead, we deal with this
functionality manually. Here are some things that the <tt class="literal"><span class="pre">handle</span></tt>
portion of your form might need to implement illustrated in the
order that often makes sense.</p>
<blockquote>
<ol class="arabic simple">
<li>Get the value of any submit buttons using <tt class="literal"><span class="pre">form.get_submit</span></tt></li>
<li>If the form has not been submitted yet, return <tt class="literal"><span class="pre">render()</span></tt>.</li>
<li>See if the cancel button was pressed, if so return a redirect.</li>
<li>Call your <tt class="literal"><span class="pre">process</span></tt> 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.</li>
<li>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 <tt class="literal"><span class="pre">get_submit</span></tt> is <tt class="literal"><span class="pre">True</span></tt> in this case and if it is,
you want to clear any errors and re-render the form.</li>
<li>If the form has not been submitted or if the form has errors,
you simply want to render the form.</li>
<li>Check for your named submit buttons which you expect for
successful form posting e.g. <tt class="literal"><span class="pre">add</span></tt> or <tt class="literal"><span class="pre">update</span></tt>. If one of
these is pressed, call you action inner function.</li>
<li>Finally, return a redirect to the expected page following a
form submission.</li>
</ol>
</blockquote>
<p>These steps are illustrated by the following snippet of code and to a
large degree in the above functional form2 code example. Often this
<tt class="literal"><span class="pre">handle</span></tt> 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 <tt class="literal"><span class="pre">process</span></tt> function or the call to it:</p>
<pre class="literal-block">
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)
</pre>
</div>
</div>
</body>
</html>

358
doc/form2conversion.txt Normal file
View File

@ -0,0 +1,358 @@
Converting form1 forms to use the form2 library
===============================================
Note:
-----
The packages names have changed in Quixote 2.
Quixote form1 forms are now in the package quixote.form1.
(In Quixote 1, they were in quixote.form.)
Quixote form2 forms are now in the package quixote.form.
(In Quixote 1, they were in quixote.form2.)
Introduction
------------
These are some notes and examples for converting Quixote form1 forms,
that is forms derived from ``quixote.form1.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.form.compatibility``
rather than from quixote.form1.
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.form.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.form.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.form.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)

48
doc/multi-threaded.html Normal file
View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Multi-Threaded Quixote Applications</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="multi-threaded-quixote-applications">
<h1 class="title">Multi-Threaded Quixote Applications</h1>
<p>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.</p>
<p>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:</p>
<pre class="literal-block">
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())
</pre>
<p>Using ThreadedPublisher, you now have one current request per thread,
rather than one for the entire process.</p>
<p>$Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $</p>
</div>
</body>
</html>

39
doc/multi-threaded.txt Normal file
View File

@ -0,0 +1,39 @@
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 $

156
doc/programming.html Normal file
View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Quixote Programming Overview</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="quixote-programming-overview">
<h1 class="title">Quixote Programming Overview</h1>
<p>This document explains how a Quixote application is structured.
The demo.txt file should probably be read before you read this file.
There are three components to a Quixote application:</p>
<ol class="arabic">
<li><p class="first">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.</p>
</li>
<li><p class="first">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.</p>
<p>The most important configuration parameters are:</p>
<blockquote>
<dl>
<dt><tt class="literal"><span class="pre">ERROR_EMAIL</span></tt></dt>
<dd><p class="first last">e-mail address to which errors will be mailed</p>
</dd>
<dt><tt class="literal"><span class="pre">ERROR_LOG</span></tt></dt>
<dd><p class="first last">file to which errors will be logged</p>
</dd>
</dl>
</blockquote>
<p>For development/debugging, you should also set <tt class="literal"><span class="pre">DISPLAY_EXCEPTIONS</span></tt>
true; the default value is false, to favor security over convenience.</p>
</li>
<li><p class="first">Finally, the bulk of the code will be called through a call (by the
Publisher) to the _q_traverse() method of an instance designated as
the <tt class="literal"><span class="pre">root_directory</span></tt>. Normally, the root_directory will be an
instance of the Directory class.</p>
</li>
</ol>
<div class="section" id="driver-script">
<h1><a name="driver-script">Driver script</a></h1>
<p>The driver script is the interface between your web server and Quixote's
&quot;publishing loop&quot;, which in turn is the gateway to your application
code. Thus, there are two things that your Quixote driver script must
do:</p>
<ul class="simple">
<li>create a Quixote publisher -- that is, an instance of the Publisher
class provided by the quixote.publish module -- and customize it for
your application</li>
<li>invoke the publisher's process_request() method as needed to get
responses for one or more requests, writing the responses back
to the client(s).</li>
</ul>
<p>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.</p>
<p>The most important application-specific customization done by the driver
script is to set the root directory of your application.</p>
<p>The quixote.servers package includes driver modules for cgi, fastcgi,
scgi, medusa, twisted, and the simple_server. Each of these modules
includes a <tt class="literal"><span class="pre">run()</span></tt> function that you can use in a driver script that
provides a function to create the publisher that you want. For an example
of this pattern, see the __main__ part of demo/mini_demo.py. You could
run the mini_demo.py with scgi by using the <tt class="literal"><span class="pre">run()</span></tt> function imported
from quixote.server.scgi_server instead of the one from
quixote.server.simple_server. (You would also need your http server
set up to use the scgi server.)</p>
<p>That's almost the simplest possible case -- there's no
application-specific configuration info apart from the root directory.</p>
<p>Getting the driver script to actually run is between you and your web
server. See the web-server.txt document for help.</p>
</div>
<div class="section" id="configuration-file">
<h1><a name="configuration-file">Configuration file</a></h1>
<p>By default, the Publisher uses the configuration information from
quixote/config.py. 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. If you want to
customize any of the configuration variables, your driver script
should provide your customized Config instance as an argument to the
Publisher constructor.</p>
</div>
<div class="section" id="logging">
<h1><a name="logging">Logging</a></h1>
<p>The publisher also accepts an optional <tt class="literal"><span class="pre">logger</span></tt> keyword argument,
that should, if provided, support the same methods as the
default value, an instance of <tt class="literal"><span class="pre">DefaultLogger</span></tt>. Even if you
use the default logger, you can still customize the behavior
by setting configuration values for <tt class="literal"><span class="pre">access_log</span></tt>, <tt class="literal"><span class="pre">error_log</span></tt>, and/or
<tt class="literal"><span class="pre">error_email</span></tt>. These configuration variables are described
more fully in config.py.</p>
<p>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:</p>
<pre class="literal-block">
127.0.0.1 - 2001-10-15 09:48:43
2504 &quot;GET /catalog/ HTTP/1.1&quot;
200 'Opera/6.0 (Linux; U)' 0.10sec
</pre>
<p>This line consists of:</p>
<ul class="simple">
<li>client IP address</li>
<li>current user (according to Quixote session management mechanism,
so this will be &quot;-&quot; unless you're using a session manager that
does authentication)</li>
<li>date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss</li>
<li>process ID of the process serving the request (eg. your CGI/FastCGI
driver script)</li>
<li>the HTTP request line (request method, URI, and protocol)</li>
<li>response status code</li>
<li>HTTP user agent string (specifically, this is
<tt class="literal"><span class="pre">repr(os.environ.get('HTTP_USER_AGENT',</span> <span class="pre">''))</span></tt>)</li>
<li>time to complete the request</li>
</ul>
<p>If no access log is configured (ie., <tt class="literal"><span class="pre">ACCESS_LOG</span></tt> is <tt class="literal"><span class="pre">None</span></tt>), then
Quixote will not do any access logging.</p>
<p>The error log is used for three purposes:</p>
<ul class="simple">
<li>application output to <tt class="literal"><span class="pre">sys.stdout</span></tt> and <tt class="literal"><span class="pre">sys.stderr</span></tt> goes to
Quixote's error log</li>
<li>application tracebacks will be written to Quixote's error log</li>
</ul>
<p>If no error log is configured (with <tt class="literal"><span class="pre">ERROR_LOG</span></tt>), then all output is
redirected 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.</p>
<p>Having stdout redirected to the error log is useful for debugging. You
can just sprinkle <tt class="literal"><span class="pre">print</span></tt> statements into your application and the
output will wind up in the error log.</p>
</div>
<div class="section" id="application-code">
<h1><a name="application-code">Application code</a></h1>
<p>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. You may, optionally, wish to use PTL,
which is simply Python with a novel way of generating function return
values -- see PTL.txt for details.</p>
<p>Quixote's Publisher constructs a request, splits the path into a list
of components, and calls the root directory's _q_traverse() method,
giving the component list as an argument. The _q_traverse() will either
return a value that will become the content of the HTTPResponse, or
else it may raise an Exception. Exceptions are caught by the Publisher
and handled as needed, depending on configuration variables and
whether or not the Exception is an instance of PublisherError.</p>
</div>
</div>
</body>
</html>

157
doc/programming.txt Normal file
View File

@ -0,0 +1,157 @@
Quixote Programming Overview
============================
This document explains how a Quixote application is structured.
The demo.txt file should probably be read before you read this file.
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; the default value is false, to favor security over convenience.
3) Finally, the bulk of the code will be called through a call (by the
Publisher) to the _q_traverse() method of an instance designated as
the ``root_directory``. Normally, the root_directory will be an
instance of the Directory class.
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 publisher's process_request() method as needed to get
responses for one or more requests, writing the responses back
to the client(s).
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 directory of your application.
The quixote.servers package includes driver modules for cgi, fastcgi,
scgi, medusa, twisted, and the simple_server. Each of these modules
includes a ``run()`` function that you can use in a driver script that
provides a function to create the publisher that you want. For an example
of this pattern, see the __main__ part of demo/mini_demo.py. You could
run the mini_demo.py with scgi by using the ``run()`` function imported
from quixote.server.scgi_server instead of the one from
quixote.server.simple_server. (You would also need your http server
set up to use the scgi server.)
That's almost the simplest possible case -- there's no
application-specific configuration info apart from the root directory.
Getting the driver script to actually run is between you and your web
server. See the web-server.txt document for help.
Configuration file
------------------
By default, the Publisher uses the configuration information from
quixote/config.py. 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. If you want to
customize any of the configuration variables, your driver script
should provide your customized Config instance as an argument to the
Publisher constructor.
Logging
-------
The publisher also accepts an optional ``logger`` keyword argument,
that should, if provided, support the same methods as the
default value, an instance of ``DefaultLogger``. Even if you
use the default logger, you can still customize the behavior
by setting configuration values for ``access_log``, ``error_log``, and/or
``error_email``. These configuration variables are described
more fully in config.py.
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 three purposes:
* application output to ``sys.stdout`` and ``sys.stderr`` goes to
Quixote's error log
* application tracebacks will be written to Quixote's error log
If no error log is configured (with ``ERROR_LOG``), then all output is
redirected 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.
Having stdout redirected to the error log is useful for debugging. You
can just sprinkle ``print`` statements into your application and the
output will wind up in the error log.
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. You may, optionally, wish to use PTL,
which is simply Python with a novel way of generating function return
values -- see PTL.txt for details.
Quixote's Publisher constructs a request, splits the path into a list
of components, and calls the root directory's _q_traverse() method,
giving the component list as an argument. The _q_traverse() will either
return a value that will become the content of the HTTPResponse, or
else it may raise an Exception. Exceptions are caught by the Publisher
and handled as needed, depending on configuration variables and
whether or not the Exception is an instance of PublisherError.

307
doc/session-mgmt.html Normal file
View File

@ -0,0 +1,307 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Quixote Session Management</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="quixote-session-management">
<h1 class="title">Quixote Session Management</h1>
<p>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 &quot;session&quot; for each user, ie. where all requests performed by
that user are somehow tied together on the server.</p>
<p>HTTP cookies were invented to address this requirement, and they are
still the best solution for establishing sessions on top of HTTP. Thus,
the session management mechanism that comes with Quixote is
cookie-based. (The most common alternative is to embed the session
identifier in the URL. Since Quixote views the URL as a fundamental
part of the web user interface, a URL-based session management scheme is
considered un-Quixotic.)</p>
<p>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.</p>
<p>In a nutshell, session management with Quixote works like this:</p>
<ul>
<li><p class="first">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. The get_session() function provides uniform
access to the current Session object.</p>
</li>
<li><p class="first">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 <tt class="literal"><span class="pre">QX_session</span></tt> 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.</p>
<pre class="literal-block">
Set-Cookie: QX_session=&quot;928F82A9B8FA92FD&quot;
</pre>
<p>(You can instruct Quixote to specify the domain and path for
URLs to which this cookie should be sent.)</p>
</li>
<li><p class="first">the user agent stores this cookie for future requests</p>
</li>
<li><p class="first">the next time the user agent requests a resource that matches the
cookie's domain and path, it includes the <tt class="literal"><span class="pre">QX_session</span></tt> cookie
previously generated by Quixote in the request headers, eg.:</p>
<pre class="literal-block">
Cookie: QX_session=&quot;928F82A9B8FA92FD&quot;
</pre>
</li>
<li><p class="first">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 made
available, through the get_session() function, as the application
code processes the request.</p>
</li>
</ul>
<p>There are two caveats to keep in mind before proceeding, one major and
one minor:</p>
<ul class="simple">
<li>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 &quot;Session persistence&quot; below for information.</li>
<li>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.</li>
</ul>
<div class="section" id="session-management-demo">
<h1><a name="session-management-demo">Session management demo</a></h1>
<p>There's a simple demo of Quixote's session management in demo/altdemo.py.
If the durus (<a class="reference" href="http://www.mems-exchange.org/software/durus/">http://www.mems-exchange.org/software/durus/</a>) package is
installed, the demo uses a durus database to store sessions, so sessions
will be preserved, even if your are running it with plain cgi.</p>
<p>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 <tt class="literal"><span class="pre">user</span></tt> 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.</p>
<p>Tracking the number of requests is a bit more interesting: from the
DemoSession class in altdemo.py:</p>
<pre class="literal-block">
def __init__ (self, id):
Session.__init__(self, id)
self.num_requests = 0
def start_request (self):
Session.start_request(self)
self.num_requests += 1
</pre>
<p>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. If you want the
value of the user attribute of the current session, just call
get_user(). If you want some other attribute or method Use
get_session() to get the current Session if you need access to other
attributes (such as <tt class="literal"><span class="pre">num_requests</span></tt> in the demo) or methods of the
current Session instance.</p>
<p>Note that the Session class initializes the user attribute to None,
so get_user() will return None if no user has been identified for
this session. Application code can use this to change behavior,
as in the following:</p>
<pre class="literal-block">
if not get_user():
content += htmltext('&lt;p&gt;%s&lt;/p&gt;' % href('login', 'login'))
else:
content += htmltext(
'&lt;p&gt;Hello, %s.&lt;/p&gt;') % get_user()
content += htmltext('&lt;p&gt;%s&lt;/p&gt;' % href('logout', 'logout'))
</pre>
<p>Note that we must quote the user's name, because they are free to enter
anything they please, including special HTML characters like <tt class="literal"><span class="pre">&amp;</span></tt> or
<tt class="literal"><span class="pre">&lt;</span></tt>.</p>
<p>Of course, <tt class="literal"><span class="pre">session.user</span></tt> will never be set if we don't set it
ourselves. The code that processes the login form is just this (from
<tt class="literal"><span class="pre">login()</span></tt> in <tt class="literal"><span class="pre">demo/altdemo.py</span></tt>)</p>
<pre class="literal-block">
if get_field(&quot;name&quot;):
session = get_session()
session.set_user(get_field(&quot;name&quot;)) # This is the important part.
</pre>
<p>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 &quot;user name&quot;. 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.</p>
</div>
<div class="section" id="configuring-the-session-cookie">
<h1><a name="configuring-the-session-cookie">Configuring the session cookie</a></h1>
<p>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</p>
<pre class="literal-block">
SESSION_COOKIE_NAME = &quot;foo_session&quot;
</pre>
<p>and the second application might have</p>
<pre class="literal-block">
SESSION_COOKIE_NAME = &quot;bar_session&quot;
</pre>
<p>Next, you can use <tt class="literal"><span class="pre">SESSION_COOKIE_DOMAIN</span></tt> and <tt class="literal"><span class="pre">SESSION_COOKIE_PATH</span></tt>
to set the cookie attributes that control which requests the cookie is
included with. By default, these are both <tt class="literal"><span class="pre">None</span></tt>, which instructs
Quixote to send the cookie without <tt class="literal"><span class="pre">Domain</span></tt> or <tt class="literal"><span class="pre">Path</span></tt> qualifiers.
For example, if the client requests <tt class="literal"><span class="pre">/foo/bar/</span></tt> 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</p>
<pre class="literal-block">
Set-Cookie: QX_session=&quot;928F82A9B8FA92FD&quot;
</pre>
<p>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 <tt class="literal"><span class="pre">/foo/bar/</span></tt>.</p>
<p>If you want to ensure that your session cookie is included with all
requests to www.example.com, you should set <tt class="literal"><span class="pre">SESSION_COOKIE_PATH</span></tt> in your
config file:</p>
<pre class="literal-block">
SESSION_COOKIE_PATH = &quot;/&quot;
</pre>
<p>which will cause Quixote to set the cookie like this:</p>
<pre class="literal-block">
Set-Cookie: QX_session=&quot;928F82A9B8FA92FD&quot;; Path=&quot;/&quot;
</pre>
<p>which will instruct the browser to include that cookie with <em>all</em>
requests to www.example.com.</p>
<p>However, think carefully about what you set <tt class="literal"><span class="pre">SESSION_COOKIE_PATH</span></tt> to
-- eg. if you set it to &quot;/&quot;, but all of your Quixote code is under &quot;/q/&quot;
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.)</p>
<pre class="literal-block">
SESSION_COOKIE_PATH = &quot;/q/&quot;
</pre>
<p>on such servers. The trailing slash is important; without it, your
session cookies will be sent to URIs like <tt class="literal"><span class="pre">/qux</span></tt> and <tt class="literal"><span class="pre">/qix</span></tt>, even if
you don't control those URIs.</p>
<p>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
<tt class="literal"><span class="pre">SESSION_COOKIE_DOMAIN</span></tt>:</p>
<blockquote>
SESSION_COOKIE_DOMAIN = &quot;.example.com&quot;</blockquote>
<p>Finally, note that the <tt class="literal"><span class="pre">SESSION_COOKIE_*</span></tt> configuration variables
<em>only</em> affect Quixote's session cookie; if you set your own cookies
using the <tt class="literal"><span class="pre">HTTPResponse.set_cookie()</span></tt> method, then the cookie sent to
the client is completely determined by that <tt class="literal"><span class="pre">set_cookie()</span></tt> call.</p>
<p>See RFCs 2109 and 2965 for more information on the rules browsers are
supposed to follow for including cookies with HTTP requests.</p>
</div>
<div class="section" id="writing-the-session-class">
<h1><a name="writing-the-session-class">Writing the session class</a></h1>
<p>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:</p>
<ul class="simple">
<li>initialize the attributes that will be used by your application</li>
<li>override the <tt class="literal"><span class="pre">has_info()</span></tt> method, so the session manager knows when
it must save your session object</li>
</ul>
<p>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 defense 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.</p>
<p>For development/testing work, it's fine to say that your session objects
should always be saved:</p>
<pre class="literal-block">
def has_info (self):
return 1
</pre>
<p>The opposite extreme is to forget to override <tt class="literal"><span class="pre">has_info()</span></tt> altogether,
in which case session management most likely won't work: unless you
tickle the Session object such that the base <tt class="literal"><span class="pre">has_info()</span></tt> 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.</p>
<p>In a real application, you need to think carefully about what data to
store in your sessions, and how <tt class="literal"><span class="pre">has_info()</span></tt> 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 <tt class="literal"><span class="pre">robots.txt</span></tt> will
come along and crawl your entire site, wreaking havoc on your Quixote
application (or the database underlying it).</p>
</div>
<div class="section" id="session-persistence">
<h1><a name="session-persistence">Session persistence</a></h1>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>The second hook (two hooks, really) apply if you use a transactional
persistence mechanism to provide your SessionManager's mapping. The
<tt class="literal"><span class="pre">altdemo.py</span></tt> script does this with Durus, if the durus package is
installed, but you could also use ZODB or a relational database for
this purpose. The hooks make sure that session (and other) changes
get committed or aborted at the appropriate times. SessionManager
provides two methods for you to override: <tt class="literal"><span class="pre">forget_changes()</span></tt> and
<tt class="literal"><span class="pre">commit_changes()</span></tt>. <tt class="literal"><span class="pre">forget_changes()</span></tt> is called by
SessionPublisher whenever a request crashes, ie. whenever your
application raises an exception other than PublishError.
<tt class="literal"><span class="pre">commit_changes()</span></tt> is called for requests that complete
successfully, or that raise a PublishError exception. You'll have to
use your own SessionManager subclass if you need to take advantage of
these hooks for transactional session persistence.</p>
<p>The third available hook is the Session's is_dirty() method. This is
used when your mapping class uses a more primitive storage mechanism,
as, for example, the standard 'shelve' module, which provides a
mapping object on top of a DBM or Berkeley DB file:</p>
<pre class="literal-block">
import shelve
sessions = shelve.open(&quot;/tmp/quixote-sessions&quot;)
session_manager = SessionManager(session_mapping=sessions)
</pre>
<p>If you use one of these relatively simple persistent mapping types,
you'll also need to override <tt class="literal"><span class="pre">is_dirty()</span></tt> in your Session class.
That's in addition to overriding <tt class="literal"><span class="pre">has_info()</span></tt>, which determines if a
session object is <em>ever</em> saved; <tt class="literal"><span class="pre">is_dirty()</span></tt> is only called on
sessions that have already been added to the session mapping, to see
if they need to be &quot;re-added&quot;. 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, you need to store the object
again each time it changes. Thus, <tt class="literal"><span class="pre">is_dirty()</span></tt> 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
<tt class="literal"><span class="pre">has_info()</span></tt> will work -- that just means that once the session has
been written once, it will be re-written on every request.</p>
</div>
</div>
</body>
</html>

323
doc/session-mgmt.txt Normal file
View File

@ -0,0 +1,323 @@
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,
the session management mechanism that comes with Quixote is
cookie-based. (The most common alternative is to embed the session
identifier in the URL. Since Quixote views the URL as a fundamental
part of the web user interface, a URL-based session management scheme is
considered 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.
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. The get_session() function provides uniform
access to the current 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 made
available, through the get_session() function, as the application
code processes 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/altdemo.py.
If the durus (http://www.mems-exchange.org/software/durus/) package is
installed, the demo uses a durus database to store sessions, so sessions
will be preserved, even if you are running it with plain cgi.
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 altdemo.py::
def __init__ (self, id):
Session.__init__(self, id)
self.num_requests = 0
def start_request (self):
Session.start_request(self)
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. If you want the
value of the user attribute of the current session, just call
get_user(). If you want some other attribute or method Use
get_session() to get the current Session if you need access to other
attributes (such as ``num_requests`` in the demo) or methods of the
current Session instance.
Note that the Session class initializes the user attribute to None,
so get_user() will return None if no user has been identified for
this session. Application code can use this to change behavior,
as in the following::
if not get_user():
content += htmltext('<p>%s</p>' % href('login', 'login'))
else:
content += htmltext(
'<p>Hello, %s.</p>') % get_user()
content += htmltext('<p>%s</p>' % href('logout', 'logout'))
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/altdemo.py``) ::
if get_field("name"):
session = get_session()
session.set_user(get_field("name")) # This is the important part.
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 defense 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.
The second hook (two hooks, really) apply if you use a transactional
persistence mechanism to provide your SessionManager's mapping. The
``altdemo.py`` script does this with Durus, if the durus package is
installed, but you could also use ZODB or a relational database for
this purpose. The hooks make sure that session (and other) changes
get committed or aborted at the appropriate times. SessionManager
provides two methods for you to override: ``forget_changes()`` and
``commit_changes()``. ``forget_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. You'll have to
use your own SessionManager subclass if you need to take advantage of
these hooks for transactional session persistence.
The third available hook is the Session's is_dirty() method. This is
used when your mapping class uses a more primitive storage mechanism,
as, for example, 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_manager = SessionManager(session_mapping=sessions)
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, 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.

55
doc/static-files.html Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Examples of serving static files</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="examples-of-serving-static-files">
<h1 class="title">Examples of serving static files</h1>
<p>The <tt class="literal"><span class="pre">quixote.util</span></tt> module includes classes for making files and
directories available as Quixote resources. Here are some examples.</p>
<div class="section" id="publishing-a-single-file">
<h1><a name="publishing-a-single-file">Publishing a Single File</a></h1>
<p>The <tt class="literal"><span class="pre">StaticFile</span></tt> 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 <tt class="literal"><span class="pre">mimetypes.guess_type()</span></tt> function.
The default action is to not follow symbolic links, but this behaviour
can be changed using the <tt class="literal"><span class="pre">follow_symlinks</span></tt> parameter.</p>
<p>The following example publishes a file with the URL <tt class="literal"><span class="pre">.../stylesheet_css</span></tt>:</p>
<pre class="literal-block">
# 'stylesheet_css' must be in the _q_exports list
_q_exports = [ ..., 'stylesheet_css', ...]
stylesheet_css = StaticFile(
&quot;/htdocs/legacy_app/stylesheet.css&quot;,
follow_symlinks=1, mime_type=&quot;text/css&quot;)
</pre>
<p>If you want the URL of the file to have a <tt class="literal"><span class="pre">.css</span></tt> extension, you use
the external to internal name mapping feature of <tt class="literal"><span class="pre">_q_exports</span></tt>. For
example:</p>
<pre class="literal-block">
_q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...]
</pre>
</div>
<div class="section" id="publishing-a-directory">
<h1><a name="publishing-a-directory">Publishing a Directory</a></h1>
<p>Publishing a directory is similar. The <tt class="literal"><span class="pre">StaticDirectory</span></tt> class
makes a complete filesystem directory available. Again, the default
behaviour is to not follow symlinks. You can also request that the
<tt class="literal"><span class="pre">StaticDirectory</span></tt> object cache information about the files in
memory so that it doesn't try to guess the MIME type on every hit.</p>
<p>This example publishes the <tt class="literal"><span class="pre">notes/</span></tt> directory:</p>
<pre class="literal-block">
_q_exports = [ ..., 'notes', ...]
notes = StaticDirectory(&quot;/htdocs/legacy_app/notes&quot;)
</pre>
</div>
</div>
</body>
</html>

51
doc/static-files.txt Normal file
View File

@ -0,0 +1,51 @@
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")

293
doc/upgrading.html Normal file
View File

@ -0,0 +1,293 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Upgrading code from older versions of Quixote</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="upgrading-code-from-older-versions-of-quixote">
<h1 class="title">Upgrading code from older versions of Quixote</h1>
<p>This document lists backward-incompatible changes in Quixote, and
explains how to update application code to work with the newer
version.</p>
<div class="section" id="changes-from-1-0-to-2-0">
<h1><a name="changes-from-1-0-to-2-0">Changes from 1.0 to 2.0</a></h1>
<p>Change any imports you have from quixote.form to be from quixote.form1.</p>
<p>Change any imports you have from quixote.form2 to be from quixote.form.</p>
<p>Replace calls to HTTPRequest.get_form_var() with calls to get_field().</p>
<p>Define a create_publisher() function to get the publisher you need
and figure out how you want to connect it to web server.
See files in demo and server for examples. Note that publish1.py
contains a publisher that works more like the Quixote1 Publisher,
and does not require the changes listed below.</p>
<p>Make every namespace be an instance of quixote.directory.Directory.
Update namespaces that are modules (or in the init.py of a package) by
defining a new class in the module that inherits from Directory and
moving your _q_exports and _q_* functions onto the class. Replace
&quot;request&quot; parameters with &quot;self&quot; parameters on the new methods. If
you have a _q_resolve method, include Resolving in the bases of your
new class.</p>
<p>Remove request from calls to _q_ functions. If request, session,
user, path, or redirect is used in these new methods, replace as
needed with calls to get_request(), get_session(), get_user(),
get_path(), and/or redirect(), imported from quixote.</p>
<p>In every namespace that formerly traversed into a module, import the
new Directory class from the module and create an instance of the
Directory in a variable whose name is the name of the module.</p>
<p>In every namespace with a _q_exports and a _q_index, either add &quot;&quot; to
_q_exports or make sure that _q_lookup handles &quot;&quot; by returning the result
of a call to _q_index.</p>
<p>If your code depends on the Publisher's namespace_stack attribute,
try using quixote.util.get_directory_path() instead. If you need the
namespace stack after the traversal, override Directory._q_traverse()
to call get_directory_path() when the end of the path is reached, and
record the result somewhere for later reference.</p>
<p>If your code depends on _q_exception_handler, override the _q_traverse
on your root namespace or on your own Directory class to catch exceptions
and handle them the way you want. If you just want a general customization
for exception responses, you can change or override
Publisher.format_publish_error().</p>
<p>If your code depended on _q_access, include the AccessControlled with
the bases of your Directory classes as needed.</p>
<p>Provide imports as needed to htmltext, TemplateIO, get_field,
get_request, get_session, get_user, get_path, redirect, ?. You may
find dulcinea/bin/unknown.py useful for identifying missing imports.</p>
<p>Quixote 1's secure_errors configuration variable is not present in Quixote 2.</p>
<p>Form.__init__ no longer has name or attrs keywords. If your existing
code calls Form.__init__ with 'attrs=foo', you'll need to change it to
'<a href="#id1" name="id2"><span class="problematic" id="id2">**</span></a>foo'. Form instances no longer have a name attribute. If your code
looks for form.name, you can find it with form.attrs.get('name').
The Form.__init__ keyword parameter (and attribute) 'action_url' is now
named 'action'.</p>
<div class="system-message" id="id1">
<p class="system-message-title">System Message: <a name="id1">WARNING/2</a> (<tt>upgrading.txt</tt>, line 65); <em><a href="#id2">backlink</a></em></p>
Inline strong start-string without end-string.</div>
<p>The SessionPublisher class is gone. Use the Publisher class instead.
Also, the 'session_mgr' keyword has been renamed to 'session_manager'.</p>
</div>
<div class="section" id="changes-from-0-6-1-to-1-0">
<h1><a name="changes-from-0-6-1-to-1-0">Changes from 0.6.1 to 1.0</a></h1>
<div class="section" id="sessions">
<h2><a name="sessions">Sessions</a></h2>
<p>A leading underscore was removed from the <tt class="literal"><span class="pre">Session</span></tt> attributes
<tt class="literal"><span class="pre">__remote_address</span></tt>, <tt class="literal"><span class="pre">__creation_time</span></tt>, and <tt class="literal"><span class="pre">__access_time</span></tt>. If
you have pickled <tt class="literal"><span class="pre">Session</span></tt> 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.</p>
</div>
</div>
<div class="section" id="changes-from-0-6-to-0-6-1">
<h1><a name="changes-from-0-6-to-0-6-1">Changes from 0.6 to 0.6.1</a></h1>
<div class="section" id="q-exception-handler-now-called-if-exception-while-traversing">
<h2><a name="q-exception-handler-now-called-if-exception-while-traversing"><tt class="literal"><span class="pre">_q_exception_handler</span></tt> now called if exception while traversing</a></h2>
<p><tt class="literal"><span class="pre">_q_exception_handler</span></tt> hooks will now be called if an exception is
raised during the traversal process. Quixote 0.6 had a bug that caused
<tt class="literal"><span class="pre">_q_exception_handler</span></tt> hooks to only be called if an exception was
raised after the traversal completed.</p>
</div>
</div>
<div class="section" id="changes-from-0-5-to-0-6">
<h1><a name="changes-from-0-5-to-0-6">Changes from 0.5 to 0.6</a></h1>
<div class="section" id="q-getname-renamed-to-q-lookup">
<h2><a name="q-getname-renamed-to-q-lookup"><tt class="literal"><span class="pre">_q_getname</span></tt> renamed to <tt class="literal"><span class="pre">_q_lookup</span></tt></a></h2>
<p>The <tt class="literal"><span class="pre">_q_getname</span></tt> special function was renamed to <tt class="literal"><span class="pre">_q_lookup</span></tt>,
because that name gives a clearer impression of the function's
purpose. In 0.6, <tt class="literal"><span class="pre">_q_getname</span></tt> still works but will trigger a
warning.</p>
</div>
<div class="section" id="form-framework-changes">
<h2><a name="form-framework-changes">Form Framework Changes</a></h2>
<p>The <tt class="literal"><span class="pre">quixote.form.form</span></tt> module was changed from a .ptl file to a .py
file. You should delete or move the existing <tt class="literal"><span class="pre">quixote/</span></tt> directory
in <tt class="literal"><span class="pre">site-packages</span></tt> before running <tt class="literal"><span class="pre">setup.py</span></tt>, or at least delete
the old <tt class="literal"><span class="pre">form.ptl</span></tt> and <tt class="literal"><span class="pre">form.ptlc</span></tt> files.</p>
<p>The widget and form classes in the <tt class="literal"><span class="pre">quixote.form</span></tt> package now return
<tt class="literal"><span class="pre">htmltext</span></tt> instances. Applications that use forms and widgets will
likely have to be changed to use the <tt class="literal"><span class="pre">[html]</span></tt> template type to avoid
over-escaping of HTML special characters.</p>
<p>Also, the constructor arguments to <tt class="literal"><span class="pre">SelectWidget</span></tt> and its subclasses have
changed. This only affects applications that use the form framework
located in the <tt class="literal"><span class="pre">quixote.form</span></tt> package.</p>
<p>In Quixote 0.5, the <tt class="literal"><span class="pre">SelectWidget</span></tt> constructor had this signature:</p>
<pre class="literal-block">
def __init__ (self, name, value=None,
allowed_values=None,
descriptions=None,
size=None,
sort=0):
</pre>
<p><tt class="literal"><span class="pre">allowed_values</span></tt> was the list of objects that the user could choose,
and <tt class="literal"><span class="pre">descriptions</span></tt> was a list of strings that would actually be
shown to the user in the generated HTML.</p>
<p>In Quixote 0.6, the signature has changed slightly:</p>
<pre class="literal-block">
def __init__ (self, name, value=None,
allowed_values=None,
descriptions=None,
options=None,
size=None,
sort=0):
</pre>
<p>The <tt class="literal"><span class="pre">quote</span></tt> argument is gone, and the <tt class="literal"><span class="pre">options</span></tt> argument has been
added. If an <tt class="literal"><span class="pre">options</span></tt> argument is provided, <tt class="literal"><span class="pre">allowed_values</span></tt>
and <tt class="literal"><span class="pre">descriptions</span></tt> must not be supplied.</p>
<p>The <tt class="literal"><span class="pre">options</span></tt> argument, if present, must be a list of tuples with
1,2, or 3 elements, of the form <tt class="literal"><span class="pre">(value:any,</span> <span class="pre">description:any,</span>
<span class="pre">key:string)</span></tt>.</p>
<blockquote>
<ul class="simple">
<li><tt class="literal"><span class="pre">value</span></tt> is the object that will be returned if the user chooses
this item, and must always be supplied.</li>
<li><tt class="literal"><span class="pre">description</span></tt> 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 '&amp;' will be converted to '&amp;amp;'.
htmltext instances will be left as they are.</li>
<li>If supplied, <tt class="literal"><span class="pre">key</span></tt> will be used in the value attribute
of the option element (<tt class="literal"><span class="pre">&lt;option</span> <span class="pre">value=&quot;...&quot;&gt;</span></tt>).
If not supplied, keys will be generated; <tt class="literal"><span class="pre">value</span></tt> is checked for a
<tt class="literal"><span class="pre">_p_oid</span></tt> attribute and if present, that string is used;
otherwise the description is used.</li>
</ul>
</blockquote>
<p>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.</p>
</div>
<div class="section" id="file-upload-changes">
<h2><a name="file-upload-changes">File Upload Changes</a></h2>
<p>Quixote 0.6 introduces new support for HTTP upload requests. Any HTTP
request with a Content-Type of &quot;multipart/form-data&quot; -- 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.</p>
<p>Whenever an HTTP request has a Content-Type of &quot;multipart/form-data&quot;,
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.</p>
<p>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:</p>
<dl>
<dt><tt class="literal"><span class="pre">orig_filename</span></tt></dt>
<dd>the filename supplied by the browser.</dd>
<dt><tt class="literal"><span class="pre">base_filename</span></tt></dt>
<dd>a stripped-down version of orig_filename with unsafe characters removed.
This could be used when writing uploaded data to a permanent location.</dd>
<dt><tt class="literal"><span class="pre">tmp_filename</span></tt></dt>
<dd>the path of the temporary file containing the uploaded data for this field.</dd>
</dl>
<p>Consult upload.txt for more information about handling file uploads.</p>
</div>
<div class="section" id="refactored-publisher-class">
<h2><a name="refactored-publisher-class">Refactored <cite>Publisher</cite> Class</a></h2>
<p>Various methods in the <cite>Publisher</cite> class were rearranged. If your
application subclasses Publisher, you may need to change your code
accordingly.</p>
<blockquote>
<ul>
<li><p class="first"><tt class="literal"><span class="pre">parse_request()</span></tt> no longer creates the HTTPRequest object;
instead a new method, <tt class="literal"><span class="pre">create_request()</span></tt>, handles this,
and can be overridden as required.</p>
<p>As a result, the method signature has changed from
<tt class="literal"><span class="pre">parse_request(stdin,</span> <span class="pre">env)</span></tt> to <tt class="literal"><span class="pre">parse_request(request)</span></tt>.</p>
</li>
<li><p class="first">The <tt class="literal"><span class="pre">Publisher.publish()</span></tt> method now catches exceptions raised
by <tt class="literal"><span class="pre">parse_request()</span></tt>.</p>
</li>
</ul>
</blockquote>
</div>
</div>
<div class="section" id="changes-from-0-4-to-0-5">
<h1><a name="changes-from-0-4-to-0-5">Changes from 0.4 to 0.5</a></h1>
<div class="section" id="session-management-changes">
<h2><a name="session-management-changes">Session Management Changes</a></h2>
<p>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.</p>
<div class="section" id="attribute-renamings-and-pickled-objects">
<h3><a name="attribute-renamings-and-pickled-objects">Attribute renamings and pickled objects</a></h3>
<p>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:</p>
<pre class="literal-block">
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
</pre>
<p>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:</p>
<pre class="literal-block">
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 &quot;__&quot; 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
</pre>
<p>It's not pretty, but it worked for us.</p>
</div>
<div class="section" id="cookie-domains-and-paths">
<h3><a name="cookie-domains-and-paths">Cookie domains and paths</a></h3>
<p>The session cookie config variables -- <tt class="literal"><span class="pre">COOKIE_NAME</span></tt>,
<tt class="literal"><span class="pre">COOKIE_DOMAIN</span></tt>, and <tt class="literal"><span class="pre">COOKIE_PATH</span></tt> -- have been renamed to
<tt class="literal"><span class="pre">SESSION_COOKIE_*</span></tt> for clarity.</p>
<p>If you previously set the config variable <tt class="literal"><span class="pre">COOKIE_DOMAIN</span></tt> to the name
of your server, this is most likely no longer necessary -- it's now fine
to leave <tt class="literal"><span class="pre">SESSION_COOKIE_DOMAIN</span></tt> unset (ie. <tt class="literal"><span class="pre">None</span></tt>), which
ultimately means browsers will only include the session cookie in
requests to the same server that sent it to them in the first place.</p>
<p>If you previously set <tt class="literal"><span class="pre">COOKIE_PATH</span></tt>, then you should probably preserve
your setting as <tt class="literal"><span class="pre">SESSION_COOKIE_PATH</span></tt>. The default of <tt class="literal"><span class="pre">None</span></tt> means
that browsers will only send session cookies with requests for URIs
under the URI that originally resulted in the session cookie being sent.
See session-mgmt.txt and RFCs 2109 and 2965.</p>
<p>If you previously set <tt class="literal"><span class="pre">COOKIE_NAME</span></tt>, change it to
<tt class="literal"><span class="pre">SESSION_COOKIE_NAME</span></tt>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

324
doc/upgrading.txt Normal file
View File

@ -0,0 +1,324 @@
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 1.0 to 2.0
-------------------------
Change any imports you have from quixote.form to be from quixote.form1.
Change any imports you have from quixote.form2 to be from quixote.form.
Replace calls to HTTPRequest.get_form_var() with calls to get_field().
Define a create_publisher() function to get the publisher you need
and figure out how you want to connect it to web server.
See files in demo and server for examples. Note that publish1.py
contains a publisher that works more like the Quixote1 Publisher,
and does not require the changes listed below.
Make every namespace be an instance of quixote.directory.Directory.
Update namespaces that are modules (or in the init.py of a package) by
defining a new class in the module that inherits from Directory and
moving your _q_exports and _q_* functions onto the class. Replace
"request" parameters with "self" parameters on the new methods. If
you have a _q_resolve method, include Resolving in the bases of your
new class.
Remove request from calls to _q_ functions. If request, session,
user, path, or redirect is used in these new methods, replace as
needed with calls to get_request(), get_session(), get_user(),
get_path(), and/or redirect(), imported from quixote.
In every namespace that formerly traversed into a module, import the
new Directory class from the module and create an instance of the
Directory in a variable whose name is the name of the module.
In every namespace with a _q_exports and a _q_index, either add "" to
_q_exports or make sure that _q_lookup handles "" by returning the result
of a call to _q_index.
If your code depends on the Publisher's namespace_stack attribute,
try using quixote.util.get_directory_path() instead. If you need the
namespace stack after the traversal, override Directory._q_traverse()
to call get_directory_path() when the end of the path is reached, and
record the result somewhere for later reference.
If your code depends on _q_exception_handler, override the _q_traverse
on your root namespace or on your own Directory class to catch exceptions
and handle them the way you want. If you just want a general customization
for exception responses, you can change or override
Publisher.format_publish_error().
If your code depended on _q_access, include the AccessControlled with
the bases of your Directory classes as needed.
Provide imports as needed to htmltext, TemplateIO, get_field,
get_request, get_session, get_user, get_path, redirect, ?. You may
find dulcinea/bin/unknown.py useful for identifying missing imports.
Quixote 1's secure_errors configuration variable is not present in Quixote 2.
Form.__init__ no longer has name or attrs keywords. If your existing
code calls Form.__init__ with 'attrs=foo', you'll need to change it to
'**foo'. Form instances no longer have a name attribute. If your code
looks for form.name, you can find it with form.attrs.get('name').
The Form.__init__ keyword parameter (and attribute) 'action_url' is now
named 'action'.
The SessionPublisher class is gone. Use the Publisher class instead.
Also, the 'session_mgr' keyword has been renamed to 'session_manager'.
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.
Cookie domains and paths
++++++++++++++++++++++++
The session cookie config variables -- ``COOKIE_NAME``,
``COOKIE_DOMAIN``, and ``COOKIE_PATH`` -- have been renamed to
``SESSION_COOKIE_*`` for clarity.
If you previously set the config variable ``COOKIE_DOMAIN`` to the name
of your server, this is most likely no longer necessary -- it's now fine
to leave ``SESSION_COOKIE_DOMAIN`` unset (ie. ``None``), which
ultimately means browsers will only include the session cookie in
requests to the same server that sent it to them in the first place.
If you previously set ``COOKIE_PATH``, then you should probably preserve
your setting as ``SESSION_COOKIE_PATH``. The default of ``None`` means
that browsers will only send session cookies with requests for URIs
under the URI that originally resulted in the session cookie being sent.
See session-mgmt.txt and RFCs 2109 and 2965.
If you previously set ``COOKIE_NAME``, change it to
``SESSION_COOKIE_NAME``.

229
doc/web-server.html Normal file
View File

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Web Server Configuration for Quixote</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="web-server-configuration-for-quixote">
<h1 class="title">Web Server Configuration for Quixote</h1>
<p>For a simple Quixote installation, there are two things you have to get
right:</p>
<ul class="simple">
<li>installation of the Quixote modules to Python's library (the
trick here is that the <tt class="literal"><span class="pre">quixote</span></tt> package must be visible to the user
that CGI scripts run as, not necessarily to you as an interactive
command-line user)</li>
<li>configuration of your web server to run Quixote driver scripts</li>
</ul>
<p>This document is concerned with the second of these.</p>
<div class="section" id="which-web-servers">
<h1><a name="which-web-servers">Which web servers?</a></h1>
<p>We are only familiar with Apache, and we develop Quixote for use under
Apache. However, Quixote doesn't rely on any Apache-specific tricks;
if you can execute CGI scripts, then you can run Quixote applications
(although they'll run a lot faster with mod_scgi or FastCGI). If you
can redirect arbitrary URLs to a CGI script and preserve parts of the
URL as an add-on to the script name (with <tt class="literal"><span class="pre">PATH_INFO</span></tt>), then you can
run Quixote applications in the ideal manner, ie. with superfluous
implementation details hidden from the user.</p>
</div>
<div class="section" id="which-operating-systems">
<h1><a name="which-operating-systems">Which operating systems?</a></h1>
<p>We are mainly familiar with Unix, and develop and deploy Quixote under
Linux. However, we've had several reports of people using Quixote under
Windows, more-or-less successfully. There are still a few Unix-isms in
the code, but they are being rooted out in favor of portability.</p>
<p>Remember that your system is only as secure as its weakest link.
Quixote can't help you write secure web applications on an inherently
insecure operating system.</p>
</div>
<div class="section" id="basic-cgi-configuration">
<h1><a name="basic-cgi-configuration">Basic CGI configuration</a></h1>
<p>Throughout this document, I'm going to assume that:</p>
<ul class="simple">
<li>CGI scripts live in the <tt class="literal"><span class="pre">/www/cgi-bin</span></tt> directory of your web server,
and have the extension <tt class="literal"><span class="pre">.cgi</span></tt></li>
<li>HTTP requests for <tt class="literal"><span class="pre">/cgi-bin/foo.cgi</span></tt> will result in the execution
of <tt class="literal"><span class="pre">/www/cgi-bin/foo.cgi</span></tt> (for various values of <tt class="literal"><span class="pre">foo</span></tt>)</li>
<li>if the web server is instructed to serve an executable file
<tt class="literal"><span class="pre">bar.cgi</span></tt>, the file is treated as a CGI script</li>
</ul>
<p>With Apache, these configuration directives will do the trick:</p>
<pre class="literal-block">
AddHandler cgi-script .cgi
ScriptAlias /cgi-bin/ /www/cgi-bin/
</pre>
<p>Consult the Apache documentation for other ways of configuring CGI
script execution.</p>
<p>For other web servers, consult your server's documentation.</p>
</div>
<div class="section" id="installing-driver-scripts">
<h1><a name="installing-driver-scripts">Installing driver scripts</a></h1>
<p>Given the above configuration, installing a Quixote driver script is the
same as installing any other CGI script: copy it to <tt class="literal"><span class="pre">/www/cgi-bin</span></tt> (or
whatever). To install the Quixote demo's cgi driver script:</p>
<pre class="literal-block">
cp -p server/cgi_server.py /www/cgi-bin/demo.cgi
</pre>
<p>(The <tt class="literal"><span class="pre">-p</span></tt> option ensures that <tt class="literal"><span class="pre">cp</span></tt> preserves the file mode, so that
it remains executable.)</p>
</div>
<div class="section" id="url-rewriting">
<h1><a name="url-rewriting">URL rewriting</a></h1>
<p>With the above configuration, users need to use URLs like</p>
<pre class="literal-block">
http://www.example.com/cgi-bin/demo.cgi
</pre>
<p>to access the Quixote demo (or other Quixote applications installed in
the same way). This works, but it's ugly and unnecessarily exposes
implementation details.</p>
<p>In our view, it's preferable to give each Quixote application its own
chunk of URL-space -- a &quot;virtual directory&quot; if you like. For example,
you might want</p>
<pre class="literal-block">
http://www.example.com/qdemo
</pre>
<p>to handle the Quixote demo.</p>
<p>With Apache, this is quite easy, as long as mod_rewrite is compiled,
loaded, and enabled. (Building and loading Apache modules is beyond the
scope of this document; consult the Apache documentation.)</p>
<p>To enable the rewrite engine, use the</p>
<pre class="literal-block">
RewriteEngine on
</pre>
<p>directive. If you have virtual hosts, make sure to repeat this for each
<tt class="literal"><span class="pre">&lt;VirtualHost&gt;</span></tt> section of your config file.</p>
<p>The rewrite rule to use in this case is</p>
<pre class="literal-block">
RewriteRule ^/qdemo(/.*) /www/cgi-bin/demo.cgi$1 [last]
</pre>
<p>This is <em>not</em> a redirect; this is all handled with one HTTP
request/response cycle, and the user never sees <tt class="literal"><span class="pre">/cgi-bin/demo.cgi</span></tt> in
a URL.</p>
<p>Note that requests for <tt class="literal"><span class="pre">/qdemo/</span></tt> and <tt class="literal"><span class="pre">/qdemo</span></tt> are <em>not</em> the same; in
particular, with the above rewrite rule, the former will succeed and the
latter will not. (Look at the regex again if you don't believe me:
<tt class="literal"><span class="pre">/qdemo</span></tt> doesn't match the regex, so <tt class="literal"><span class="pre">demo.cgi</span></tt> is never invoked.)</p>
<p>The solution for <tt class="literal"><span class="pre">/qdemo</span></tt> is the same as if it corresponded to a
directory in your document tree: redirect it to <tt class="literal"><span class="pre">/qdemo/</span></tt>. Apache
(and, presumably, other web servers) does this automatically for &quot;real&quot;
directories; however, <tt class="literal"><span class="pre">/qdemo/</span></tt> is just a directory-like chunk of
URL-space, so either you or Quixote have to take care of the redirect.</p>
<p>It's almost certainly faster for you to take care of it in the web
server's configuration. With Apache, simply insert this directive
<em>before</em> the above rewrite rule:</p>
<pre class="literal-block">
RewriteRule ^/qdemo$ /qdemo/ [redirect=permanent]
</pre>
<p>If, for some reason, you are unwilling or unable to instruct your web
server to perform this redirection, Quixote will do it for you.
However, you have to make sure that the <tt class="literal"><span class="pre">/qdemo</span></tt> URL is handled by
Quixote. Change the rewrite rule to:</p>
<pre class="literal-block">
RewriteRule ^/qdemo(/.*)?$ /www/cgi-bin/demo.cgi$1 [last]
</pre>
<p>Now a request for <tt class="literal"><span class="pre">/qdemo</span></tt> will be handled by Quixote, and it will
generate a redirect to <tt class="literal"><span class="pre">/qdemo/</span></tt>. If you're using a CGI driver
script, this will be painfully slow, but it will work.</p>
<p>For redirecting and rewriting URLs with other web servers, consult your
server's documentation.</p>
</div>
<div class="section" id="long-running-processes">
<h1><a name="long-running-processes">Long-running processes</a></h1>
<p>For serious web applications, CGI is unacceptably slow. For a CGI-based
Quixote application, you have to start a Python interpreter, load the
Quixote modules, and load your application's modules before you can
start working. For sophisticated, database-backed applications, you'll
probably have to open a new database connection as well for every hit.</p>
<p>Small wonder so many high-performance alternatives to CGI exist. (The
main advantages of CGI are that it is widely supported and easy to
develop with. Even for large Quixote applications, running in CGI mode
is nice in development because you don't have to kill a long-running
driver script every time the code changes.) Quixote includes support
for mod_scgi and FastCGI.</p>
</div>
<div class="section" id="mod-scgi-configuration">
<h1><a name="mod-scgi-configuration">mod_scgi configuration</a></h1>
<p>SCGI is a CGI replacement written by Neil Schemenauer, one of
Quixote's developers, and is similar to FastCGI but is designed to be
easier to implement. mod_scgi simply forwards requests to an
already-running SCGI server on a different TCP port, and doesn't try
to start or stop processes, leaving that up to the SCGI server.</p>
<p>The SCGI code is available from <a class="reference" href="http://www.mems-exchange.org/software/scgi/">http://www.mems-exchange.org/software/scgi/</a> .</p>
<p>The quixote.server.scgi_server module is a script that
publishes the demo quixote application via SCGI. You can use
it for your application by importing it and calling the <tt class="literal"><span class="pre">run()</span></tt>
function with arguments to run your application, on the port
you choose. Here is an example:</p>
<pre class="literal-block">
#!/usr/bin/python
from quixote.server.scgi_server import run
from quixote.publish import Publisher
from mymodule import MyRootDirectory
def create_my_publisher():
return Publisher(MyRootDirectory())
run(create_my_publisher, port=3001)
</pre>
<p>The following Apache directive will direct requests to an SCGI server
running on port 3001:</p>
<pre class="literal-block">
&lt;Location /&gt;
SCGIServer 127.0.0.1 3001
SCGIHandler On
&lt;/Location&gt;
</pre>
<p>[Note: the mod_scgi module for Apache 2 requires a colon, instead of a
space, between the host and port on the SCGIServer line.]</p>
</div>
<div class="section" id="scgi-through-cgi">
<h1><a name="scgi-through-cgi">SCGI through CGI</a></h1>
<p>Recent releases of the scgi package include cgi2scgi.c, a small program
that offers an extremely convenient way to take advantage of SCGI using
Apache or any web server that supports CGI. To use it, compile the
cgi2scgi.c and install the compiled program as usual for your
webserver. The default SCGI port is 3000, but you can change that
by adding <tt class="literal"><span class="pre">-DPORT=3001</span></tt> (for example) to your compile command.</p>
<p>Although this method requires a new process to be launched for each
request, the process is small and fast, so the performance is
acceptable for many applications.</p>
</div>
<div class="section" id="fastcgi-configuration">
<h1><a name="fastcgi-configuration">FastCGI configuration</a></h1>
<p>If your web server supports FastCGI, you can significantly speed up your
Quixote applications with a simple change to your configuration. You
don't have to change your code at all (unless it makes assumptions about
how many requests are handled by each process). (See
<a class="reference" href="http://www.fastcgi.com/">http://www.fastcgi.com/</a> for more information on FastCGI.)</p>
<p>To use FastCGI with Apache, you'll need to download mod_fastcgi from
<a class="reference" href="http://www.fastcgi.com/">http://www.fastcgi.com/</a> and add it to your Apache installation.</p>
<p>Configuring a FastCGI driver script is best done after reading the fine
documentation for mod_fastcgi at
<a class="reference" href="http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html">http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html</a></p>
<p>However, if you just want to try it with the Quixote demo to see if it
works, add this directive to your Apache configuration:</p>
<pre class="literal-block">
AddHandler fastcgi-script .fcgi
</pre>
<p>and copy server/fastcgi_server.py to demo.fcgi. If you're using a URL
rewrite to map requests for (eg.) <tt class="literal"><span class="pre">/qdemo</span></tt> to
<tt class="literal"><span class="pre">/www/cgi-bin/demo.cgi</span></tt>, be sure to change the rewrite -- it should
now point to <tt class="literal"><span class="pre">/www/cgi-bin/demo.fcgi</span></tt>.</p>
<p>After the first access to <tt class="literal"><span class="pre">demo.fcgi</span></tt> (or <tt class="literal"><span class="pre">/qdemo/</span></tt> with the
modified rewrite rule), the demo should be noticeably faster. You
should also see a <tt class="literal"><span class="pre">demo.fcgi</span></tt> process running if you do <tt class="literal"><span class="pre">ps</span> <span class="pre">-le</span></tt>
(<tt class="literal"><span class="pre">ps</span> <span class="pre">-aux</span></tt> on BSD-ish systems, or maybe <tt class="literal"><span class="pre">ps</span> <span class="pre">aux</span></tt>). (On my 800 MHz
Athlon machine, there are slight but perceptible delays navigating the
Quixote demo in CGI mode. In FastCGI mode, the delay between pages is
no longer perceptible -- navigation is instantaneous.) The larger your
application is, the more code it loads, and the more work it does at
startup, the bigger a win FastCGI will be for you (in comparison to CGI).</p>
</div>
</div>
</body>
</html>

258
doc/web-server.txt Normal file
View File

@ -0,0 +1,258 @@
Web Server Configuration for Quixote
====================================
For a simple Quixote installation, there are two things you have to get
right:
* installation of the Quixote modules to Python's library (the
trick here is that the ``quixote`` package must be visible to the user
that CGI scripts run as, not necessarily to you as an interactive
command-line user)
* configuration of your web server to run Quixote driver scripts
This document is concerned with the second of these.
Which web servers?
------------------
We are only familiar with Apache, and we develop Quixote for use under
Apache. However, Quixote doesn't rely on any Apache-specific tricks;
if you can execute CGI scripts, then you can run Quixote applications
(although they'll run a lot faster with mod_scgi or FastCGI). If you
can redirect arbitrary URLs to a CGI script and preserve parts of the
URL as an add-on to the script name (with ``PATH_INFO``), then you can
run Quixote applications in the ideal manner, ie. with superfluous
implementation details hidden from the user.
Which operating systems?
------------------------
We are mainly familiar with Unix, and develop and deploy Quixote under
Linux. However, we've had several reports of people using Quixote under
Windows, more-or-less successfully. There are still a few Unix-isms in
the code, but they are being rooted out in favor of portability.
Remember that your system is only as secure as its weakest link.
Quixote can't help you write secure web applications on an inherently
insecure operating system.
Basic CGI configuration
-----------------------
Throughout this document, I'm going to assume that:
* CGI scripts live in the ``/www/cgi-bin`` directory of your web server,
and have the extension ``.cgi``
* HTTP requests for ``/cgi-bin/foo.cgi`` will result in the execution
of ``/www/cgi-bin/foo.cgi`` (for various values of ``foo``)
* if the web server is instructed to serve an executable file
``bar.cgi``, the file is treated as a CGI script
With Apache, these configuration directives will do the trick::
AddHandler cgi-script .cgi
ScriptAlias /cgi-bin/ /www/cgi-bin/
Consult the Apache documentation for other ways of configuring CGI
script execution.
For other web servers, consult your server's documentation.
Installing driver scripts
-------------------------
Given the above configuration, installing a Quixote driver script is the
same as installing any other CGI script: copy it to ``/www/cgi-bin`` (or
whatever). To install the Quixote demo's cgi driver script::
cp -p server/cgi_server.py /www/cgi-bin/demo.cgi
(The ``-p`` option ensures that ``cp`` preserves the file mode, so that
it remains executable.)
URL rewriting
-------------
With the above configuration, users need to use URLs like ::
http://www.example.com/cgi-bin/demo.cgi
to access the Quixote demo (or other Quixote applications installed in
the same way). This works, but it's ugly and unnecessarily exposes
implementation details.
In our view, it's preferable to give each Quixote application its own
chunk of URL-space -- a "virtual directory" if you like. For example,
you might want ::
http://www.example.com/qdemo
to handle the Quixote demo.
With Apache, this is quite easy, as long as mod_rewrite is compiled,
loaded, and enabled. (Building and loading Apache modules is beyond the
scope of this document; consult the Apache documentation.)
To enable the rewrite engine, use the ::
RewriteEngine on
directive. If you have virtual hosts, make sure to repeat this for each
``<VirtualHost>`` section of your config file.
The rewrite rule to use in this case is ::
RewriteRule ^/qdemo(/.*) /www/cgi-bin/demo.cgi$1 [last]
This is *not* a redirect; this is all handled with one HTTP
request/response cycle, and the user never sees ``/cgi-bin/demo.cgi`` in
a URL.
Note that requests for ``/qdemo/`` and ``/qdemo`` are *not* the same; in
particular, with the above rewrite rule, the former will succeed and the
latter will not. (Look at the regex again if you don't believe me:
``/qdemo`` doesn't match the regex, so ``demo.cgi`` is never invoked.)
The solution for ``/qdemo`` is the same as if it corresponded to a
directory in your document tree: redirect it to ``/qdemo/``. Apache
(and, presumably, other web servers) does this automatically for "real"
directories; however, ``/qdemo/`` is just a directory-like chunk of
URL-space, so either you or Quixote have to take care of the redirect.
It's almost certainly faster for you to take care of it in the web
server's configuration. With Apache, simply insert this directive
*before* the above rewrite rule::
RewriteRule ^/qdemo$ /qdemo/ [redirect=permanent]
If, for some reason, you are unwilling or unable to instruct your web
server to perform this redirection, Quixote will do it for you.
However, you have to make sure that the ``/qdemo`` URL is handled by
Quixote. Change the rewrite rule to::
RewriteRule ^/qdemo(/.*)?$ /www/cgi-bin/demo.cgi$1 [last]
Now a request for ``/qdemo`` will be handled by Quixote, and it will
generate a redirect to ``/qdemo/``. If you're using a CGI driver
script, this will be painfully slow, but it will work.
For redirecting and rewriting URLs with other web servers, consult your
server's documentation.
Long-running processes
----------------------
For serious web applications, CGI is unacceptably slow. For a CGI-based
Quixote application, you have to start a Python interpreter, load the
Quixote modules, and load your application's modules before you can
start working. For sophisticated, database-backed applications, you'll
probably have to open a new database connection as well for every hit.
Small wonder so many high-performance alternatives to CGI exist. (The
main advantages of CGI are that it is widely supported and easy to
develop with. Even for large Quixote applications, running in CGI mode
is nice in development because you don't have to kill a long-running
driver script every time the code changes.) Quixote includes support
for mod_scgi and FastCGI.
mod_scgi configuration
----------------------
SCGI is a CGI replacement written by Neil Schemenauer, one of
Quixote's developers, and is similar to FastCGI but is designed to be
easier to implement. mod_scgi simply forwards requests to an
already-running SCGI server on a different TCP port, and doesn't try
to start or stop processes, leaving that up to the SCGI server.
The SCGI code is available from http://www.mems-exchange.org/software/scgi/ .
The quixote.server.scgi_server module is a script that
publishes the demo quixote application via SCGI. You can use
it for your application by importing it and calling the ``run()``
function with arguments to run your application, on the port
you choose. Here is an example::
#!/usr/bin/python
from quixote.server.scgi_server import run
from quixote.publish import Publisher
from mymodule import MyRootDirectory
def create_my_publisher():
return Publisher(MyRootDirectory())
run(create_my_publisher, port=3001)
The following Apache directive will direct requests to an SCGI server
running on port 3001::
<Location />
SCGIServer 127.0.0.1 3001
SCGIHandler On
</Location>
[Note: the mod_scgi module for Apache 2 requires a colon, instead of a
space, between the host and port on the SCGIServer line.]
SCGI through CGI
----------------
Recent releases of the scgi package include cgi2scgi.c, a small program
that offers an extremely convenient way to take advantage of SCGI using
Apache or any web server that supports CGI. To use it, compile the
cgi2scgi.c and install the compiled program as usual for your
webserver. The default SCGI port is 3000, but you can change that
by adding ``-DPORT=3001`` (for example) to your compile command.
Although this method requires a new process to be launched for each
request, the process is small and fast, so the performance is
acceptable for many applications.
FastCGI configuration
---------------------
If your web server supports FastCGI, you can significantly speed up your
Quixote applications with a simple change to your configuration. You
don't have to change your code at all (unless it makes assumptions about
how many requests are handled by each process). (See
http://www.fastcgi.com/ for more information on FastCGI.)
To use FastCGI with Apache, you'll need to download mod_fastcgi from
http://www.fastcgi.com/ and add it to your Apache installation.
Configuring a FastCGI driver script is best done after reading the fine
documentation for mod_fastcgi at
http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html
However, if you just want to try it with the Quixote demo to see if it
works, add this directive to your Apache configuration::
AddHandler fastcgi-script .fcgi
and copy server/fastcgi_server.py to demo.fcgi. If you're using a URL
rewrite to map requests for (eg.) ``/qdemo`` to
``/www/cgi-bin/demo.cgi``, be sure to change the rewrite -- it should
now point to ``/www/cgi-bin/demo.fcgi``.
After the first access to ``demo.fcgi`` (or ``/qdemo/`` with the
modified rewrite rule), the demo should be noticeably faster. You
should also see a ``demo.fcgi`` process running if you do ``ps -le``
(``ps -aux`` on BSD-ish systems, or maybe ``ps aux``). (On my 800 MHz
Athlon machine, there are slight but perceptible delays navigating the
Quixote demo in CGI mode. In FastCGI mode, the delay between pages is
no longer perceptible -- navigation is instantaneous.) The larger your
application is, the more code it loads, and the more work it does at
startup, the bigger a win FastCGI will be for you (in comparison to CGI).

191
doc/web-services.html Normal file
View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Implementing Web Services with Quixote</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="implementing-web-services-with-quixote">
<h1 class="title">Implementing Web Services with Quixote</h1>
<p>This document will show you how to implement Web services using
Quixote.</p>
<div class="section" id="an-xml-rpc-service">
<h1><a name="an-xml-rpc-service">An XML-RPC Service</a></h1>
<p>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
<a class="reference" href="http://www.pythonware.com/products/xmlrpc/">http://www.pythonware.com/products/xmlrpc/</a>, converts between Python's
standard data types and the XML-RPC data types.</p>
<table class="table" frame="border" rules="all">
<colgroup>
<col width="40%" />
<col width="60%" />
</colgroup>
<tbody valign="top">
<tr><td>XML-RPC Type</td>
<td>Python Type or Class</td>
</tr>
<tr><td>&lt;int&gt;</td>
<td>int</td>
</tr>
<tr><td>&lt;double&gt;</td>
<td>float</td>
</tr>
<tr><td>&lt;string&gt;</td>
<td>string</td>
</tr>
<tr><td>&lt;array&gt;</td>
<td>list</td>
</tr>
<tr><td>&lt;struct&gt;</td>
<td>dict</td>
</tr>
<tr><td>&lt;boolean&gt;</td>
<td>xmlrpclib.Boolean</td>
</tr>
<tr><td>&lt;base64&gt;</td>
<td>xmlrpclib.Binary</td>
</tr>
<tr><td>&lt;dateTime&gt;</td>
<td>xmlrpclib.DateTime</td>
</tr>
</tbody>
</table>
</div>
<div class="section" id="making-xml-rpc-calls">
<h1><a name="making-xml-rpc-calls">Making XML-RPC Calls</a></h1>
<p>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.</p>
<pre class="literal-block">
&gt;&gt;&gt; import xmlrpclib
&gt;&gt;&gt; s = xmlrpclib.ServerProxy(
'http://www.stuffeddog.com/speller/speller-rpc.cgi')
</pre>
<p>Now you can simply make a call to the spell-checking service offered
by this server:</p>
<pre class="literal-block">
&gt;&gt;&gt; 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}]
&gt;&gt;&gt;
</pre>
<p>This call results in the following XML being sent:</p>
<pre class="literal-block">
&lt;?xml version='1.0'?&gt;
&lt;methodCall&gt;
&lt;methodName&gt;speller.spellCheck&lt;/methodName&gt;
&lt;params&gt;
&lt;param&gt;
&lt;value&gt;&lt;string&gt;my speling isnt gud&lt;/string&gt;&lt;/value&gt;
&lt;/param&gt;
&lt;param&gt;
&lt;value&gt;&lt;struct&gt;&lt;/struct&gt;&lt;/value&gt;
&lt;/param&gt;
&lt;/params&gt;
&lt;/methodCall&gt;
</pre>
</div>
<div class="section" id="writing-a-quixote-service">
<h1><a name="writing-a-quixote-service">Writing a Quixote Service</a></h1>
<p>In the quixote.util module, Quixote provides a function,
<tt class="literal"><span class="pre">xmlrpc(request,</span> <span class="pre">func)</span></tt>, that processes the body of an XML-RPC
request. <tt class="literal"><span class="pre">request</span></tt> is the HTTPRequest object that Quixote passes to
every function it invokes. <tt class="literal"><span class="pre">func</span></tt> 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 <tt class="literal"><span class="pre">xmlrpc()</span></tt> function will
catch the exception and return a <tt class="literal"><span class="pre">Fault</span></tt> to the remote caller.</p>
<p>Here's an example of implementing a simple XML-RPC handler with a
single method, <tt class="literal"><span class="pre">get_time()</span></tt>, that simply returns the current
time. The first task is to expose a URL for accessing the service.</p>
<pre class="literal-block">
from quixote.directory import Directory
from quixote.util import xmlrpc
from quixote import get_request
class RPCDirectory(Directory):
_q_exports = ['rpc']
def rpc (self):
return xmlrpc(get_request(), rpc_process)
def rpc_process (meth, params):
...
</pre>
<p>When the above code is placed in the __init__.py file for the Python
package corresponding to your Quixote application, it exposes the URL
<tt class="literal"><span class="pre">http://&lt;hostname&gt;/rpc</span></tt> as the access point for the XML-RPC service.</p>
<p>Next, we need to fill in the contents of the <tt class="literal"><span class="pre">rpc_process()</span></tt>
function:</p>
<pre class="literal-block">
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, &quot;Unknown XML-RPC method: %r&quot; % meth
</pre>
<p><tt class="literal"><span class="pre">rpc_process()</span></tt> 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 <tt class="literal"><span class="pre">rpc_process()</span></tt> will
therefore usually be an <tt class="literal"><span class="pre">if</span></tt> statement that checks the name of the
method, and calls another function to do the actual work. In this case,
<tt class="literal"><span class="pre">get_time()</span></tt> is very simple so the two lines of code it requires are
simply included in the body of <tt class="literal"><span class="pre">rpc_process()</span></tt>.</p>
<p>If the method name doesn't belong to a supported method, execution
will fall through to the <tt class="literal"><span class="pre">else</span></tt> clause, which will raise a
RuntimeError exception. Quixote's <tt class="literal"><span class="pre">xmlrpc()</span></tt> will catch this
exception and report it to the caller as an XML-RPC fault, with the
error code set to 1.</p>
<p>As you add additional XML-RPC services, the <tt class="literal"><span class="pre">if</span></tt> statement in
<tt class="literal"><span class="pre">rpc_process()</span></tt> will grow more branches. You might be tempted to pass
the method name to <tt class="literal"><span class="pre">getattr()</span></tt> 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 <tt class="literal"><span class="pre">if...</span> <span class="pre">elif...</span> <span class="pre">elif...</span> <span class="pre">else</span></tt> 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.</p>
<p>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:</p>
<pre class="literal-block">
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, &quot;Unknown XML-RPC method: %r&quot; % meth
</pre>
<p>This approach works nicely when there are many methods and the
<tt class="literal"><span class="pre">if...elif...else</span></tt> statement would be unworkably long.</p>
<p>$Id: web-services.txt 25695 2004-11-30 20:53:44Z dbinger $</p>
</div>
</div>
</body>
</html>

169
doc/web-services.txt Normal file
View File

@ -0,0 +1,169 @@
Implementing 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> 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.directory import Directory
from quixote.util import xmlrpc
from quixote import get_request
class RPCDirectory(Directory):
_q_exports = ['rpc']
def rpc (self):
return xmlrpc(get_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 25695 2004-11-30 20:53:44Z dbinger $

503
doc/widgets.html Normal file
View File

@ -0,0 +1,503 @@
<?xml version="1.0" encoding="us-ascii" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii" />
<meta name="generator" content="Docutils 0.3.0: http://docutils.sourceforge.net/" />
<title>Quixote Widget Classes</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head>
<body>
<div class="document" id="quixote-widget-classes">
<h1 class="title">Quixote Widget Classes</h1>
<p>[This is reference documentation. If you haven't yet read &quot;Lesson 5:
widgets&quot; 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 <em>HTML: The Definitive
Guide</em> by Chuck Musciano &amp; Bill Kennedy (O'Reilly). I usually keep it
within arm's reach.]</p>
<p>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:</p>
<pre class="literal-block">
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
</pre>
<div class="section" id="widget-the-base-class">
<h1><a name="widget-the-base-class">Widget: the base class</a></h1>
<p>Widget is the abstract base class for the widget hierarchy. It provides
the following facilities:</p>
<ul class="simple">
<li>widget name (<tt class="literal"><span class="pre">name</span></tt> attribute, <tt class="literal"><span class="pre">set_name()</span></tt> method)</li>
<li>widget value (<tt class="literal"><span class="pre">value</span></tt> attribute, <tt class="literal"><span class="pre">set_value()</span></tt> and <tt class="literal"><span class="pre">clear()</span></tt> methods)</li>
<li><tt class="literal"><span class="pre">__str__()</span></tt> and <tt class="literal"><span class="pre">__repr__()</span></tt> methods</li>
<li>some facilities for writing composite widget classes</li>
</ul>
<p>The Widget constructor signature is:</p>
<pre class="literal-block">
Widget(name : string, value : any = None)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">name</span></tt></dt>
<dd>the name of the widget. For non-compound widgets (ie. everything in
the above class hierarchy), this will be used as the &quot;name&quot;
attribute for the main HTML tag that defines the form element.</dd>
<dt><tt class="literal"><span class="pre">value</span></tt></dt>
<dd>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.</dd>
</dl>
</div>
<div class="section" id="common-widget-methods">
<h1><a name="common-widget-methods">Common widget methods</a></h1>
<p>The Widget base class also provides a couple of useful
methods:</p>
<dl>
<dt><tt class="literal"><span class="pre">set_name(name:string)</span></tt></dt>
<dd>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.</dd>
<dt><tt class="literal"><span class="pre">set_value(value:any)</span></tt></dt>
<dd>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).</dd>
<dt><tt class="literal"><span class="pre">clear()</span></tt></dt>
<dd>clear the widget's current value. Equivalent to
<tt class="literal"><span class="pre">widget.set_value(None)</span></tt>.</dd>
</dl>
<p>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:</p>
<dl>
<dt><tt class="literal"><span class="pre">render(request:HTTPRequest)</span></tt> <span class="classifier-delimiter">:</span> <span class="classifier"><tt class="literal"><span class="pre">string</span></tt></span></dt>
<dd>return a chunk of HTML that implements the form element
corresponding to this widget.</dd>
<dt><tt class="literal"><span class="pre">parse(request:HTTPRequest)</span></tt> <span class="classifier-delimiter">:</span> <span class="classifier"><tt class="literal"><span class="pre">any</span></tt></span></dt>
<dd>extract the form value for this widget from <tt class="literal"><span class="pre">request.form</span></tt>, 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 <tt class="literal"><span class="pre">set_value()</span></tt>.</dd>
</dl>
</div>
<div class="section" id="stringwidget">
<h1><a name="stringwidget">StringWidget</a></h1>
<p>Used for short, single-line string input with no validation (ie. any
string will be accepted.) Generates an <tt class="literal"><span class="pre">&lt;input</span> <span class="pre">type=&quot;text&quot;&gt;</span></tt> form
element.</p>
<div class="section" id="constructor">
<h2><a name="constructor">Constructor</a></h2>
<pre class="literal-block">
StringWidget(name : string,
value : string = None,
size : int = None,
maxlength : int = None)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">size</span></tt></dt>
<dd>used as the <tt class="literal"><span class="pre">size</span></tt> attribute of the generated <tt class="literal"><span class="pre">&lt;input&gt;</span></tt> tag;
controls the physical size of the input field.</dd>
<dt><tt class="literal"><span class="pre">maxlength</span></tt></dt>
<dd>used as the <tt class="literal"><span class="pre">maxlength</span></tt> attribute; controls the maximum amount
of input.</dd>
</dl>
</div>
<div class="section" id="examples">
<h2><a name="examples">Examples</a></h2>
<pre class="literal-block">
&gt;&gt;&gt; StringWidget(&quot;foo&quot;, value=&quot;hello&quot;).render(request)
'&lt;input name=&quot;foo&quot; type=&quot;text&quot; value=&quot;hello&quot;&gt;'
&gt;&gt;&gt; StringWidget(&quot;foo&quot;, size=10, maxlength=20).render(request)
'&lt;input name=&quot;foo&quot; type=&quot;text&quot; size=&quot;10&quot; maxlength=&quot;20&quot;&gt;'
</pre>
</div>
</div>
<div class="section" id="passwordwidget">
<h1><a name="passwordwidget">PasswordWidget</a></h1>
<p>PasswordWidget is identical to StringWidget except for the type of the
HTML form element: <tt class="literal"><span class="pre">password</span></tt> instead of <tt class="literal"><span class="pre">text</span></tt>.</p>
</div>
<div class="section" id="textwidget">
<h1><a name="textwidget">TextWidget</a></h1>
<p>Used for multi-line text input. The value is a single string with
newlines right where the browser supplied them. (<tt class="literal"><span class="pre">\r\n</span></tt>, if present,
is converted to <tt class="literal"><span class="pre">\n</span></tt>.) Generates a <tt class="literal"><span class="pre">&lt;textarea&gt;</span></tt> form element.</p>
<div class="section" id="id1">
<h2><a name="id1">Constructor</a></h2>
<pre class="literal-block">
TextWidget(name : string,
value : string = None,
cols : int = None,
rows : int = None,
wrap : string = &quot;physical&quot;)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">cols</span></tt>, <tt class="literal"><span class="pre">rows</span></tt></dt>
<dd>number of columns/rows in the textarea</dd>
<dt><tt class="literal"><span class="pre">wrap</span></tt></dt>
<dd>controls how the browser wraps text and includes newlines in the
submitted form value; consult an HTML book for details.</dd>
</dl>
</div>
</div>
<div class="section" id="checkboxwidget">
<h1><a name="checkboxwidget">CheckboxWidget</a></h1>
<p>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 <tt class="literal"><span class="pre">parse()</span></tt> will always be 0 or 1 (but you shouldn't
rely on that!). Generates an <tt class="literal"><span class="pre">&lt;input</span> <span class="pre">type=&quot;checkbox&quot;&gt;</span></tt> form element.</p>
<div class="section" id="id2">
<h2><a name="id2">Constructor</a></h2>
<pre class="literal-block">
CheckboxWidget(name : string,
value : boolean = false)
</pre>
</div>
<div class="section" id="id3">
<h2><a name="id3">Examples</a></h2>
<pre class="literal-block">
&gt;&gt;&gt; CheckboxWidget(&quot;foo&quot;, value=0).render(request)
'&lt;input name=&quot;foo&quot; type=&quot;checkbox&quot; value=&quot;yes&quot;&gt;'
&gt;&gt;&gt; CheckboxWidget(&quot;foo&quot;, value=&quot;you bet&quot;).render(request)
'&lt;input name=&quot;foo&quot; type=&quot;checkbox&quot; value=&quot;yes&quot; checked&gt;'
</pre>
</div>
</div>
<div class="section" id="radiobuttonswidget">
<h1><a name="radiobuttonswidget">RadiobuttonsWidget</a></h1>
<p>Used for a <em>set</em> of related radiobuttons, ie. several <tt class="literal"><span class="pre">&lt;input</span>
<span class="pre">type=&quot;radio&quot;&gt;</span></tt> tags with the same name and different values. The set
of values are supplied to the constructor as <tt class="literal"><span class="pre">allowed_values</span></tt>, which
may be a list of any Python objects (not just strings). The current
value must be either <tt class="literal"><span class="pre">None</span></tt> (the default) or one of the values in
<tt class="literal"><span class="pre">allowed_values</span></tt>; if you supply a <tt class="literal"><span class="pre">value</span></tt> not in <tt class="literal"><span class="pre">allowed_values</span></tt>,
it will be ignored. <tt class="literal"><span class="pre">parse()</span></tt> will return either <tt class="literal"><span class="pre">None</span></tt> or one of
the values in <tt class="literal"><span class="pre">allowed_values</span></tt>.</p>
<div class="section" id="id4">
<h2><a name="id4">Constructor</a></h2>
<pre class="literal-block">
RadiobuttonsWidget(name : string,
value : any = None,
allowed_values : [any] = None,
descriptions : [string] = map(str, allowed_values),
quote : boolean = true,
delim : string = &quot;\n&quot;)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">allowed_values</span></tt></dt>
<dd><p class="first">specifies how many <tt class="literal"><span class="pre">&lt;input</span> <span class="pre">type=&quot;radio&quot;&gt;</span></tt> tags to generate and the
values for each. Eg. <tt class="literal"><span class="pre">allowed_values=[&quot;foo&quot;,</span> <span class="pre">&quot;bar&quot;]</span></tt> will result in
(roughly):</p>
<pre class="last literal-block">
&lt;input type=&quot;radio&quot; value=&quot;foo&quot;&gt;
&lt;input type=&quot;radio&quot; value=&quot;bar&quot;&gt;
</pre>
</dd>
<dt><tt class="literal"><span class="pre">descriptions</span></tt></dt>
<dd>the text that will actually be shown to the user in the web page
that includes this widget. Handy when the elements of
<tt class="literal"><span class="pre">allowed_values</span></tt> are too terse, or don't have a meaningful
<tt class="literal"><span class="pre">str()</span></tt>, or you want to add some additional cues for the user. If
not supplied, <tt class="literal"><span class="pre">map(str,</span> <span class="pre">allowed_values)</span></tt> is used, with the
exception that <tt class="literal"><span class="pre">None</span></tt> in <tt class="literal"><span class="pre">allowed_values</span></tt> becomes <tt class="literal"><span class="pre">&quot;&quot;</span></tt> (the
empty string) in <tt class="literal"><span class="pre">descriptions</span></tt>. If supplied, <tt class="literal"><span class="pre">descriptions</span></tt>
must be the same length as <tt class="literal"><span class="pre">allowed_values</span></tt>.</dd>
<dt><tt class="literal"><span class="pre">quote</span></tt></dt>
<dd>if true (the default), the elements of 'descriptions' will be
HTML-quoted (using <tt class="literal"><span class="pre">quixote.html.html_quote()</span></tt>) when the widget is
rendered. This is essential if you might have characters like
<tt class="literal"><span class="pre">&amp;</span></tt> or <tt class="literal"><span class="pre">&lt;</span></tt> in your descriptions. However, you'll want to set
<tt class="literal"><span class="pre">quote</span></tt> to false if you are deliberately including HTML markup
in your descriptions.</dd>
<dt><tt class="literal"><span class="pre">delim</span></tt></dt>
<dd>the delimiter to separate the radiobuttons with when rendering
the whole widget. The default ensures that your HTML is readable
(by putting each <tt class="literal"><span class="pre">&lt;input&gt;</span></tt> tag on a separate line), and that there
is horizontal whitespace between each radiobutton.</dd>
</dl>
</div>
<div class="section" id="id5">
<h2><a name="id5">Examples</a></h2>
<pre class="literal-block">
&gt;&gt;&gt; colours = [&quot;red&quot;, &quot;green&quot;, &quot;blue&quot;, &quot;pink&quot;]
&gt;&gt;&gt; widget = RadiobuttonsWidget(&quot;foo&quot;, allowed_values=colours)
&gt;&gt;&gt; print widget.render(request)
&lt;input name=&quot;foo&quot; type=&quot;radio&quot; value=&quot;0&quot;&gt;red&lt;/input&gt;
&lt;input name=&quot;foo&quot; type=&quot;radio&quot; value=&quot;1&quot;&gt;green&lt;/input&gt;
&lt;input name=&quot;foo&quot; type=&quot;radio&quot; value=&quot;2&quot;&gt;blue&lt;/input&gt;
&lt;input name=&quot;foo&quot; type=&quot;radio&quot; value=&quot;3&quot;&gt;pink&lt;/input&gt;
</pre>
<p>(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 <tt class="literal"><span class="pre">&quot;1&quot;</span></tt> to <tt class="literal"><span class="pre">1</span></tt> and looking up <tt class="literal"><span class="pre">allowed_values[1]</span></tt>.)</p>
<pre class="literal-block">
&gt;&gt;&gt; values = [val1, val2, val3]
&gt;&gt;&gt; descs = [&quot;thing &lt;b&gt;1&lt;/b&gt;&quot;,
&quot;thing &lt;b&gt;2&lt;/b&gt;&quot;,
&quot;thing &lt;b&gt;3&lt;/b&gt;&quot;]
&gt;&gt;&gt; widget = RadiobuttonsWidget(&quot;bar&quot;,
allowed_values=values,
descriptions=descs,
value=val3,
delim=&quot;&lt;br&gt;\n&quot;,
quote=0)
&gt;&gt;&gt; print widget.render(request)
&lt;input name=&quot;bar&quot; type=&quot;radio&quot; value=&quot;0&quot;&gt;thing &lt;b&gt;1&lt;/b&gt;&lt;/input&gt;&lt;br&gt;
&lt;input name=&quot;bar&quot; type=&quot;radio&quot; value=&quot;1&quot;&gt;thing &lt;b&gt;2&lt;/b&gt;&lt;/input&gt;&lt;br&gt;
&lt;input name=&quot;bar&quot; type=&quot;radio&quot; value=&quot;2&quot; checked=&quot;checked&quot;&gt;thing &lt;b&gt;3&lt;/b&gt;&lt;/input&gt;
</pre>
</div>
</div>
<div class="section" id="singleselectwidget">
<h1><a name="singleselectwidget">SingleSelectWidget</a></h1>
<p>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; <tt class="literal"><span class="pre">parse()</span></tt> will return either
<tt class="literal"><span class="pre">None</span></tt> or one of the values you supply to the constructor as
<tt class="literal"><span class="pre">allowed_values</span></tt>. Generates a <tt class="literal"><span class="pre">&lt;select&gt;...&lt;/select&gt;</span></tt> tag, with one
<tt class="literal"><span class="pre">&lt;option&gt;</span></tt> tag for each element of <tt class="literal"><span class="pre">allowed_values</span></tt>.</p>
<div class="section" id="id6">
<h2><a name="id6">Constructor</a></h2>
<pre class="literal-block">
SingleSelectWidget(name : string,
value : any = None,
allowed_values : [any] = None,
descriptions : [string] = map(str, allowed_values),
quote : boolean = true,
size : int = None)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">allowed_values</span></tt></dt>
<dd>determines the set of <tt class="literal"><span class="pre">&lt;option&gt;</span></tt> tags that will go inside the
<tt class="literal"><span class="pre">&lt;select&gt;</span></tt> tag; these can be any Python values (not just strings).
<tt class="literal"><span class="pre">parse()</span></tt> will return either one of the <tt class="literal"><span class="pre">allowed_values</span></tt> or <tt class="literal"><span class="pre">None</span></tt>.
If you supply a <tt class="literal"><span class="pre">value</span></tt> that is not in <tt class="literal"><span class="pre">allowed_values</span></tt>, it
will be ignored.</dd>
<dt><tt class="literal"><span class="pre">descriptions</span></tt></dt>
<dd>(same as RadiobuttonsWidget above)</dd>
<dt><tt class="literal"><span class="pre">quote</span></tt></dt>
<dd>(same as RadiobuttonsWidget above)</dd>
<dt><tt class="literal"><span class="pre">size</span></tt></dt>
<dd>corresponds to the <tt class="literal"><span class="pre">size</span></tt> attribute of the <tt class="literal"><span class="pre">&lt;select&gt;</span></tt> tag: ask
the browser to show a select list with <tt class="literal"><span class="pre">size</span></tt> items visible.
Not always respected by the browser; consult an HTML book.</dd>
</dl>
</div>
<div class="section" id="id7">
<h2><a name="id7">Examples</a></h2>
<pre class="literal-block">
&gt;&gt;&gt; widget = SingleSelectWidget(&quot;foo&quot;,
allowed_values=[&quot;abc&quot;, 123, 5.5])
&gt;&gt;&gt; print widget.render(request)
&lt;select name=&quot;foo&quot;&gt;
&lt;option value=&quot;0&quot;&gt;abc
&lt;option value=&quot;1&quot;&gt;123
&lt;option value=&quot;2&quot;&gt;5.5
&lt;/select&gt;
&gt;&gt;&gt; widget = SingleSelectWidget(&quot;bar&quot;,
value=val2,
allowed_values=[val1, val2, val3],
descriptions=[&quot;foo&quot;, &quot;bar&quot;, &quot;foo &amp; bar&quot;],
size=3)
&gt;&gt;&gt; print widget.render(request)
&lt;select name=&quot;bar&quot; size=&quot;3&quot;&gt;
&lt;option value=&quot;0&quot;&gt;foo
&lt;option selected value=&quot;1&quot;&gt;bar
&lt;option value=&quot;2&quot;&gt;foo &amp;amp; bar
&lt;/select&gt;
</pre>
</div>
</div>
<div class="section" id="multipleselectwidget">
<h1><a name="multipleselectwidget">MultipleSelectWidget</a></h1>
<p>Used to select multiple values from a list. Everything is just like
SingleSelectWidget, except that <tt class="literal"><span class="pre">value</span></tt> can be a list of objects
selected from <tt class="literal"><span class="pre">allowed_values</span></tt> (in which case every object in <tt class="literal"><span class="pre">value</span></tt>
will initially be selected). Generates a <tt class="literal"><span class="pre">&lt;select</span> <span class="pre">multiple&gt;...&lt;/select&gt;</span></tt>
tag, with one <tt class="literal"><span class="pre">&lt;option&gt;</span></tt> tag for each element of <tt class="literal"><span class="pre">allowed_values</span></tt>.</p>
<div class="section" id="id8">
<h2><a name="id8">Constructor</a></h2>
<pre class="literal-block">
MultipleSelectWidget(name : string,
value : any | [any] = None,
allowed_values : [any] = None,
descriptions : [string] = map(str, allowed_values),
quote : boolean = true,
size : int = None)
</pre>
</div>
</div>
<div class="section" id="submitbuttonwidget">
<h1><a name="submitbuttonwidget">SubmitButtonWidget</a></h1>
<p>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.</p>
<p>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.</p>
<p>Also, SubmitButtonWidget is the only widget with an optional <tt class="literal"><span class="pre">name</span></tt>.
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.</p>
<div class="section" id="id9">
<h2><a name="id9">Constructor</a></h2>
<pre class="literal-block">
SubmitButtonWidget(name : string = None,
value : string = None)
</pre>
<dl>
<dt><tt class="literal"><span class="pre">value</span></tt></dt>
<dd>the text that will be shown in the user's browser, <em>and</em> the
value that will be returned for this form element (widget)
if the user selects this submit button.</dd>
</dl>
</div>
<div class="section" id="id10">
<h2><a name="id10">Examples</a></h2>
<blockquote>
<pre class="doctest-block">
&gt;&gt;&gt; SubmitButtonWidget(value=&quot;Submit Form&quot;).render(request)
'&lt;input type=&quot;submit&quot; value=&quot;Submit Form&quot;&gt;'
</pre>
</blockquote>
</div>
</div>
<div class="section" id="hiddenwidget">
<h1><a name="hiddenwidget">HiddenWidget</a></h1>
<p>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 &quot;sensitive&quot; I mean &quot;information
which should not be revealed&quot;, rather than &quot;security-related&quot;. If you
wouldn't put it in a cookie or in email, don't put it in a hidden form
element.)</p>
<div class="section" id="id11">
<h2><a name="id11">Constructor</a></h2>
<pre class="literal-block">
HiddenWidget(name : string,
value : string)
</pre>
</div>
<div class="section" id="id12">
<h2><a name="id12">Examples</a></h2>
<pre class="literal-block">
&gt;&gt;&gt; HiddenWidget(&quot;form_id&quot;, &quot;2452345135&quot;).render(request)
'&lt;input type=&quot;hidden&quot; name=&quot;form_id&quot; value=&quot;2452345135&quot;&gt;'
</pre>
</div>
</div>
<div class="section" id="intwidget">
<h1><a name="intwidget">IntWidget</a></h1>
<p>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 <tt class="literal"><span class="pre">int()</span></tt> can't convert to an integer,
IntWidget's <tt class="literal"><span class="pre">parse()</span></tt> 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.</p>
<p><tt class="literal"><span class="pre">IntWidget.parse()</span></tt> always returns an integer or <tt class="literal"><span class="pre">None</span></tt>.</p>
<div class="section" id="id13">
<h2><a name="id13">Constructor</a></h2>
<pre class="literal-block">
IntWidget(name : string,
value : int = None,
size : int = None,
maxlength : int = None)
</pre>
<p>Constructor arguments are as for StringWidget, except that <tt class="literal"><span class="pre">value</span></tt>
must be an integer (or <tt class="literal"><span class="pre">None</span></tt>). Note that <tt class="literal"><span class="pre">size</span></tt> and
<tt class="literal"><span class="pre">maxlength</span></tt> have exactly the same meaning: they control the size of
the input widget and the maximum number of characters of input.</p>
<p>[Examples]</p>
<blockquote>
<pre class="doctest-block">
&gt;&gt;&gt; IntWidget(&quot;num&quot;, value=37, size=5).render(request)
'&lt;input type=&quot;string&quot; name=&quot;num&quot; value=&quot;37&quot; size=&quot;5&quot;&gt;'
</pre>
</blockquote>
</div>
</div>
<div class="section" id="floatwidget">
<h1><a name="floatwidget">FloatWidget</a></h1>
<p>FloatWidget is identical to IntWidget, except:</p>
<ul class="simple">
<li><tt class="literal"><span class="pre">value</span></tt> must be a float</li>
<li><tt class="literal"><span class="pre">parse()</span></tt> returns a float or <tt class="literal"><span class="pre">None</span></tt></li>
<li><tt class="literal"><span class="pre">parse()</span></tt> raises FormValueError if the string entered by the
user cannot be converted by Python's built-in <tt class="literal"><span class="pre">float()</span></tt> function</li>
</ul>
</div>
<div class="section" id="optionselectwidget">
<h1><a name="optionselectwidget">OptionSelectWidget</a></h1>
<p>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.)</p>
<p>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 &quot;Canada&quot;,
you'll ask them for a province and a postal code; if they select &quot;United
States&quot;, 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 [&quot;How many states does Australia
have again?&quot;], and you're bound to get something wrong -- there are over
200 countries in the world, after all.)</p>
<p>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 <tt class="literal"><span class="pre">submit</span></tt>,
since that masks the Javascript function called by OptionSelectWidget.</p>
<p>$Id: widgets.txt 20217 2003-01-16 20:51:53Z akuchlin $</p>
</div>
</div>
</body>
</html>

533
doc/widgets.txt Normal file
View File

@ -0,0 +1,533 @@
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
|
+--ButtonWidget
| |
| +-SubmitWidget
| |
| +-ResetWidget
|
+--HiddenWidget
|
+--CompositeWidget [A]
|
+-WidgetList [*]
|
+-WidgetDict [*]
[*] 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_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).
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()`` : ``string``
return a chunk of HTML that implements the form element
corresponding to this widget.
``parse()`` : ``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()
'<input name="foo" type="text" value="hello">'
>>> StringWidget("foo", size=10, maxlength=20).render()
'<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()
'<input name="foo" type="checkbox" value="yes">'
>>> CheckboxWidget("foo", value="you bet").render()
'<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()
<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()
<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()
<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()
<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)
ButtonWidget
------------
Base class of SubmitWidget and ResetWidget. A ButtonWidget does nothing
except create a button on the page.
>>> ButtonWidget("button", value="hello").render()
'<input type="button" name="button" value="hello">'
SubmitWidget
------------
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.
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("submit", value="Submit Form").render()
'<input type="submit" value="Submit Form">'
ResetWidget
-----------
Generates a button to reset the form::
>>> ResetWidget("reset").render()
'<input type="reset" name="reset">'
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()
'<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()
'<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.

14
doc/win32.txt Normal file
View File

@ -0,0 +1,14 @@
Compiling Quixote c extensions for Windows using the mingw compiler:
The variable build_extensions in setup.py must be set to True (instead
of "sys.platform != win32"). Then, Quixote can be installed through
python setup.py build_ext --compiler=mingw32 install
The mingw compiler can be downloaded from
http://www.bloodshed.net/dev/devcpp.html. Probably you need to list
the bin directory (C:\Program files\Dev-Cpp\bin) in the PATH.
To permit gcc to link against Python library, you also need a
libpython2x.a file. Instructions for its creation are provided at
http://sebsauvage.net/python/mingw.html.

139
errors.py Normal file
View File

@ -0,0 +1,139 @@
"""quixote.errors
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/errors.py $
$Id: errors.py 26782 2005-05-11 20:32:01Z nascheme $
Exception classes used by Quixote
"""
from quixote.html import htmltext, htmlescape
class PublishError(Exception):
"""PublishError exceptions are raised due to some problem with the
data provided by the client and are raised during the publishing
process. Quixote will abort the current request and return an error
page to the client.
public_msg should be a user-readable message that reveals no
inner workings of your application; it will always be shown.
private_msg will only be shown if the config option DISPLAY_EXCEPTIONS is
true; Quixote uses this to give you more detail about why the error
occurred. You might want to use it for similar, application-specific
information. (DISPLAY_EXCEPTIONS should always be false in a production
environment, since these details about the inner workings of your
application could conceivably be useful to attackers.)
The formatting done by the Quixote versions of these exceptions is
very simple. Applications will probably wish to raise application
specific subclasses which do more sophisticated formatting or provide
a _q_except handler to format the exception.
"""
status_code = 400 # bad request
title = "Publishing error"
description = "no description"
def __init__(self, public_msg=None, private_msg=None):
self.public_msg = public_msg
self.private_msg = private_msg # cleared if DISPLAY_EXCEPTIONS is false
def __str__(self):
return self.private_msg or self.public_msg or "???"
def format(self):
msg = htmlescape(self.title)
if self.public_msg:
msg = msg + ": " + self.public_msg
if self.private_msg:
msg = msg + ": " + self.private_msg
return msg
class TraversalError(PublishError):
"""
Raised when a client attempts to access a resource that does not
exist or is otherwise unavailable to them (eg. a Python function
not listed in its module's _q_exports list).
path should be the path to the requested resource; if not
supplied, the current request object will be fetched and its
get_path() method called.
"""
status_code = 404 # not found
title = "Page not found"
description = ("The requested link does not exist on this site. If "
"you arrived here by following a link from an external "
"page, please inform that page's maintainer.")
def __init__(self, public_msg=None, private_msg=None, path=None):
PublishError.__init__(self, public_msg, private_msg)
if path is None:
import quixote
path = quixote.get_request().get_path()
self.path = path
def format(self):
msg = htmlescape(self.title) + ": " + self.path
if self.public_msg:
msg = msg + ": " + self.public_msg
if self.private_msg:
msg = msg + ": " + self.private_msg
return msg
class RequestError(PublishError):
"""
Raised when Quixote is unable to parse an HTTP request (or its CGI
representation). This is a lower-level error than QueryError -- it
either means that Quixote is not smart enough to handle the request
being passed to it, or the user-agent is broken and/or malicious.
"""
status_code = 400
title = "Invalid request"
description = "Unable to parse HTTP request."
class QueryError(PublishError):
"""Should be raised if bad data was provided in the query part of a
URL or in the content of a POST request. What constitutes bad data is
solely application dependent (eg: letters in a form field when the
application expects a number).
"""
status_code = 400
title = "Invalid query"
description = ("An error occurred while handling your request. The "
"query data provided as part of the request is invalid.")
class AccessError(PublishError):
"""Should be raised if the client does not have access to the
requested resource. Usually applications will raise this error from
an _q_access method.
"""
status_code = 403
title = "Access denied"
description = ("An error occurred while handling your request. "
"Access to the requested resource was not permitted.")
def format_publish_error(exc):
"""(exc : PublishError) -> string
Format a PublishError exception as a web page.
"""
return htmltext("""\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
"http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head><title>Error: %s</title></head>
<body>
<p>%s</p>
<p>%s</p>
</body>
</html>
""") % (exc.title, exc.description, exc.format())

18
form/__init__.py Normal file
View File

@ -0,0 +1,18 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/__init__.py $
$Id: __init__.py 26469 2005-04-05 10:26:03Z dbinger $
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.form.form import Form, FormTokenWidget
from quixote.form.widget import Widget, StringWidget, FileWidget, \
PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \
SingleSelectWidget, SelectWidget, OptionSelectWidget, \
MultipleSelectWidget, SubmitWidget, HiddenWidget, \
FloatWidget, IntWidget, subname, WidgetValueError, CompositeWidget, \
WidgetList, WidgetDict

101
form/compatibility.py Normal file
View File

@ -0,0 +1,101 @@
'''$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/compatibility.py $
$Id: compatibility.py 26061 2005-02-11 02:48:16Z dbinger $
A Form subclass that provides close to the same API as the old form
class (useful for transitioning existing forms).
'''
from quixote import get_request, get_path, redirect
from quixote.form 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, action_url=None, *args, **kwargs):
_Form.__init__(self, action=action_url, *args, **kwargs)
self.cancel_url = None
self.action_url = self.action
def add_widget(self, widget_class, name, value=None,
title=None, hint=None, required=False, **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):
action_url = url_quote(get_path())
query = get_request().get_query()
if query:
action_url += "?" + query
return action_url
def render(self, action_url=None):
if action_url:
self.action_url = action_url
return _Form.render(self)
def process(self):
values = {}
request = get_request()
for name, widget in self._names.items():
values[name] = widget.parse()
return values
def action(self, submit, values):
raise NotImplementedError, "sub-classes must implement 'action()'"
def handle(self):
"""handle() -> 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.
"""
request = get_request()
if not self.is_submitted():
return self.render(self.action_url)
submit = self.get_submit()
if submit == "cancel":
return redirect(self.cancel_url)
values = self.process()
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(self.action_url)
if self.has_errors():
return self.render(self.action_url)
else:
return self.action(submit, values)

76
form/css.py Normal file
View File

@ -0,0 +1,76 @@
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;
}
"""

365
form/form.py Normal file
View File

@ -0,0 +1,365 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/form.py $
$Id: form.py 26200 2005-02-18 22:58:30Z dbinger $
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 htmltag, htmltext, TemplateIO
from quixote.form.widget import HiddenWidget, StringWidget, TextWidget, \
CheckboxWidget, SingleSelectWidget, RadiobuttonsWidget, \
MultipleSelectWidget, ResetWidget, SubmitWidget, FloatWidget, \
IntWidget, PasswordWidget
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' # this error does not get displayed
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(object):
"""
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('<script type="text/javascript">\n'
'<!--\n'
'%s\n'
'// -->\n'
'</script>\n')
TOKEN_NOTICE = htmltext('<div class="errornotice">'
'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.'
'</div>')
ERROR_NOTICE = htmltext('<div class="errornotice">'
'There were errors processing your form. '
'See below for details.'
'</div>')
def __init__(self,
method="post",
action=None,
enctype=None,
use_tokens=True,
**attrs):
if method not in ("post", "get"):
raise ValueError("Form method must be 'post' or 'get', "
"not %r" % method)
self.method = method
self.action = action or self._get_default_action()
if 'class' not in attrs:
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(self):
query = get_request().get_query()
if query:
return "?" + query
else:
return ""
# -- 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_password(self, name, value=None, **kwargs):
self.add(PasswordWidget, 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', method=self.method,
enctype=self.enctype, action=self.action,
**self.attrs)
r += self._render_hidden_widgets()
return r.getvalue()
def _render_finish(self):
r = TemplateIO(html=True)
r += htmltext('</form><br class="quixoteform" />')
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('<div class="submit">')
for widget in self.submit_widgets:
r += widget.render()
r += htmltext('</div><br class="submit" />')
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 self.TOKEN_NOTICE
else:
return self.ERROR_NOTICE
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()

951
form/widget.py Normal file
View File

@ -0,0 +1,951 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/widget.py $
$Id: widget.py 27293 2005-09-04 05:46:31Z nascheme $
Provides the basic web widget classes: Widget itself, plus StringWidget,
TextWidget, CheckboxWidget, etc.
"""
import struct
from quixote import get_request
from quixote.html import htmltext, htmlescape, htmltag, TemplateIO, stringify
from quixote.http_request import Upload
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 stringify(self.msg)
class Widget(object):
"""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.
"""
REQUIRED_ERROR = 'required'
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(stringify(exc))
if (self.required and self.value is None and
not self.has_error()):
self.set_error(self.REQUIRED_ERROR)
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 isinstance(value, basestring) and value.strip():
self.value = value
else:
self.value = None
def render_title(self, title):
if title:
if self.required:
title += htmltext('<span class="required">*</span>')
return htmltext('<div class="title">%s</div>') % title
else:
return ''
def render_hint(self, hint):
if hint:
return htmltext('<div class="hint">%s</div>') % hint
else:
return ''
def render_error(self, error):
if error:
return htmltext('<div class="error">%s</div>') % error
else:
return ''
def render(self):
r = TemplateIO(html=True)
classnames = '%s widget' % self.__class__.__name__
r += htmltext('<div class="%s">') % classnames
r += self.render_title(self.get_title())
r += htmltext('<div class="content">')
r += self.render_content()
r += self.render_hint(self.get_hint())
r += self.render_error(self.get_error())
r += htmltext('</div>')
r += htmltext('</div>')
if self.render_br:
r += htmltext('<br class="%s" />') % 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:
# <textarea> TextWidget
# <select> SingleSelectWidget
# <select multiple>
# MultipleSelectWidget
class StringWidget(Widget):
"""Widget for entering a single string: corresponds to
'<input type="text">' in HTML.
Instance attributes:
value : string
"""
# This lets PasswordWidget be a trivial subclass
HTML_TYPE = "text"
def render_content(self):
return htmltag("input", xml_end=True,
type=self.HTML_TYPE,
name=self.name,
value=self.value,
**self.attrs)
class FileWidget(StringWidget):
"""Subclass of StringWidget for uploading files.
Instance attributes: none
"""
HTML_TYPE = "file"
def _parse(self, request):
parsed_value = request.form.get(self.name)
if isinstance(parsed_value, Upload):
self.value = parsed_value
else:
self.value = None
class PasswordWidget(StringWidget):
"""Trivial subclass of StringWidget for entering passwords (different
widget type because HTML does it that way).
Instance attributes: none
"""
HTML_TYPE = "password"
class TextWidget(Widget):
"""Widget for entering a long, multi-line string; corresponds to
the HTML "<textarea>" tag.
Instance attributes:
value : string
"""
def _parse(self, request):
Widget._parse(self, request)
if self.value and self.value.find("\r\n") >= 0:
self.value = self.value.replace("\r\n", "\n")
def render_content(self):
return (htmltag("textarea", name=self.name, **self.attrs) +
htmlescape(self.value or "") +
htmltext("</textarea>"))
class CheckboxWidget(Widget):
"""Widget for a single checkbox: corresponds to "<input
type=checkbox>". 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
<select name=...>
<option value="Foo">Foo</option>
...
</select>
Instance attributes:
options : [ (value:any, description:any, key:string) ]
value : any
The value is None or an element of dict(options.values()).
"""
SELECTION_ERROR = "invalid value selected"
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(stringify, 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 isinstance(first, tuple):
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(stringify(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 (stringify(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 = self.SELECTION_ERROR
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('</option>'))
tags.append(htmltext("</select>"))
return htmltext("\n").join(tags)
class SingleSelectWidget(SelectWidget):
"""Widget for single selection.
"""
SELECT_TYPE = "single_select"
MULTIPLE_SELECTION_ERROR = "cannot select multiple values"
def _parse(self, request):
parsed_key = request.form.get(self.name)
if parsed_key:
if isinstance(parsed_key, list):
self.error = self.MULTIPLE_SELECTION_ERROR
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 isinstance(value, (list, tuple)):
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 isinstance(parsed_keys, list):
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(ButtonWidget):
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
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_OBJECT(self.value)
except ValueError:
self.error = self.TYPE_ERROR
class FloatWidget(NumberWidget):
"""
Instance attributes:
value : float
"""
TYPE_OBJECT = float
TYPE_ERROR = "must be a number"
class IntWidget(NumberWidget):
"""
Instance attributes:
value : int
"""
TYPE_OBJECT = 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 isinstance(parsed_key, list):
self.error = self.MULTIPLE_SELECTION_ERROR
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('<noscript>'
'<input type="submit" name="" value="apply" />'
'</noscript>'))
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
if self.attrs.get('disabled') and 'disabled' not in kwargs:
kwargs['disabled'] = True
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={},
add_element_label="Add 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 isinstance(add_element_label, (basestring, htmltext)), (
"value '%s'add_element_label not a string: "
"got %r" % (name, add_element_label))
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_element_label)
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 dict can have its own
error. You can also set an error on the WidgetDict 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={},
add_element_label='Add 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 isinstance(add_element_label, (basestring, htmltext)), (
'value %r element_name not a string: '
'got %r' % (name, add_element_label))
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_element_label)
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<div class="widget">: </div>%s') % (
key_widget.render(),
value_widget.render())
if self.render_br:
r += htmltext('<br clear="left" class="widget" />')
r += htmltext('\n')
r += self.get_widget('add_element').render()
r += self.get_widget('added_elements').render()
return r.getvalue()

34
form1/__init__.py Normal file
View File

@ -0,0 +1,34 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/__init__.py $
$Id: __init__.py 25664 2004-11-22 20:35:07Z 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 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.
"""
from quixote.form1.form import Form, register_widget_class, FormTokenWidget
from quixote.form1.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)

534
form1/form.py Normal file
View File

@ -0,0 +1,534 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/form.py $
$Id: form.py 25664 2004-11-22 20:35:07Z nascheme $
Provides the Form class and bureaucracy for registering widget classes.
(The standard widget classes are registered automatically.)
"""
from types import StringType
from quixote import get_session, get_publisher, redirect
from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO
from quixote.form1.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('''\
<script type="text/javascript">
<!--
%s
// -->
</script>
''')
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('</form>')
r += self._render_javascript(request)
return r.getvalue()
def _render_sep(self, text, line=1):
return htmltext('<tr><td colspan="3">%s<strong><big>%s'
'</big></strong></td></tr>') % \
(line and htmltext('<hr>') or '', text)
def _render_error(self, error):
if error:
return htmltext('<font color="red">%s</font><br />') % nl2br(error)
else:
return ''
def _render_hint(self, hint):
if hint:
return htmltext('<em>%s</em>') % 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('&nbsp;*')
r = TemplateIO(html=1)
r += htmltext('<tr><th colspan="3" align="left">')
r += title
r += htmltext('</th></tr>'
'<tr><td>&nbsp;&nbsp;</td><td>')
r += widget.render(request)
r += htmltext('</td><td>')
r += self._render_error(self.error.get(widget.name))
r += self._render_hint(self.hint.get(widget.name))
r += htmltext('</td></tr>')
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('<tr><td colspan="%d">\n') % ncols
for button in self.submit_buttons:
r += button.render(request)
r += htmltext('</td></tr>')
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('<tr><td colspan="3">'
'<font color="red"><strong>Warning:</strong></font> '
'there were errors processing your form. '
'See below for details.'
'</td></tr>')
else:
r = ''
return r
def _render_required_notice(self, request):
if filter(None, self.required.values()):
r = htmltext('<tr><td colspan="3">'
'<b>*</b> = <em>required field</em>'
'</td></tr>')
else:
r = ''
return r
def _render_body(self, request):
r = TemplateIO(html=1)
r += htmltext('<table>')
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('</table>')
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 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)

842
form1/widget.py Normal file
View File

@ -0,0 +1,842 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/widget.py $
$Id: widget.py 25664 2004-11-22 20:35:07Z nascheme $
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
from quixote.http_request 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:
# <textarea> TextWidget
# <select> SingleSelectWidget
# <select multiple>
# MultipleSelectWidget
class StringWidget (Widget):
"""Widget for entering a single string: corresponds to
'<input type="text">' in HTML.
Instance attributes:
value : string
size : int
maxlength : int
"""
widget_type = "string"
# This lets PasswordWidget be a trivial subclass
html_type = "text"
def __init__(self, name, value=None,
size=None, maxlength=None):
Widget.__init__(self, name, value)
self.size = size
self.maxlength = maxlength
def render(self, request, **attributes):
return htmltag("input", xml_end=1,
type=self.html_type,
name=self.name,
size=self.size,
maxlength=self.maxlength,
value=self.value,
**attributes)
class FileWidget (StringWidget):
"""Trivial subclass of StringWidget for uploading files.
Instance attributes: none
"""
widget_type = "file"
html_type = "file"
def parse(self, request):
"""parse(request) -> any"""
value = request.form.get(self.name)
if isinstance(value, Upload):
self.value = value
else:
self.value = None
return self.value
class PasswordWidget (StringWidget):
"""Trivial subclass of StringWidget for entering passwords (different
widget type because HTML does it that way).
Instance attributes: none
"""
widget_type = "password"
html_type = "password"
class TextWidget (Widget):
"""Widget for entering a long, multi-line string; corresponds to
the HTML "<textarea>" tag.
Instance attributes:
value : string
cols : int
rows : int
wrap : string
(see an HTML book for details on text widget wrap options)
css_class : string
"""
widget_type = "text"
def __init__(self, name, value=None, cols=None, rows=None, wrap=None,
css_class=None):
Widget.__init__(self, name, value)
self.cols = cols
self.rows = rows
self.wrap = wrap
self.css_class = css_class
def render(self, request):
return (htmltag("textarea", name=self.name,
cols=self.cols,
rows=self.rows,
wrap=self.wrap,
css_class=self.css_class) +
htmlescape(self.value or "") +
htmltext("</textarea>"))
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 "<input
type=checkbox>". 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 "checked" 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
<select name=...>
<option value="Foo">Foo</option>
...
</select>
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 = "multiple"
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 = "selected"
else:
selected = None
if description is None:
description = ""
r = htmltag("option", value=key, selected=selected)
tags.append(r + htmlescape(description) + htmltext('</option>'))
tags.append(htmltext("</select>"))
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 = "checked"
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('<noscript>'
'<input type="submit" name="" value="apply" />'
'</noscript>'))
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('<br />\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('<br />\n').join(tags)

107
html/__init__.py Normal file
View File

@ -0,0 +1,107 @@
"""Various functions for dealing with HTML.
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/html/__init__.py $
$Id: __init__.py 26631 2005-04-20 21:13:38Z dbinger $
These functions are fairly simple but it is critical that they be
used correctly. Many security problems are caused by escaping errors
(cross site scripting is one example). The HTML and XML standards on
www.w3c.org and www.xml.com should be studied, especially the sections
on character sets, entities, attribute and values.
htmltext and htmlescape
-----------------------
This type and function are meant to be used with [html] PTL template type.
The htmltext type designates data that does not need to be escaped and the
htmlescape() function calls str() on the argment, escapes the resulting
string and returns a htmltext instance. htmlescape() does nothing to
htmltext instances.
url_quote
---------
Use for quoting data to be included as part of a URL, for example:
input = "foo bar"
...
'<a href="/search?keyword=%s">' % url_quote(input)
Note that URLs are usually used as attribute values and might need to have
HTML special characters escaped. As an example of incorrect usage:
url = 'http://example.com/?a=1&copy=0' # INCORRECT
url = 'http://example.com/?a=1&amp;copy=0' # CORRECT
...
'<a href="%s">do something</a>' % url
Old browsers would treat "&copy" as an entity reference and replace it with
the copyright character. XML processors should treat it as an invalid entity
reference.
"""
import urllib
try:
# faster C implementation
from quixote.html._c_htmltext import htmltext, htmlescape, \
stringify, TemplateIO
except ImportError:
from quixote.html._py_htmltext import htmltext, htmlescape, \
stringify, TemplateIO
ValuelessAttr = object() # magic singleton object
def htmltag(tag, xml_end=False, css_class=None, **attrs):
"""Create a HTML tag.
"""
r = ["<%s" % tag]
if css_class is not None:
attrs['class'] = css_class
for (attr, val) in attrs.items():
if val is ValuelessAttr:
val = attr
if val is not None:
r.append(' %s="%s"' % (attr,
stringify(htmlescape(val))))
if xml_end:
r.append(" />")
else:
r.append(">")
return htmltext("".join(r))
def href(url, text, title=None, **attrs):
return (htmltag("a", href=url, title=title, **attrs) +
htmlescape(text) +
htmltext("</a>"))
def url_with_query(path, **attrs):
result = htmltext(url_quote(path))
if attrs:
result += "?" + "&".join([url_quote(key) + "=" + url_quote(value)
for key, value in attrs.items()])
return result
def nl2br(value):
"""nl2br(value : any) -> htmltext
Insert <br /> tags before newline characters.
"""
text = htmlescape(value)
return htmltext(text.s.replace('\n', '<br />\n'))
def url_quote(value, fallback=None):
"""url_quote(value : any [, fallback : string]) -> string
Quotes 'value' for use in a URL; see urllib.quote(). If value is None,
then the behavior depends on the fallback argument. If it is not
supplied then an error is raised. Otherwise, the fallback value is
returned unquoted.
"""
if value is None:
if fallback is None:
raise ValueError, "value is None and no fallback supplied"
else:
return fallback
return urllib.quote(stringify(value))

1110
html/_c_htmltext.c Normal file

File diff suppressed because it is too large Load Diff

231
html/_py_htmltext.py Normal file
View File

@ -0,0 +1,231 @@
"""Python implementation of the htmltext type, the htmlescape function and
TemplateIO.
"""
#$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/html/_py_htmltext.py $
#$Id: _py_htmltext.py 27294 2005-09-04 06:00:01Z nascheme $
def _escape_string(s):
if not isinstance(s, basestring):
raise TypeError, 'string object required'
s = s.replace("&", "&amp;")
s = s.replace("<", "&lt;")
s = s.replace(">", "&gt;")
s = s.replace('"', "&quot;")
return s
def stringify(obj):
"""Return 'obj' as a string or unicode object. Tries to prevent
turning strings into unicode objects.
"""
tp = type(obj)
if issubclass(tp, basestring):
return obj
elif hasattr(tp, '__unicode__'):
s = tp.__unicode__(obj)
if not isinstance(s, basestring):
raise TypeError, '__unicode__ did not return a string'
return s
elif hasattr(tp, '__str__'):
s = tp.__str__(obj)
if not isinstance(s, basestring):
raise TypeError, '__str__ did not return a string'
return s
else:
return str(obj)
class htmltext(object):
"""The htmltext string-like type. This type serves as a tag
signifying that HTML special characters do not need to be escaped
using entities.
"""
__slots__ = ['s']
def __init__(self, s):
self.s = stringify(s)
# XXX make read-only
#def __setattr__(self, name, value):
# raise AttributeError, 'immutable object'
def __getstate__(self):
raise ValueError, 'htmltext objects should not be pickled'
def __repr__(self):
return '<htmltext %r>' % self.s
def __str__(self):
return self.s
def __len__(self):
return len(self.s)
def __cmp__(self, other):
return cmp(self.s, other)
def __hash__(self):
return hash(self.s)
def __mod__(self, args):
if isinstance(args, tuple):
return htmltext(self.s % tuple(map(_wraparg, args)))
else:
return htmltext(self.s % _wraparg(args))
def __add__(self, other):
if isinstance(other, basestring):
return htmltext(self.s + _escape_string(other))
elif isinstance(other, htmltext):
return htmltext(self.s + other.s)
else:
return NotImplemented
def __radd__(self, other):
if isinstance(other, basestring):
return htmltext(_escape_string(other) + self.s)
else:
return NotImplemented
def __mul__(self, n):
return htmltext(self.s * n)
def join(self, items):
quoted_items = []
for item in items:
if isinstance(item, htmltext):
quoted_items.append(stringify(item))
elif isinstance(item, basestring):
quoted_items.append(_escape_string(item))
else:
raise TypeError(
'join() requires string arguments (got %r)' % item)
return htmltext(self.s.join(quoted_items))
def startswith(self, s):
if isinstance(s, htmltext):
s = s.s
else:
s = _escape_string(s)
return self.s.startswith(s)
def endswith(self, s):
if isinstance(s, htmltext):
s = s.s
else:
s = _escape_string(s)
return self.s.endswith(s)
def replace(self, old, new, count=-1):
if isinstance(old, htmltext):
old = old.s
else:
old = _escape_string(old)
if isinstance(new, htmltext):
new = new.s
else:
new = _escape_string(new)
return htmltext(self.s.replace(old, new, count))
def lower(self):
return htmltext(self.s.lower())
def upper(self):
return htmltext(self.s.upper())
def capitalize(self):
return htmltext(self.s.capitalize())
class _QuoteWrapper(object):
# helper for htmltext class __mod__
__slots__ = ['value']
def __init__(self, value):
self.value = value
def __str__(self):
return _escape_string(stringify(self.value))
def __repr__(self):
return _escape_string(`self.value`)
def __getitem__(self, key):
return _wraparg(self.value[key])
class _UnicodeWrapper(unicode):
__slots__ = ['raw']
def __new__(cls, s):
result = unicode.__new__(cls, _escape_string(s))
result.raw = s
return result
def __repr__(self):
return _escape_string(`self.raw`)
def _wraparg(arg):
if isinstance(arg, htmltext):
# necessary to work around a PyString_Format bug in Python. Should
# be fixed in Python 2.5
return stringify(arg)
elif isinstance(arg, unicode):
# again, work around PyString_Format bug
return _UnicodeWrapper(arg)
elif (isinstance(arg, int) or
isinstance(arg, long) or
isinstance(arg, float)):
# ints, longs, floats are okay
return arg
else:
# everything is gets wrapped
return _QuoteWrapper(arg)
def htmlescape(s):
"""htmlescape(s) -> htmltext
Return an 'htmltext' object using the argument. If the argument is not
already a 'htmltext' object then the HTML markup characters \", <, >,
and & are first escaped.
"""
if isinstance(s, htmltext):
return s
else:
s = stringify(s)
# inline _escape_string for speed
s = s.replace("&", "&amp;") # must be done first
s = s.replace("<", "&lt;")
s = s.replace(">", "&gt;")
s = s.replace('"', "&quot;")
return htmltext(s)
class TemplateIO(object):
"""Collect output for PTL scripts.
"""
__slots__ = ['html', 'data']
def __init__(self, html=False):
self.html = html
self.data = []
def __iadd__(self, other):
if other is not None:
self.data.append(other)
return self
def __repr__(self):
return ("<%s at %x: %d chunks>" %
(self.__class__.__name__, id(self), len(self.data)))
def __str__(self):
return stringify(self.getvalue())
def getvalue(self):
if self.html:
return htmltext('').join(map(htmlescape, self.data))
else:
return ''.join(map(stringify, self.data))

372
html/test/utest_html.py Executable file
View File

@ -0,0 +1,372 @@
import sys
from sancho.utest import UTest
from quixote.html import _py_htmltext
from quixote.html import href, url_with_query, url_quote, nl2br
markupchars = '<>&"'
quotedchars = '&lt;&gt;&amp;&quot;'
if sys.hexversion >= 0x20400a2:
unicodechars = u'\u1234'
else:
unicodechars = 'x' # lie, Python <= 2.3 is broken
class Wrapper:
def __init__(self, s):
self.s = s
def __repr__(self):
return self.s
def __str__(self):
return self.s
class BrokenError(Exception):
pass
class Broken:
def __str__(self):
raise BrokenError, 'eieee'
def __repr__(self):
raise BrokenError, 'eieee'
htmltext = escape = htmlescape = TemplateIO = stringify = None
class HTMLTest (UTest):
def check_href(self):
assert str(href('/foo/bar', 'bar')) == '<a href="/foo/bar">bar</a>'
def check_url_with_query(self):
assert str(url_with_query('/f/b', a='1')) == '/f/b?a=1'
assert str(url_with_query(
'/f/b', a='1', b='3 4')) == '/f/b?a=1&amp;b=3%204'
def check_nl2br(self):
assert str(nl2br('a\nb\nc')) == 'a<br />\nb<br />\nc'
def check_url_quote(self):
assert url_quote('abc') == 'abc'
assert url_quote('a b c') == 'a%20b%20c'
assert url_quote(None, fallback='abc') == 'abc'
class HTMLTextTest (UTest):
def _pre(self):
global htmltext, escape, htmlescape, TemplateIO, stringify
htmltext = _py_htmltext.htmltext
escape = _py_htmltext._escape_string
stringify = _py_htmltext.stringify
htmlescape = _py_htmltext.htmlescape
TemplateIO = _py_htmltext.TemplateIO
def _post(self):
global htmltext, escape, htmlescape, TemplateIO, stringify
htmltext = escape = htmlescape = TemplateIO = stringify = None
def _check_init(self):
assert str(htmltext('foo')) == 'foo'
assert str(htmltext(markupchars)) == markupchars
assert unicode(htmltext(unicodechars)) == unicodechars
assert str(htmltext(unicode(markupchars))) == markupchars
assert str(htmltext(None)) == 'None'
assert str(htmltext(1)) == '1'
try:
htmltext(Broken())
assert 0
except BrokenError: pass
def check_stringify(self):
assert stringify(markupchars) is markupchars
assert stringify(unicodechars) is unicodechars
assert stringify(Wrapper(unicodechars)) is unicodechars
assert stringify(Wrapper(markupchars)) is markupchars
assert stringify(Wrapper) == str(Wrapper)
assert stringify(None) == str(None)
def check_escape(self):
assert htmlescape(markupchars) == quotedchars
assert isinstance(htmlescape(markupchars), htmltext)
assert escape(markupchars) == quotedchars
assert escape(unicodechars) == unicodechars
assert escape(unicode(markupchars)) == quotedchars
assert isinstance(escape(markupchars), basestring)
assert htmlescape(htmlescape(markupchars)) == quotedchars
try:
escape(1)
assert 0
except TypeError: pass
def check_cmp(self):
s = htmltext("foo")
assert s == 'foo'
assert s != 'bar'
assert s == htmltext('foo')
assert s != htmltext('bar')
assert htmltext(u'\u1234') == u'\u1234'
assert htmltext('1') != 1
assert 1 != s
def check_len(self):
assert len(htmltext('foo')) == 3
assert len(htmltext(markupchars)) == len(markupchars)
assert len(htmlescape(markupchars)) == len(quotedchars)
def check_hash(self):
assert hash(htmltext('foo')) == hash('foo')
assert hash(htmltext(markupchars)) == hash(markupchars)
assert hash(htmlescape(markupchars)) == hash(quotedchars)
def check_concat(self):
s = htmltext("foo")
assert s + 'bar' == "foobar"
assert 'bar' + s == "barfoo"
assert s + htmltext('bar') == "foobar"
assert s + markupchars == "foo" + quotedchars
assert isinstance(s + markupchars, htmltext)
assert markupchars + s == quotedchars + "foo"
assert isinstance(markupchars + s, htmltext)
assert markupchars + htmltext(u'') == quotedchars
try:
s + 1
assert 0
except TypeError: pass
try:
1 + s
assert 0
except TypeError: pass
# mixing unicode and str
assert repr(htmltext('a') + htmltext('b')) == "<htmltext 'ab'>"
assert repr(htmltext(u'a') + htmltext('b')) == "<htmltext u'ab'>"
assert repr(htmltext('a') + htmltext(u'b')) == "<htmltext u'ab'>"
def check_repeat(self):
s = htmltext('a')
assert s * 3 == "aaa"
assert isinstance(s * 3, htmltext)
assert htmlescape(markupchars) * 3 == quotedchars * 3
try:
s * 'a'
assert 0
except TypeError: pass
try:
'a' * s
assert 0
except TypeError: pass
try:
s * s
assert 0
except TypeError: pass
def check_format(self):
s_fmt = htmltext('%s')
u_fmt = htmltext(u'%s')
assert s_fmt % 'foo' == "foo"
assert u_fmt % 'foo' == u"foo"
assert isinstance(s_fmt % 'foo', htmltext)
assert isinstance(u_fmt % 'foo', htmltext)
assert s_fmt % markupchars == quotedchars
assert u_fmt % markupchars == quotedchars
assert s_fmt % None == "None"
assert u_fmt % None == "None"
assert s_fmt % unicodechars == unicodechars
assert u_fmt % unicodechars == unicodechars
assert s_fmt % htmltext(unicodechars) == unicodechars
assert u_fmt % htmltext(unicodechars) == unicodechars
assert htmltext('%r') % Wrapper(markupchars) == quotedchars
assert htmltext('%r') % unicodechars == `unicodechars`
assert htmltext('%s%s') % ('foo', htmltext(markupchars)) \
== ("foo" + markupchars)
assert htmltext('%d') % 10 == "10"
assert htmltext('%.1f') % 10 == "10.0"
try:
s_fmt % Broken()
assert 0
except BrokenError: pass
try:
htmltext('%r') % Broken()
assert 0
except BrokenError: pass
try:
s_fmt % (1, 2)
assert 0
except TypeError: pass
assert htmltext('%d') % 12300000000000000000L == "12300000000000000000"
def check_dict_format(self):
args = {'a': 'foo&', 'b': htmltext('bar&')}
result = "foo&amp; 'foo&amp;' bar&"
assert htmltext('%(a)s %(a)r %(b)s') % args == result
assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&amp;"
assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext)
assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&amp;'}"
try:
htmltext('%(a)s') % 1
assert 0
except TypeError: pass
try:
htmltext('%(a)s') % {}
assert 0
except KeyError: pass
assert htmltext('') % {} == ''
assert htmltext('%%') % {} == '%'
def check_join(self):
assert htmltext(' ').join(['foo', 'bar']) == "foo bar"
assert htmltext(' ').join(['foo', markupchars]) == \
"foo " + quotedchars
assert htmlescape(markupchars).join(['foo', 'bar']) == \
"foo" + quotedchars + "bar"
assert htmltext(' ').join([htmltext(markupchars), 'bar']) == \
markupchars + " bar"
assert isinstance(htmltext('').join([]), htmltext)
assert htmltext(u' ').join([unicodechars]) == unicodechars
assert htmltext(u' ').join(['']) == u''
try:
htmltext('').join(1)
assert 0
except TypeError: pass
try:
htmltext('').join([1])
assert 0
except TypeError: pass
def check_startswith(self):
assert htmltext('foo').startswith('fo')
assert htmlescape(markupchars).startswith(markupchars[:3])
assert htmltext(markupchars).startswith(htmltext(markupchars[:3]))
try:
htmltext('').startswith(1)
assert 0
except TypeError: pass
def check_endswith(self):
assert htmltext('foo').endswith('oo')
assert htmlescape(markupchars).endswith(markupchars[-3:])
assert htmltext(markupchars).endswith(htmltext(markupchars[-3:]))
try:
htmltext('').endswith(1)
assert 0
except TypeError: pass
def check_replace(self):
assert htmlescape('&').replace('&', 'foo') == "foo"
assert htmltext('&').replace(htmltext('&'), 'foo') == "foo"
assert htmltext('foo').replace('foo', htmltext('&')) == "&"
assert isinstance(htmltext('a').replace('a', 'b'), htmltext)
try:
htmltext('').replace(1, 'a')
assert 0
except TypeError: pass
def check_lower(self):
assert htmltext('aB').lower() == "ab"
assert isinstance(htmltext('a').lower(), htmltext)
def check_upper(self):
assert htmltext('aB').upper() == "AB"
assert isinstance(htmltext('a').upper(), htmltext)
def check_capitalize(self):
assert htmltext('aB').capitalize() == "Ab"
assert isinstance(htmltext('a').capitalize(), htmltext)
class TemplateTest (UTest):
def _pre(self):
global TemplateIO
TemplateIO = _py_htmltext.TemplateIO
def _post(self):
global TemplateIO
TemplateIO = None
def check_init(self):
TemplateIO()
TemplateIO(html=True)
TemplateIO(html=False)
def check_text_iadd(self):
t = TemplateIO()
assert t.getvalue() == ''
t += "abcd"
assert t.getvalue() == 'abcd'
t += None
assert t.getvalue() == 'abcd'
t += 123
assert t.getvalue() == 'abcd123'
t += u'\u1234'
assert t.getvalue() == u'abcd123\u1234'
try:
t += Broken(); t.getvalue()
assert 0
except BrokenError: pass
def check_html_iadd(self):
t = TemplateIO(html=1)
assert t.getvalue() == ''
t += "abcd"
assert t.getvalue() == 'abcd'
t += None
assert t.getvalue() == 'abcd'
t += 123
assert t.getvalue() == 'abcd123'
try:
t += Broken(); t.getvalue()
assert 0
except BrokenError: pass
t = TemplateIO(html=1)
t += markupchars
assert t.getvalue() == quotedchars
def check_repr(self):
t = TemplateIO()
t += "abcd"
assert "TemplateIO" in repr(t)
def check_str(self):
t = TemplateIO()
t += "abcd"
assert str(t) == "abcd"
try:
from quixote.html import _c_htmltext
except ImportError:
_c_htmltext = None
if _c_htmltext:
class CHTMLTest(HTMLTest):
def _pre(self):
# using globals like this is a bit of a hack since it assumes
# Sancho tests each class individually, oh well
global htmltext, escape, htmlescape, stringify
htmltext = _c_htmltext.htmltext
escape = _c_htmltext._escape_string
stringify = _py_htmltext.stringify
htmlescape = _c_htmltext.htmlescape
class CHTMLTextTest(HTMLTextTest):
def _pre(self):
global htmltext, escape, htmlescape, stringify
htmltext = _c_htmltext.htmltext
escape = _c_htmltext._escape_string
stringify = _py_htmltext.stringify
htmlescape = _c_htmltext.htmlescape
class CTemplateTest(TemplateTest):
def _pre(self):
global TemplateIO
TemplateIO = _c_htmltext.TemplateIO
if __name__ == "__main__":
HTMLTest()
HTMLTextTest()
TemplateTest()
if _c_htmltext:
CHTMLTest()
CHTMLTextTest()
CTemplateTest()

767
http_request.py Normal file
View File

@ -0,0 +1,767 @@
"""quixote.http_request
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/http_request.py $
$Id: http_request.py 27293 2005-09-04 05:46:31Z nascheme $
Provides the HTTPRequest class and related code for parsing HTTP
requests, such as the Upload class.
"""
import re
import string
import tempfile
import urllib
import rfc822
from cStringIO import StringIO
import quixote
from quixote.http_response import HTTPResponse
from quixote.errors import RequestError
# Various regexes for parsing specific bits of HTTP, all from RFC 2616.
# These are needed by 'get_encoding()', to parse the "Accept-Encoding"
# header. LWS is linear whitespace; the latter two assume that LWS
# has been removed.
_http_lws_re = re.compile(r"(\r\n)?[ \t]+")
_http_list_re = re.compile(r",+")
_http_encoding_re = re.compile(r"([^;]+)(;q=([\d.]+))?$")
# These are needed by 'guess_browser_version()', for parsing the
# "User-Agent" header.
# token = 1*<any CHAR except CTLs or separators>
# CHAR = any 7-bit US ASCII character (0-127)
# separators are ( ) < > @ , ; : \ " / [ ] ? = { }
#
# The user_agent RE is a simplification; it only looks for one "product",
# possibly followed by a comment.
_http_token_pat = r"[\w!#$%&'*+.^`|~-]+"
_http_product_pat = r'(%s)(?:/(%s))?' % (_http_token_pat, _http_token_pat)
_http_product_re = re.compile(_http_product_pat)
_comment_delim_re = re.compile(r';\s*')
def get_content_type(environ):
ctype = environ.get("CONTENT_TYPE")
if ctype:
return ctype.split(";")[0]
else:
return None
def _decode_string(s, charset):
if charset == 'iso-8859-1' == quixote.DEFAULT_CHARSET:
# To avoid breaking applications that are not Unicode-safe, return
# a str instance in this case.
return s
try:
return s.decode(charset)
except LookupError:
raise RequestError('unknown charset %r' % charset)
except UnicodeDecodeError:
raise RequestError('invalid %r encoded string' % charset)
def parse_header(line):
"""Parse a Content-type like header.
Return the main content-type and a dictionary of options.
"""
plist = map(lambda x: x.strip(), line.split(';'))
key = plist.pop(0).lower()
pdict = {}
for p in plist:
i = p.find('=')
if i >= 0:
name = p[:i].strip().lower()
value = p[i+1:].strip()
if len(value) >= 2 and value[0] == value[-1] == '"':
value = value[1:-1]
pdict[name] = value
return key, pdict
def parse_content_disposition(full_cdisp):
(cdisp, cdisp_params) = parse_header(full_cdisp)
name = cdisp_params.get('name')
if not (cdisp == 'form-data' and name):
raise RequestError('expected Content-Disposition: form-data '
'with a "name" parameter: got %r' % full_cdisp)
return (name, cdisp_params.get('filename'))
def parse_query(qs, charset):
"""(qs: string) -> {key:string, string|[string]}
Parse a query given as a string argument and return a dictionary.
"""
fields = {}
for chunk in filter(None, qs.split('&')):
if '=' not in chunk:
name = chunk
value = ''
else:
name, value = chunk.split('=', 1)
name = urllib.unquote(name.replace('+', ' '))
value = urllib.unquote(value.replace('+', ' '))
name = _decode_string(name, charset)
value = _decode_string(value, charset)
_add_field_value(fields, name, value)
return fields
def _add_field_value(fields, name, value):
if name in fields:
values = fields[name]
if not isinstance(values, list):
fields[name] = values = [values]
values.append(value)
else:
fields[name] = value
class HTTPRequest:
"""
Model a single HTTP request and all associated data: environment
variables, form variables, cookies, etc.
To access environment variables associated with the request, use
get_environ(): eg. request.get_environ('SERVER_PORT', 80).
To access form variables, use get_field(), eg.
request.get_field("name").
To access cookies, use get_cookie().
Various bits and pieces of the requested URL can be accessed with
get_url(), get_path(), get_server()
The HTTPResponse object corresponding to this request is available
in the 'response' attribute. This is rarely needed: eg. to send an
error response, you should raise one of the exceptions in errors.py;
to send a redirect, you should use the quixote.redirect() function,
which lets you specify relative URLs. However, if you need to tweak
the response object in other ways, you can do so via 'response'.
Just keep in mind that Quixote discards the original response object
when handling an exception.
"""
DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET
def __init__(self, stdin, environ):
self.stdin = stdin
self.environ = environ
self.form = {}
self.session = None
self.charset = self.DEFAULT_CHARSET or quixote.DEFAULT_CHARSET
self.response = HTTPResponse()
# The strange treatment of SERVER_PORT_SECURE is because IIS
# sets this environment variable to "0" for non-SSL requests
# (most web servers -- well, Apache at least -- simply don't set
# it in that case).
if (environ.get('HTTPS', 'off').lower() == 'on' or
environ.get('SERVER_PORT_SECURE', '0') != '0'):
self.scheme = "https"
else:
self.scheme = "http"
k = self.environ.get('HTTP_COOKIE', '')
if k:
self.cookies = parse_cookies(k)
else:
self.cookies = {}
# IIS breaks PATH_INFO because it leaves in the path to
# the script, so SCRIPT_NAME is "/cgi-bin/q.py" and PATH_INFO
# is "/cgi-bin/q.py/foo/bar". The following code fixes
# PATH_INFO to the expected value "/foo/bar".
web_server = environ.get('SERVER_SOFTWARE', 'unknown')
if web_server.find('Microsoft-IIS') != -1:
script = environ['SCRIPT_NAME']
path = environ['PATH_INFO']
if path.startswith(script):
path = path[len(script):]
self.environ['PATH_INFO'] = path
def process_inputs(self):
query = self.get_query()
if query:
self.form.update(parse_query(query, self.charset))
length = self.environ.get('CONTENT_LENGTH') or "0"
try:
length = int(length)
except ValueError:
raise RequestError('invalid content-length header')
ctype = self.environ.get("CONTENT_TYPE")
if ctype:
ctype, ctype_params = parse_header(ctype)
if ctype == 'application/x-www-form-urlencoded':
self._process_urlencoded(length, ctype_params)
elif ctype == 'multipart/form-data':
self._process_multipart(length, ctype_params)
def _process_urlencoded(self, length, params):
query = self.stdin.read(length)
if len(query) != length:
raise RequestError('unexpected end of request body')
# Use the declared charset if it's provided (most browser's don't
# provide it to avoid breaking old HTTP servers).
charset = params.get('charset', self.charset)
self.form.update(parse_query(query, charset))
def _process_multipart(self, length, params):
boundary = params.get('boundary')
if not boundary:
raise RequestError('multipart/form-data missing boundary')
charset = params.get('charset')
mimeinput = MIMEInput(self.stdin, boundary, length)
try:
for line in mimeinput.readpart():
pass # discard lines up to first boundary
while mimeinput.moreparts():
self._process_multipart_body(mimeinput, charset)
except EOFError:
raise RequestError('unexpected end of multipart/form-data')
def _process_multipart_body(self, mimeinput, charset):
headers = StringIO()
lines = mimeinput.readpart()
for line in lines:
headers.write(line)
if line == '\r\n':
break
headers.seek(0)
headers = rfc822.Message(headers)
ctype, ctype_params = parse_header(headers.get('content-type', ''))
if ctype and 'charset' in ctype_params:
charset = ctype_params['charset']
cdisp, cdisp_params = parse_header(headers.get('content-disposition',
''))
if not cdisp:
raise RequestError('expected Content-Disposition header')
name = cdisp_params.get('name')
filename = cdisp_params.get('filename')
if not (cdisp == 'form-data' and name):
raise RequestError('expected Content-Disposition: form-data'
'with a "name" parameter: got %r' %
headers.get('content-disposition', ''))
# FIXME: should really to handle Content-Transfer-Encoding and other
# MIME complexity here. See RFC2048 for the full horror story.
if filename:
# it might be large file upload so use a temporary file
upload = Upload(filename, ctype, charset)
upload.receive(lines)
_add_field_value(self.form, name, upload)
else:
value = _decode_string(''.join(lines), charset or self.charset)
_add_field_value(self.form, name, value)
def get_header(self, name, default=None):
"""get_header(name : string, default : string = None) -> string
Return the named HTTP header, or an optional default argument
(or None) if the header is not found. Note that both original
and CGI-ified header names are recognized, e.g. 'Content-Type',
'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE' should all return the
Content-Type header, if available.
"""
environ = self.environ
name = name.replace("-", "_").upper()
val = environ.get(name)
if val is not None:
return val
if name[:5] != 'HTTP_':
name = 'HTTP_' + name
return environ.get(name, default)
def get_cookie(self, cookie_name, default=None):
return self.cookies.get(cookie_name, default)
def get_cookies(self):
return self.cookies
def get_field(self, name, default=None):
return self.form.get(name, default)
def get_fields(self):
return self.form
def get_method(self):
"""Returns the HTTP method for this request
"""
return self.environ.get('REQUEST_METHOD', 'GET')
def formiter(self):
return self.form.iteritems()
def get_scheme(self):
return self.scheme
# The following environment variables are useful for reconstructing
# the original URL, all of which are specified by CGI 1.1:
#
# SERVER_NAME "www.example.com"
# SCRIPT_NAME "/q"
# PATH_INFO "/debug/dump_sessions"
# QUERY_STRING "session_id=10.27.8.40...."
def get_server(self):
"""get_server() -> string
Return the server name with an optional port number, eg.
"www.example.com" or "foo.bar.com:8000".
"""
http_host = self.environ.get("HTTP_HOST")
if http_host:
return http_host
server_name = self.environ["SERVER_NAME"].strip()
server_port = self.environ.get("SERVER_PORT")
if (not server_port or
(self.get_scheme() == "http" and server_port == "80") or
(self.get_scheme() == "https" and server_port == "443")):
return server_name
else:
return server_name + ":" + server_port
def get_path(self, n=0):
"""get_path(n : int = 0) -> string
Return the path of the current request, chopping off 'n' path
components from the right. Eg. if the path is "/bar/baz/qux",
n=0 would return "/bar/baz/qux" and n=2 would return "/bar".
Note that the query string, if any, is not included.
A path with a trailing slash should just be considered as having
an empty last component. Eg. if the path is "/bar/baz/", then:
get_path(0) == "/bar/baz/"
get_path(1) == "/bar/baz"
get_path(2) == "/bar"
If 'n' is negative, then components from the left of the path
are returned. Continuing the above example,
get_path(-1) = "/bar"
get_path(-2) = "/bar/baz"
get_path(-3) = "/bar/baz/"
Raises ValueError if absolute value of n is larger than the number of
path components."""
path_info = self.environ.get('PATH_INFO', '')
path = self.environ['SCRIPT_NAME'] + path_info
if n == 0:
return path
else:
path_comps = path.split('/')
if abs(n) > len(path_comps)-1:
raise ValueError, "n=%d too big for path '%s'" % (n, path)
if n > 0:
return '/'.join(path_comps[:-n])
elif n < 0:
return '/'.join(path_comps[:-n+1])
else:
assert 0, "Unexpected value for n (%s)" % n
def get_query(self):
"""() -> string
Return the query component of the URL.
"""
return self.environ.get('QUERY_STRING', '')
def get_url(self, n=0):
"""get_url(n : int = 0) -> string
Return the URL of the current request, chopping off 'n' path
components from the right. Eg. if the URL is
"http://foo.com/bar/baz/qux", n=2 would return
"http://foo.com/bar". Does not include the query string (if
any).
"""
return "%s://%s%s" % (self.get_scheme(), self.get_server(),
urllib.quote(self.get_path(n)))
def get_environ(self, key, default=None):
"""get_environ(key : string) -> string
Fetch a CGI environment variable from the request environment.
See http://hoohoo.ncsa.uiuc.edu/cgi/env.html
for the variables specified by the CGI standard.
"""
return self.environ.get(key, default)
def get_encoding(self, encodings):
"""get_encoding(encodings : [string]) -> string
Parse the "Accept-encoding" header. 'encodings' is a list of
encodings supported by the server sorted in order of preference.
The return value is one of 'encodings' or None if the client
does not accept any of the encodings.
"""
accept_encoding = self.get_header("accept-encoding") or ""
found_encodings = self._parse_pref_header(accept_encoding)
if found_encodings:
for encoding in encodings:
if found_encodings.has_key(encoding):
return encoding
return None
def get_accepted_types(self):
"""get_accepted_types() : {string:float}
Return a dictionary mapping MIME types the client will accept
to the corresponding quality value (1.0 if no value was specified).
"""
accept_types = self.environ.get('HTTP_ACCEPT', "")
return self._parse_pref_header(accept_types)
def _parse_pref_header(self, S):
"""_parse_pref_header(S:string) : {string:float}
Parse a list of HTTP preferences (content types, encodings) and
return a dictionary mapping strings to the quality value.
"""
found = {}
# remove all linear whitespace
S = _http_lws_re.sub("", S)
for coding in _http_list_re.split(S):
m = _http_encoding_re.match(coding)
if m:
encoding = m.group(1).lower()
q = m.group(3) or 1.0
try:
q = float(q)
except ValueError:
continue
if encoding == "*":
continue # stupid, ignore it
if q > 0:
found[encoding] = q
return found
def dump(self):
result=[]
row='%-15s %s'
result.append("Form:")
L = self.form.items() ; L.sort()
for k,v in L:
result.append(row % (k,v))
result.append("")
result.append("Cookies:")
L = self.cookies.items() ; L.sort()
for k,v in L:
result.append(row % (k,v))
result.append("")
result.append("Environment:")
L = self.environ.items() ; L.sort()
for k,v in L:
result.append(row % (k,v))
return "\n".join(result)
def guess_browser_version(self):
"""guess_browser_version() -> (name : string, version : string)
Examine the User-agent request header to try to figure out what
the current browser is. Returns either (name, version) where
each element is a string, (None, None) if we couldn't parse the
User-agent header at all, or (name, None) if we got the name but
couldn't figure out the version.
Handles Microsoft's little joke of pretending to be Mozilla,
eg. if the "User-Agent" header is
Mozilla/5.0 (compatible; MSIE 5.5)
returns ("MSIE", "5.5"). Konqueror does the same thing, and
it's handled the same way.
"""
ua = self.get_header('user-agent')
if ua is None:
return (None, None)
# The syntax for "User-Agent" in RFC 2616 is fairly simple:
#
# User-Agent = "User-Agent" ":" 1*( product | comment )
# product = token ["/" product-version ]
# product-version = token
# comment = "(" *( ctext | comment ) ")"
# ctext = <any TEXT excluding "(" and ")">
# token = 1*<any CHAR except CTLs or tspecials>
# tspecials = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" |
# "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" |
# "}" | SP | HT
#
# This function handles the most-commonly-used subset of this syntax,
# namely
# User-Agent = "User-Agent" ":" product 1*SP [comment]
# ie. one product string followed by an optional comment;
# anything after that first comment is ignored. This should be
# enough to distinguish Mozilla/Netscape, MSIE, Opera, and
# Konqueror.
m = _http_product_re.match(ua)
if not m:
import sys
sys.stderr.write("couldn't parse User-Agent header: %r\n" % ua)
return (None, None)
name, version = m.groups()
ua = ua[m.end():].lstrip()
if ua.startswith('('):
# we need to handle nested comments since MSIE uses them
depth = 1
chars = []
for c in ua[1:]:
if c == '(':
depth += 1
elif c == ')':
depth -= 1
if depth == 0:
break
elif depth == 1:
# nested comments are discarded
chars.append(c)
comment = ''.join(chars)
else:
comment = ''
if comment:
comment_chunks = _comment_delim_re.split(comment)
else:
comment_chunks = []
if ("compatible" in comment_chunks and
len(comment_chunks) > 1 and comment_chunks[1]):
# A-ha! Someone is kidding around, pretending to be what
# they are not. Most likely MSIE masquerading as Mozilla,
# but lots of other clients (eg. Konqueror) do the same.
real_ua = comment_chunks[1]
if "/" in real_ua:
(name, version) = real_ua.split("/", 1)
else:
if real_ua.startswith("MSIE") and ' ' in real_ua:
(name, version) = real_ua.split(" ", 1)
else:
name = real_ua
version = None
return (name, version)
# Either nobody is pulling our leg, or we didn't find anything
# that looks vaguely like a user agent in the comment. So use
# what we found outside the comment, ie. what the spec says we
# should use (sigh).
return (name, version)
# guess_browser_version ()
# See RFC 2109 for details. Note that this parser is more liberal.
_COOKIE_RE = re.compile(r"""
\s*
(?P<name>[^=;,\s]+)
\s*
(
=
\s*
(
(?P<qvalue> "(\\[\x00-\x7f] | [^"])*")
|
(?P<value> [^";,\s]*)
)
)?
\s*
[;,]?
""", re.VERBOSE)
def parse_cookies(text):
result = {}
for m in _COOKIE_RE.finditer(text):
name = m.group('name')
if name[0] == '$':
# discard, we don't handle per cookie attributes (e.g. $Path)
continue
qvalue = m.group('qvalue')
if qvalue:
value = re.sub(r'\\(.)', r'\1', qvalue)[1:-1]
else:
value = m.group('value') or ''
result[name] = value
return result
SAFE_CHARS = string.letters + string.digits + "-@&+=_., "
_safe_trans = None
def make_safe_filename(s):
global _safe_trans
if _safe_trans is None:
_safe_trans = ["_"] * 256
for c in SAFE_CHARS:
_safe_trans[ord(c)] = c
_safe_trans = "".join(_safe_trans)
return s.translate(_safe_trans)
class Upload:
r"""
Represents a single uploaded file. Uploaded files live in the
filesystem, *not* in memory.
fp
an open file containing the content of the upload. The file pointer
points to the beginning of the file
orig_filename
the complete filename supplied by the user-agent in the
request that uploaded this file. Depending on the browser,
this might have the complete path of the original file
on the client system, in the client system's syntax -- eg.
"C:\foo\bar\upload_this" or "/foo/bar/upload_this" or
"foo:bar:upload_this".
base_filename
the base component of orig_filename, shorn of MS-DOS,
Mac OS, and Unix path components and with "unsafe"
characters neutralized (see make_safe_filename())
content_type
the content type provided by the user-agent in the request
that uploaded this file.
charset
the charset provide by the user-agent
"""
def __init__(self, orig_filename, content_type=None, charset=None):
if orig_filename:
self.orig_filename = orig_filename
bspos = orig_filename.rfind("\\")
cpos = orig_filename.rfind(":")
spos = orig_filename.rfind("/")
if bspos != -1: # eg. "\foo\bar" or "D:\ding\dong"
filename = orig_filename[bspos+1:]
elif cpos != -1: # eg. "C:foo" or ":ding:dong:foo"
filename = orig_filename[cpos+1:]
elif spos != -1: # eg. "foo/bar/baz" or "/tmp/blah"
filename = orig_filename[spos+1:]
else:
filename = orig_filename
self.base_filename = make_safe_filename(filename)
else:
self.orig_filename = None
self.base_filename = None
self.content_type = content_type
self.charset = charset
self.fp = None
def receive(self, lines):
self.fp = tempfile.TemporaryFile("w+b")
for line in lines:
self.fp.write(line)
self.fp.seek(0)
def __str__(self):
return str(self.orig_filename)
def __repr__(self):
return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self)
def read(self, n):
return self.fp.read(n)
def readline(self):
return self.fp.readline()
def readlines(self):
return self.fp.readlines()
def __iter__(self):
return iter(self.fp)
def close(self):
self.fp.close()
class LineInput:
r"""
A wrapper for an input stream that has the following properties:
* lines are terminated by \r\n
* lines shorter than 'maxlength' are always returned unbroken
* lines longer than 'maxlength' are broken but the pair of
characters \r\n are never split
* no more than 'length' characters are read from the underlying
stream
* if the underlying stream does not produce at least 'length'
characters then EOFError is raised
"""
def __init__(self, fp, length):
self.fp = fp
self.length = length
self.buf = ''
def readline(self, maxlength=4096):
# fill buffer
n = min(self.length, maxlength - len(self.buf))
if n > 0:
self.length -= n
assert self.length >= 0
chunk = self.fp.read(n)
if len(chunk) != n:
raise EOFError('unexpected end of input')
self.buf += chunk
# split into lines
buf = self.buf
i = buf.find('\r\n')
if i >= 0:
i += 2
self.buf = buf[i:]
return buf[:i]
elif buf.endswith('\r'):
# avoid splitting CR LF pairs
self.buf = '\r'
return buf[:-1]
else:
self.buf = ''
return buf
class MIMEInput:
"""
Split a MIME input stream into parts. Note that this class does not
handle headers, transfer encoding, etc.
"""
def __init__(self, fp, boundary, length):
self.lineinput = LineInput(fp, length)
self.pat = re.compile(r'--%s(--)?[ \t]*\r\n' % re.escape(boundary))
self.done = False
def moreparts(self):
"""Return true if there are more parts to be read."""
return not self.done
def readpart(self):
"""Generate all the lines up to a MIME boundary. Note that you
must exhaust the generator before calling this function again."""
assert not self.done
last_line = ''
while 1:
line = self.lineinput.readline()
if not line:
# Hit EOF -- nothing more to read. This should *not* happen
# in a well-formed MIME message.
raise EOFError('MIME boundary not found (end of input)')
if last_line.endswith('\r\n') or last_line == '':
m = self.pat.match(line)
if m:
# If we hit the boundary line, return now. Forget
# the current line *and* the CRLF ending of the
# previous line.
if m.group(1):
# hit final boundary
self.done = True
yield last_line[:-2]
return
if last_line:
yield last_line
last_line = line

502
http_response.py Normal file
View File

@ -0,0 +1,502 @@
"""quixote.http_response
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/http_response.py $
$Id: http_response.py 27524 2005-10-06 19:56:57Z dbinger $
Provides the HTTPResponse class.
"""
import time
from sets import Set
try:
import zlib
except ImportError:
pass
import struct
from rfc822 import formatdate
import quixote
from quixote.html import stringify
status_reasons = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested range not satisfiable',
417: 'Expectation Failed',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not supported',
507: 'Insufficient Storage',
}
_GZIP_HEADER = ("\037\213" # magic
"\010" # compression method
"\000" # flags
"\000\000\000\000" # time, who cares?
"\002"
"\377")
_GZIP_EXCLUDE = Set(["application/pdf",
"application/zip",
"audio/mpeg",
"image/gif",
"image/jpeg",
"image/png",
"video/mpeg",
"video/quicktime",
"video/x-msvideo",
])
class HTTPResponse:
"""
An object representation of an HTTP response.
The Response type encapsulates all possible responses to HTTP
requests. Responses are normally created by the Quixote publisher
or by the HTTPRequest class (every request must have a response,
after all).
Instance attributes:
content_type : string
the MIME content type of the response (does not include extra params
like charset)
charset : string | None
the character encoding of the the response. If none, the 'charset'
parameter of the Context-Type header will not be included.
status_code : int
HTTP response status code (integer between 100 and 599)
reason_phrase : string
the reason phrase that accompanies status_code (usually
set automatically by the set_status() method)
headers : { string : string }
most of the headers included with the response; every header set
by 'set_header()' goes here. Does not include "Status" or
"Set-Cookie" headers (unless someone uses set_header() to set
them, but that would be foolish).
body : str | Stream
the response body, None by default. Note that if the body is not a
stream then it is already encoded using 'charset'.
buffered : bool
if false, response data will be flushed as soon as it is
written (the default is true). This is most useful for
responses that use the Stream() protocol. Note that whether the
client actually receives the partial response data is highly
dependent on the web server
cookies : { name:string : { attrname : value } }
collection of cookies to set in this response; it is expected
that the user-agent will remember the cookies and send them on
future requests. The cookie value is stored as the "value"
attribute. The other attributes are as specified by RFC 2109.
cache : int | None
the number of seconds the response may be cached. The default is 0,
meaning don't cache at all. This variable is used to set the HTTP
expires header. If set to None then the expires header will not be
added.
javascript_code : { string : string }
a collection of snippets of JavaScript code to be included in
the response. The collection is built by calling add_javascript(),
but actually including the code in the HTML document is somebody
else's problem.
"""
DEFAULT_CONTENT_TYPE = 'text/html'
DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET
def __init__(self, status=200, body=None, content_type=None, charset=None):
"""
Creates a new HTTP response.
"""
self.content_type = content_type or self.DEFAULT_CONTENT_TYPE
self.charset = (charset or
self.DEFAULT_CHARSET or
quixote.DEFAULT_CHARSET)
self.set_status(status)
self.headers = {}
if body is not None:
self.set_body(body)
else:
self.body = None
self.cookies = {}
self.cache = 0
self.buffered = True
self.javascript_code = None
def set_content_type(self, content_type, charset=None):
"""(content_type : string, charset : string = None)
Set the content type of the response to the MIME type specified by
'content_type'. If 'charset' is not provided and the content_type is
text/* then the charset attribute remains unchanged, otherwise the
charset attribute is set to None and the charset parameter will not
be included as part of the Content-Type header.
"""
content_type = content_type.lower()
if charset is not None or not content_type.startswith('text/'):
self.charset = charset
self.content_type = content_type
def set_charset(self, charset):
if not charset:
self.charset = None
else:
self.charset = str(charset).lower()
def set_status(self, status, reason=None):
"""set_status(status : int, reason : string = None)
Sets the HTTP status code of the response. 'status' must be an
integer in the range 100 .. 599. 'reason' must be a string; if
not supplied, the default reason phrase for 'status' will be
used. If 'status' is a non-standard status code, the generic
reason phrase for its group of status codes will be used; eg.
if status == 493, the reason for status 400 will be used.
"""
if not isinstance(status, int):
raise TypeError, "status must be an integer"
if not (100 <= status <= 599):
raise ValueError, "status must be between 100 and 599"
self.status_code = status
if reason is None:
if status_reasons.has_key(status):
reason = status_reasons[status]
else:
# Eg. for generic 4xx failures, use the reason
# associated with status 400.
reason = status_reasons[status - (status % 100)]
else:
reason = str(reason)
self.reason_phrase = reason
def set_header(self, name, value):
"""set_header(name : string, value : string)
Sets an HTTP return header "name" with value "value", clearing
the previous value set for the header, if one exists.
"""
self.headers[name.lower()] = value
def get_header(self, name, default=None):
"""get_header(name : string, default=None) -> value : string
Gets an HTTP return header "name". If none exists then 'default' is
returned.
"""
return self.headers.get(name.lower(), default)
def set_expires(self, seconds=0, minutes=0, hours=0, days=0):
if seconds is None:
self.cache = None # don't generate 'Expires' header
else:
self.cache = seconds + 60*(minutes + 60*(hours + 24*days))
def _encode_chunk(self, chunk):
"""(chunk : str | unicode) -> str
"""
if isinstance(chunk, unicode):
if self.charset is None:
# iso-8859-1 is the default for the HTTP protocol if charset
# parameter of content-type header is not provided
chunk = chunk.encode('iso-8859-1')
else:
chunk = chunk.encode(self.charset)
else:
# we assume that the str is in the correct encoding or does
# not contain character data
pass
return chunk
def _compress_body(self, body):
"""(body: str) -> str
"""
n = len(body)
co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS,
zlib.DEF_MEM_LEVEL, 0)
chunks = [_GZIP_HEADER,
co.compress(body),
co.flush(),
struct.pack("<ll", zlib.crc32(body), n)]
compressed_body = "".join(chunks)
ratio = float(n) / len(compressed_body)
#print "gzip original size %d, ratio %.1f" % (n, ratio)
if ratio > 1.0:
self.set_header("Content-Encoding", "gzip")
return compressed_body
else:
return body
def set_body(self, body, compress=False):
"""(body : any, compress : bool = False)
Sets the response body equal to the argument 'body'. If 'compress'
is true then the body may be compressed using 'gzip'.
"""
if not isinstance(body, Stream):
body = self._encode_chunk(stringify(body))
if compress and self.content_type not in _GZIP_EXCLUDE:
body = self._compress_body(body)
self.body = body
def expire_cookie(self, name, **attrs):
"""
Cause an HTTP cookie to be removed from the browser
The response will include an HTTP header that will remove the cookie
corresponding to "name" on the client, if one exists. This is
accomplished by sending a new cookie with an expiration date
that has already passed. Note that some clients require a path
to be specified - this path must exactly match the path given
when creating the cookie. The path can be specified as a keyword
argument.
"""
dict = {'max_age': 0, 'expires': 'Thu, 01-Jan-1970 00:00:00 GMT'}
dict.update(attrs)
self.set_cookie(name, "deleted", **dict)
def set_cookie(self, name, value, **attrs):
"""set_cookie(name : string, value : string, **attrs)
Set an HTTP cookie on the browser.
The response will include an HTTP header that sets a cookie on
cookie-enabled browsers with a key "name" and value "value".
Cookie attributes such as "expires" and "domains" may be
supplied as keyword arguments; see RFC 2109 for a full list.
(For the "secure" attribute, use any true value.)
This overrides any previous value for this cookie. Any
previously-set attributes for the cookie are preserved, unless
they are explicitly overridden with keyword arguments to this
call.
"""
cookies = self.cookies
if cookies.has_key(name):
cookie = cookies[name]
else:
cookie = cookies[name] = {}
cookie.update(attrs)
cookie['value'] = value
def add_javascript(self, code_id, code):
"""Add javascript code to be included in the response.
code_id is used to ensure that the same piece of code is not
included twice. The caller must be careful to avoid
unintentional code_id and javascript identifier collisions.
Note that the response object only provides a mechanism for
collecting code -- actually including it in the HTML document
that is the response body is somebody else's problem. (For
an example, see Form._render_javascript().)
"""
if self.javascript_code is None:
self.javascript_code = {code_id: code}
elif not self.javascript_code.has_key(code_id):
self.javascript_code[code_id] = code
def redirect(self, location, permanent=False):
"""Cause a redirection without raising an error"""
if not isinstance(location, str):
raise TypeError, "location must be a string (got %s)" % `location`
# Ensure that location is a full URL
if location.find('://') == -1:
raise ValueError, "URL must include the server name"
if permanent:
status = 301
else:
status = 302
self.set_status(status)
self.headers['location'] = location
self.set_content_type('text/plain')
return "Your browser should have redirected you to %s" % location
def get_status_code(self):
return self.status_code
def get_reason_phrase(self):
return self.reason_phrase
def get_content_type(self):
return self.content_type
def get_content_length(self):
if self.body is None:
return None
elif isinstance(self.body, Stream):
return self.body.length
else:
return len(self.body)
def _gen_cookie_headers(self):
"""_gen_cookie_headers() -> [string]
Build a list of "Set-Cookie" headers based on all cookies
set with 'set_cookie()', and return that list.
"""
cookie_headers = []
for name, attrs in self.cookies.items():
value = str(attrs['value'])
if '"' in value:
value = value.replace('"', '\\"')
chunks = ['%s="%s"' % (name, value)]
for name, val in attrs.items():
name = name.lower()
if val is None:
continue
if name in ('expires', 'domain', 'path', 'max_age', 'comment'):
name = name.replace('_', '-')
chunks.append('%s=%s' % (name, val))
elif name == 'secure' and val:
chunks.append("secure")
cookie_headers.append(("Set-Cookie", '; '.join(chunks)))
return cookie_headers
def generate_headers(self):
"""generate_headers() -> [(name:string, value:string)]
Generate a list of headers to be returned as part of the response.
"""
headers = []
for name, value in self.headers.items():
headers.append((name.title(), value))
# All the "Set-Cookie" headers.
if self.cookies:
headers.extend(self._gen_cookie_headers())
# Date header
now = time.time()
if "date" not in self.headers:
headers.append(("Date", formatdate(now)))
# Cache directives
if self.cache is None:
pass # don't mess with the expires header
elif "expires" not in self.headers:
if self.cache > 0:
expire_date = formatdate(now + self.cache)
else:
expire_date = "-1" # allowed by HTTP spec and may work better
# with some clients
headers.append(("Expires", expire_date))
# Content-type
if "content-type" not in self.headers:
if self.charset is not None:
value = '%s; charset=%s' % (self.content_type, self.charset)
else:
value = '%s' % self.content_type
headers.append(('Content-Type', value))
# Content-Length
if "content-length" not in self.headers:
length = self.get_content_length()
if length is not None:
headers.append(('Content-Length', str(length)))
return headers
def generate_body_chunks(self):
"""Return a sequence of body chunks, encoded using 'charset'.
"""
if self.body is None:
pass
elif isinstance(self.body, Stream):
for chunk in self.body:
yield self._encode_chunk(chunk)
else:
yield self.body # already encoded
def write(self, output, include_status=True, include_body=True):
"""(output:file, include_status:bool=True, include_body:bool=True)
Write the HTTP response headers and, by default, body to 'output'.
This is not a complete HTTP response, as it doesn't start with a
response status line as specified by RFC 2616. By default, it
does start with a "Status" header as described by the CGI spec.
It is expected that this response is parsed by the web server and
turned into a complete HTTP response. If include_body is False,
only the headers are written to 'output'. This is used to support
HTTP HEAD requests.
"""
flush_output = not self.buffered and hasattr(output, 'flush')
if include_status:
# "Status" header must come first.
output.write("Status: %03d %s\r\n" % (self.status_code,
self.reason_phrase))
for name, value in self.generate_headers():
output.write("%s: %s\r\n" % (name, value))
output.write("\r\n")
if flush_output:
output.flush()
if not include_body:
return
for chunk in self.generate_body_chunks():
output.write(chunk)
if flush_output:
output.flush()
if flush_output:
output.flush()
class Stream:
"""
A wrapper around response data that can be streamed. The 'iterable'
argument must support the iteration protocol. Items returned by 'next()'
must be strings. Beware that exceptions raised while writing the stream
will not be handled gracefully.
Instance attributes:
iterable : any
an object that supports the iteration protocol. The items produced
by the stream must be strings.
length: int | None
the number of bytes that will be produced by the stream, None
if it is not known. Used to set the Content-Length header.
"""
def __init__(self, iterable, length=None):
self.iterable = iterable
self.length = length
def __iter__(self):
return iter(self.iterable)

107
logger.py Normal file
View File

@ -0,0 +1,107 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/logger.py $
$Id: logger.py 27259 2005-08-30 18:30:30Z dbinger $
"""
import sys
import os
import codecs
import time
import socket
import quixote
from quixote.sendmail import sendmail
class DefaultLogger:
"""
This is the default logger object used by the Quixote publisher. It
controls access log and error log behavior. You may provide your own
object if you wish to have different behavior.
Instance attributes:
access_log : file | None
file to which every access will be logged. If None then access
is not logged.
error_log : file
file to which application errors (exceptions caught by Quixote,
as well as anything printed to stderr by application code) will
be logged. Set to sys.stderr by default.
error_email : string | None
if set then internal server errors will cause messages to be sent to
this address
"""
DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET
def __init__(self, access_log=None, error_log=None, error_email=None):
if access_log:
self.access_log = self._open_log(access_log)
else:
self.access_log = None
if error_log is None:
self.error_log = sys.stderr
else:
self.error_log = self._open_log(error_log)
self.error_email = error_email
sys.stdout = self.error_log # print is handy for debugging
def _open_log(self, filename):
charset = self.DEFAULT_CHARSET or quixote.DEFAULT_CHARSET
if charset == 'iso-8859-1':
return open(filename, 'ab', 1)
else:
return codecs.open(filename, 'ab',
encoding=charset,
buffering=1)
def log(self, msg):
"""
Write an message to the error log with a time stamp.
"""
timestamp = time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time()))
self.error_log.write("[%s] %s%s" % (timestamp, msg, os.linesep))
def log_internal_error(self, error_summary, error_msg):
"""(error_summary: str, error_msg: str)
error_summary is a single line summary of the internal error, suitable
for an email subject. error_msg is a multi-line plaintext message
describing the error in detail.
"""
self.log("exception caught")
self.error_log.write(error_msg)
if self.error_email:
sendmail('Quixote Traceback (%s)' % error_summary,
error_msg, [self.error_email],
from_addr=(self.error_email, socket.gethostname()))
def log_request(self, request, start_time):
"""Log a request in the access_log file.
"""
if self.access_log is None:
return
if request.session:
user = request.session.user or "-"
else:
user = "-"
now = time.time()
seconds = now - start_time
timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now))
request_uri = request.get_path()
query = request.get_query()
if query:
request_uri += "?" + query
proto = request.get_environ('SERVER_PROTOCOL')
self.access_log.write('%s %s %s %d "%s %s %s" %s %r %0.2fsec%s' %
(request.get_environ('REMOTE_ADDR'),
user,
timestamp,
os.getpid(),
request.get_method(),
request_uri,
proto,
request.response.status_code,
request.get_environ('HTTP_USER_AGENT', ''),
seconds,
os.linesep,
))

245
ptl/__init__.py Normal file
View File

@ -0,0 +1,245 @@
'''
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/__init__.py $
$Id: __init__.py 26357 2005-03-16 14:56:23Z dbinger $
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``.
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 ``.pyc``. Since vanilla Python
doesn't know anything about PTL, this package provides an import hook to let
you import PTL files just like regular Python modules. The import
hook is installed when you import *this* package.
(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. A similar problem has been reported for
BioPython and win32com.client imports.)
'''

483
ptl/cimport.c Normal file
View File

@ -0,0 +1,483 @@
/* Mostly stolen from Python/import.c. PSF license applies. */
#include "Python.h"
#include "osdefs.h"
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
/* Python function to find and load a module. */
static PyObject *loader_hook;
PyObject *
call_find_load(char *fullname, char *subname, PyObject *path)
{
PyObject *args, *m;
if (!(args = Py_BuildValue("(ssO)", fullname, subname,
path != NULL ? path : Py_None)))
return NULL;
m = PyEval_CallObject(loader_hook, args);
Py_DECREF(args);
return m;
}
/* Forward declarations for helper routines */
static PyObject *get_parent(PyObject *globals, char *buf, int *p_buflen);
static PyObject *load_next(PyObject *mod, PyObject *altmod,
char **p_name, char *buf, int *p_buflen);
static int mark_miss(char *name);
static int ensure_fromlist(PyObject *mod, PyObject *fromlist,
char *buf, int buflen, int recursive);
static PyObject * import_submodule(PyObject *mod, char *name, char *fullname);
static PyObject *
import_module(char *name, PyObject *globals, PyObject *locals,
PyObject *fromlist)
{
char buf[MAXPATHLEN+1];
int buflen = 0;
PyObject *parent, *head, *next, *tail;
parent = get_parent(globals, buf, &buflen);
if (parent == NULL)
return NULL;
head = load_next(parent, Py_None, &name, buf, &buflen);
if (head == NULL)
return NULL;
tail = head;
Py_INCREF(tail);
while (name) {
next = load_next(tail, tail, &name, buf, &buflen);
Py_DECREF(tail);
if (next == NULL) {
Py_DECREF(head);
return NULL;
}
tail = next;
}
if (fromlist != NULL) {
if (fromlist == Py_None || !PyObject_IsTrue(fromlist))
fromlist = NULL;
}
if (fromlist == NULL) {
Py_DECREF(tail);
return head;
}
Py_DECREF(head);
if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) {
Py_DECREF(tail);
return NULL;
}
return tail;
}
static PyObject *
get_parent(PyObject *globals, char *buf, int *p_buflen)
{
static PyObject *namestr = NULL;
static PyObject *pathstr = NULL;
PyObject *modname, *modpath, *modules, *parent;
if (globals == NULL || !PyDict_Check(globals))
return Py_None;
if (namestr == NULL) {
namestr = PyString_InternFromString("__name__");
if (namestr == NULL)
return NULL;
}
if (pathstr == NULL) {
pathstr = PyString_InternFromString("__path__");
if (pathstr == NULL)
return NULL;
}
*buf = '\0';
*p_buflen = 0;
modname = PyDict_GetItem(globals, namestr);
if (modname == NULL || !PyString_Check(modname))
return Py_None;
modpath = PyDict_GetItem(globals, pathstr);
if (modpath != NULL) {
int len = PyString_GET_SIZE(modname);
if (len > MAXPATHLEN) {
PyErr_SetString(PyExc_ValueError,
"Module name too long");
return NULL;
}
strcpy(buf, PyString_AS_STRING(modname));
*p_buflen = len;
}
else {
char *start = PyString_AS_STRING(modname);
char *lastdot = strrchr(start, '.');
size_t len;
if (lastdot == NULL)
return Py_None;
len = lastdot - start;
if (len >= MAXPATHLEN) {
PyErr_SetString(PyExc_ValueError,
"Module name too long");
return NULL;
}
strncpy(buf, start, len);
buf[len] = '\0';
*p_buflen = len;
}
modules = PyImport_GetModuleDict();
parent = PyDict_GetItemString(modules, buf);
if (parent == NULL)
parent = Py_None;
return parent;
/* We expect, but can't guarantee, if parent != None, that:
- parent.__name__ == buf
- parent.__dict__ is globals
If this is violated... Who cares? */
}
/* altmod is either None or same as mod */
static PyObject *
load_next(PyObject *mod, PyObject *altmod, char **p_name, char *buf,
int *p_buflen)
{
char *name = *p_name;
char *dot = strchr(name, '.');
size_t len;
char *p;
PyObject *result;
if (dot == NULL) {
*p_name = NULL;
len = strlen(name);
}
else {
*p_name = dot+1;
len = dot-name;
}
if (len == 0) {
PyErr_SetString(PyExc_ValueError,
"Empty module name");
return NULL;
}
p = buf + *p_buflen;
if (p != buf)
*p++ = '.';
if (p+len-buf >= MAXPATHLEN) {
PyErr_SetString(PyExc_ValueError,
"Module name too long");
return NULL;
}
strncpy(p, name, len);
p[len] = '\0';
*p_buflen = p+len-buf;
result = import_submodule(mod, p, buf);
if (result == Py_None && altmod != mod) {
Py_DECREF(result);
/* Here, altmod must be None and mod must not be None */
result = import_submodule(altmod, p, p);
if (result != NULL && result != Py_None) {
if (mark_miss(buf) != 0) {
Py_DECREF(result);
return NULL;
}
strncpy(buf, name, len);
buf[len] = '\0';
*p_buflen = len;
}
}
if (result == NULL)
return NULL;
if (result == Py_None) {
Py_DECREF(result);
PyErr_Format(PyExc_ImportError,
"No module named %.200s", name);
return NULL;
}
return result;
}
static int
mark_miss(char *name)
{
PyObject *modules = PyImport_GetModuleDict();
return PyDict_SetItemString(modules, name, Py_None);
}
static int
ensure_fromlist(PyObject *mod, PyObject *fromlist, char *buf, int buflen,
int recursive)
{
int i;
if (!PyObject_HasAttrString(mod, "__path__"))
return 1;
for (i = 0; ; i++) {
PyObject *item = PySequence_GetItem(fromlist, i);
int hasit;
if (item == NULL) {
if (PyErr_ExceptionMatches(PyExc_IndexError)) {
PyErr_Clear();
return 1;
}
return 0;
}
if (!PyString_Check(item)) {
PyErr_SetString(PyExc_TypeError,
"Item in ``from list'' not a string");
Py_DECREF(item);
return 0;
}
if (PyString_AS_STRING(item)[0] == '*') {
PyObject *all;
Py_DECREF(item);
/* See if the package defines __all__ */
if (recursive)
continue; /* Avoid endless recursion */
all = PyObject_GetAttrString(mod, "__all__");
if (all == NULL)
PyErr_Clear();
else {
if (!ensure_fromlist(mod, all, buf, buflen, 1))
return 0;
Py_DECREF(all);
}
continue;
}
hasit = PyObject_HasAttr(mod, item);
if (!hasit) {
char *subname = PyString_AS_STRING(item);
PyObject *submod;
char *p;
if (buflen + strlen(subname) >= MAXPATHLEN) {
PyErr_SetString(PyExc_ValueError,
"Module name too long");
Py_DECREF(item);
return 0;
}
p = buf + buflen;
*p++ = '.';
strcpy(p, subname);
submod = import_submodule(mod, subname, buf);
Py_XDECREF(submod);
if (submod == NULL) {
Py_DECREF(item);
return 0;
}
}
Py_DECREF(item);
}
/* NOTREACHED */
}
static PyObject *
import_submodule(PyObject *mod, char *subname, char *fullname)
{
PyObject *modules = PyImport_GetModuleDict();
PyObject *m;
/* Require:
if mod == None: subname == fullname
else: mod.__name__ + "." + subname == fullname
*/
if ((m = PyDict_GetItemString(modules, fullname)) != NULL) {
Py_INCREF(m);
}
else {
PyObject *path;
if (mod == Py_None)
path = NULL;
else {
path = PyObject_GetAttrString(mod, "__path__");
if (path == NULL) {
PyErr_Clear();
Py_INCREF(Py_None);
return Py_None;
}
}
m = call_find_load(fullname, subname, path);
if (m != NULL && m != Py_None && mod != Py_None) {
if (PyObject_SetAttrString(mod, subname, m) < 0) {
Py_DECREF(m);
m = NULL;
}
}
}
return m;
}
PyObject *
reload_module(PyObject *m)
{
PyObject *modules = PyImport_GetModuleDict();
PyObject *path = NULL;
char *name, *subname;
if (m == NULL || !PyModule_Check(m)) {
PyErr_SetString(PyExc_TypeError,
"reload_module() argument must be module");
return NULL;
}
name = PyModule_GetName(m);
if (name == NULL)
return NULL;
if (m != PyDict_GetItemString(modules, name)) {
PyErr_Format(PyExc_ImportError,
"reload(): module %.200s not in sys.modules",
name);
return NULL;
}
subname = strrchr(name, '.');
if (subname == NULL)
subname = name;
else {
PyObject *parentname, *parent;
parentname = PyString_FromStringAndSize(name, (subname-name));
if (parentname == NULL)
return NULL;
parent = PyDict_GetItem(modules, parentname);
Py_DECREF(parentname);
if (parent == NULL) {
PyErr_Format(PyExc_ImportError,
"reload(): parent %.200s not in sys.modules",
name);
return NULL;
}
subname++;
path = PyObject_GetAttrString(parent, "__path__");
if (path == NULL)
PyErr_Clear();
}
m = call_find_load(name, subname, path);
Py_XDECREF(path);
return m;
}
static PyObject *
cimport_import_module(PyObject *self, PyObject *args)
{
char *name;
PyObject *globals = NULL;
PyObject *locals = NULL;
PyObject *fromlist = NULL;
if (!PyArg_ParseTuple(args, "s|OOO:import_module", &name, &globals,
&locals, &fromlist))
return NULL;
return import_module(name, globals, locals, fromlist);
}
static PyObject *
cimport_reload_module(PyObject *self, PyObject *args)
{
PyObject *m;
if (!PyArg_ParseTuple(args, "O:reload_module", &m))
return NULL;
return reload_module(m);
}
static char doc_reload_module[] =
"reload(module) -> module\n\
\n\
Reload the module. The module must have been successfully imported before.";
static PyObject *
cimport_set_loader(PyObject *self, PyObject *args)
{
PyObject *l = NULL;
if (!PyArg_ParseTuple(args, "O:set_loader", &l))
return NULL;
if (!PyCallable_Check(l)) {
PyErr_SetString(PyExc_TypeError, "callable object needed");
return NULL;
}
Py_XDECREF(loader_hook);
loader_hook = l;
Py_INCREF(loader_hook);
Py_INCREF(Py_None);
return Py_None;
}
static char doc_set_loader[] = "\
Set the function that will be used to import modules.\n\
\n\
The function should should have the signature:\n\
\n\
loader(fullname : str, subname : str, path : [str] | None) -> module | None\n\
\n\
It should return the initialized module or None if it is not found.\n\
";
static PyObject *
cimport_get_loader(PyObject *self, PyObject *args)
{
if (!PyArg_ParseTuple(args, ":get_loader"))
return NULL;
Py_INCREF(loader_hook);
return loader_hook;
}
static char doc_get_loader[] = "\
Get the function that will be used to import modules.\n\
";
static char doc_import_module[] = "\
import_module(name, globals, locals, fromlist) -> module\n\
\n\
Import a module. The globals are only used to determine the context;\n\
they are not modified. The locals are currently unused. The fromlist\n\
should be a list of names to emulate ``from name import ...'', or an\n\
empty list to emulate ``import name''.\n\
\n\
When importing a module from a package, note that import_module('A.B', ...)\n\
returns package A when fromlist is empty, but its submodule B when\n\
fromlist is not empty.\n\
";
static PyMethodDef cimport_methods[] = {
{"import_module", cimport_import_module, 1, doc_import_module},
{"reload_module", cimport_reload_module, 1, doc_reload_module},
{"get_loader", cimport_get_loader, 1, doc_get_loader},
{"set_loader", cimport_set_loader, 1, doc_set_loader},
{NULL, NULL} /* sentinel */
};
void
initcimport(void)
{
PyObject *m, *d;
m = Py_InitModule4("cimport", cimport_methods, "",
NULL, PYTHON_API_VERSION);
d = PyModule_GetDict(m);
}

2
ptl/install.py Normal file
View File

@ -0,0 +1,2 @@
import quixote.ptl.ptl_import
quixote.ptl.ptl_import.install()

308
ptl/ptl_compile.py Normal file
View File

@ -0,0 +1,308 @@
#!/www/python/bin/python
"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/ptl_compile.py $
$Id: ptl_compile.py 26903 2005-06-06 11:11:49Z dbinger $
Compile a PTL template.
First template function names are mangled, noting the template type.
Next, the file is parsed into a parse tree. This tree is converted into
a modified AST. It is during this state that the semantics are modified
by adding extra nodes to the tree. Finally bytecode is generated using
the compiler package.
"""
import sys
import os
import stat
import symbol
import token
import re
import imp
import stat
import marshal
import struct
assert sys.hexversion >= 0x20300b1, 'PTL requires Python 2.3 or newer'
from compiler import pycodegen, transformer
from compiler import ast
from compiler.consts import OP_ASSIGN
from compiler import misc, syntax
HTML_TEMPLATE_PREFIX = "_q_html_template_"
PLAIN_TEMPLATE_PREFIX = "_q_plain_template_"
class TemplateTransformer(transformer.Transformer):
def __init__(self, *args, **kwargs):
transformer.Transformer.__init__(self, *args, **kwargs)
# __template_type is a stack whose values are
# "html", "plain", or None
self.__template_type = []
def _get_template_type(self):
"""Return the type of the function being compiled (
"html", "plain", or None)
"""
if self.__template_type:
return self.__template_type[-1]
else:
return None
def file_input(self, nodelist):
doc = None # self.get_docstring(nodelist, symbol.file_input)
html_imp = ast.From('quixote.html', [('TemplateIO', '_q_TemplateIO'),
('htmltext', '_q_htmltext')])
vars_imp = ast.From("__builtin__", [("vars", "_q_vars")])
stmts = [ vars_imp, html_imp ]
for node in nodelist:
if node[0] != token.ENDMARKER and node[0] != token.NEWLINE:
self.com_append_stmt(stmts, node)
return ast.Module(doc, ast.Stmt(stmts))
def funcdef(self, nodelist):
if len(nodelist) == 6:
assert nodelist[0][0] == symbol.decorators
decorators = self.decorators(nodelist[0][1:])
else:
assert len(nodelist) == 5
decorators = None
lineno = nodelist[-4][2]
name = nodelist[-4][1]
args = nodelist[-3][2]
if not re.match('_q_(html|plain)_(dollar_)?template_', name):
# just a normal function, let base class handle it
self.__template_type.append(None)
n = transformer.Transformer.funcdef(self, nodelist)
else:
if name.startswith(PLAIN_TEMPLATE_PREFIX):
name = name[len(PLAIN_TEMPLATE_PREFIX):]
template_type = "plain"
elif name.startswith(HTML_TEMPLATE_PREFIX):
name = name[len(HTML_TEMPLATE_PREFIX):]
template_type = "html"
else:
raise RuntimeError, 'unknown prefix on %s' % name
self.__template_type.append(template_type)
if args[0] == symbol.varargslist:
names, defaults, flags = self.com_arglist(args[1:])
else:
names = defaults = ()
flags = 0
doc = None # self.get_docstring(nodelist[-1])
# code for function
code = self.com_node(nodelist[-1])
# _q_output = _q_TemplateIO()
klass = ast.Name('_q_TemplateIO')
args = [ast.Const(template_type == "html")]
instance = ast.CallFunc(klass, args)
assign_name = ast.AssName('_q_output', OP_ASSIGN)
assign = ast.Assign([assign_name], instance)
# return _q_output.getvalue()
func = ast.Getattr(ast.Name('_q_output'), "getvalue")
ret = ast.Return(ast.CallFunc(func, []))
# wrap original function code
code = ast.Stmt([assign, code, ret])
if sys.hexversion >= 0x20400a2:
n = ast.Function(decorators, name, names, defaults, flags, doc,
code)
else:
n = ast.Function(name, names, defaults, flags, doc, code)
n.lineno = lineno
self.__template_type.pop()
return n
def expr_stmt(self, nodelist):
if self._get_template_type() is None:
return transformer.Transformer.expr_stmt(self, nodelist)
# Instead of discarding objects on the stack, call
# "_q_output += obj".
exprNode = self.com_node(nodelist[-1])
if len(nodelist) == 1:
lval = ast.Name('_q_output')
n = ast.AugAssign(lval, '+=', exprNode)
if hasattr(exprNode, 'lineno'):
n.lineno = exprNode.lineno
elif nodelist[1][0] == token.EQUAL:
nodes = [ ]
for i in range(0, len(nodelist) - 2, 2):
nodes.append(self.com_assign(nodelist[i], OP_ASSIGN))
n = ast.Assign(nodes, exprNode)
n.lineno = nodelist[1][2]
else:
lval = self.com_augassign(nodelist[0])
op = self.com_augassign_op(nodelist[1])
n = ast.AugAssign(lval, op[1], exprNode)
n.lineno = op[2]
return n
def atom_string(self, nodelist):
const_node = transformer.Transformer.atom_string(self, nodelist)
if "html" == self._get_template_type():
return ast.CallFunc(ast.Name('_q_htmltext'), [const_node])
else:
return const_node
_template_re = re.compile(
r"^(?P<indent>[ \t]*) def (?:[ \t]+)"
r" (?P<name>[a-zA-Z_][a-zA-Z_0-9]*)"
r" (?:[ \t]*) \[(?P<type>plain|html)\] (?:[ \t]*)"
r" (?:[ \t]*[\(\\])",
re.MULTILINE|re.VERBOSE)
def translate_tokens(buf):
"""
Since we can't modify the parser in the builtin parser module we
must do token translation here. Luckily it does not affect line
numbers.
def foo [plain] (...): -> def _q_plain_template__foo(...):
def foo [html] (...): -> def _q_html_template__foo(...):
XXX This parser is too stupid. For example, it doesn't understand
triple quoted strings.
"""
def replacement(match):
template_type = match.group('type')
return '%sdef _q_%s_template_%s(' % (match.group('indent'),
template_type,
match.group('name'))
return _template_re.sub(replacement, buf)
def parse(buf, filename='<string>'):
buf = translate_tokens(buf)
try:
return TemplateTransformer().parsesuite(buf)
except SyntaxError, e:
# set the filename attribute
raise SyntaxError(str(e), (filename, e.lineno, e.offset, e.text))
PTL_EXT = ".ptl"
class Template(pycodegen.Module):
def _get_tree(self):
tree = parse(self.source, self.filename)
misc.set_filename(self.filename, tree)
syntax.check(tree)
return tree
def dump(self, fp):
mtime = os.stat(self.filename)[stat.ST_MTIME]
fp.write('\0\0\0\0')
fp.write(struct.pack('<I', mtime))
marshal.dump(self.code, fp)
fp.flush()
fp.seek(0)
fp.write(imp.get_magic())
def compile_template(input, filename, output=None):
"""(input, filename, output=None) -> code
Compile an open file.
If output is not None then the code is written to output.
The code object is returned.
"""
buf = input.read()
template = Template(buf, filename)
template.compile()
if output is not None:
template.dump(output)
return template.code
def compile(inputname, outputname):
"""(inputname, outputname)
Compile a template file. The new template is written to outputname.
"""
input = open(inputname)
output = open(outputname, "wb")
try:
compile_template(input, inputname, output)
except:
# don't leave a corrupt .pyc file around
output.close()
os.unlink(outputname)
raise
def compile_dir(dir, maxlevels=10, force=0):
"""Byte-compile all PTL modules in the given directory tree.
(Adapted from compile_dir in Python module: compileall.py)
Arguments (only dir is required):
dir: the directory to byte-compile
maxlevels: maximum recursion level (default 10)
force: if true, force compilation, even if timestamps are up-to-date
"""
print 'Listing', dir, '...'
try:
names = os.listdir(dir)
except os.error:
print "Can't list", dir
names = []
names.sort()
success = 1
for name in names:
fullname = os.path.join(dir, name)
if os.path.isfile(fullname):
head, tail = name[:-4], name[-4:]
if tail == PTL_EXT:
cfile = fullname[:-4] + '.pyc'
ftime = os.stat(fullname)[stat.ST_MTIME]
try:
ctime = os.stat(cfile)[stat.ST_MTIME]
except os.error: ctime = 0
if (ctime > ftime) and not force:
continue
print 'Compiling', fullname, '...'
try:
ok = compile(fullname, cfile)
except KeyboardInterrupt:
raise KeyboardInterrupt
except:
# XXX compile catches SyntaxErrors
if type(sys.exc_type) == type(''):
exc_type_name = sys.exc_type
else: exc_type_name = sys.exc_type.__name__
print 'Sorry:', exc_type_name + ':',
print sys.exc_value
success = 0
else:
if ok == 0:
success = 0
elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
os.path.isdir(fullname) and not os.path.islink(fullname)):
if not compile_dir(fullname, maxlevels - 1, force):
success = 0
return success
def main():
args = sys.argv[1:]
if not args:
print "no files to compile"
else:
for filename in args:
path, ext = os.path.splitext(filename)
compile(filename, path + ".pyc")
if __name__ == "__main__":
main()

148
ptl/ptl_import.py Normal file
View File

@ -0,0 +1,148 @@
"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/ptl_import.py $
$Id: ptl_import.py 27263 2005-08-31 16:09:13Z dbinger $
Import hooks; when installed, these hooks allow importing .ptl files
as if they were Python modules.
Note: there's some unpleasant incompatibility between ZODB's import
trickery and the import hooks here. Bottom line: if you're using ZODB,
import it *before* installing the PTL import hooks.
"""
import sys
import os.path
import imp, ihooks, new
import struct
import marshal
import __builtin__
from quixote.ptl.ptl_compile import compile_template, PTL_EXT
assert sys.hexversion >= 0x20000b1, "need Python 2.0b1 or later"
def _exec_module_code(code, name, filename):
if sys.modules.has_key(name):
mod = sys.modules[name] # necessary for reload()
else:
mod = new.module(name)
sys.modules[name] = mod
mod.__name__ = name
mod.__file__ = filename
exec code in mod.__dict__
return mod
def _timestamp(filename):
try:
s = os.stat(filename)
except OSError:
return None
return s.st_mtime
def _load_pyc(name, filename, pyc_filename):
try:
fp = open(pyc_filename, "rb")
except IOError:
return None
if fp.read(4) == imp.get_magic():
mtime = struct.unpack('<I', fp.read(4))[0]
ptl_mtime = _timestamp(filename)
if ptl_mtime is not None and mtime >= ptl_mtime:
code = marshal.load(fp)
return _exec_module_code(code, name, filename)
return None
def _load_ptl(name, filename, file=None):
if not file:
try:
file = open(filename, "rb")
except IOError:
return None
path, ext = os.path.splitext(filename)
pyc_filename = path + ".pyc"
module = _load_pyc(name, filename, pyc_filename)
if module is not None:
return module
try:
output = open(pyc_filename, "wb")
except IOError:
output = None
try:
code = compile_template(file, filename, output)
except:
if output:
output.close()
os.unlink(pyc_filename)
raise
else:
if output:
output.close()
return _exec_module_code(code, name, filename)
# Constant used to signal a PTL files
PTL_FILE = object()
class PTLHooks(ihooks.Hooks):
def get_suffixes(self):
# add our suffixes
return [(PTL_EXT, 'r', PTL_FILE)] + imp.get_suffixes()
class PTLLoader(ihooks.ModuleLoader):
def load_module(self, name, stuff):
file, filename, info = stuff
(suff, mode, type) = info
# If it's a PTL file, load it specially.
if type is PTL_FILE:
return _load_ptl(name, filename, file)
else:
# Otherwise, use the default handler for loading
return ihooks.ModuleLoader.load_module(self, name, stuff)
try:
import cimport
except ImportError:
cimport = None
class cModuleImporter(ihooks.ModuleImporter):
def __init__(self, loader=None):
self.loader = loader or ihooks.ModuleLoader()
cimport.set_loader(self.find_import_module)
def find_import_module(self, fullname, subname, path):
stuff = self.loader.find_module(subname, path)
if not stuff:
return None
return self.loader.load_module(fullname, stuff)
def install(self):
self.save_import_module = __builtin__.__import__
self.save_reload = __builtin__.reload
if not hasattr(__builtin__, 'unload'):
__builtin__.unload = None
self.save_unload = __builtin__.unload
__builtin__.__import__ = cimport.import_module
__builtin__.reload = cimport.reload_module
__builtin__.unload = self.unload
_installed = False
def install():
global _installed
if not _installed:
hooks = PTLHooks()
loader = PTLLoader(hooks)
if cimport is not None:
importer = cModuleImporter(loader)
else:
importer = ihooks.ModuleImporter(loader)
ihooks.install(importer)
_installed = True
if __name__ == '__main__':
install()

6
ptl/ptlrun.py Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env python
import sys
from quixote.ptl.ptl_compile import compile_template
if __name__ == '__main__':
exec compile_template(open(sys.argv[1]), sys.argv[1])

47
ptl/qx_distutils.py Normal file
View File

@ -0,0 +1,47 @@
"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/qx_distutils.py $
$Id: qx_distutils.py 26357 2005-03-16 14:56:23Z dbinger $
Provides a version of the Distutils "build_py" command that knows about
PTL files.
"""
import os, string
from glob import glob
from types import StringType, ListType, TupleType
from distutils.command.build_py import build_py
class qx_build_py(build_py):
def find_package_modules(self, package, package_dir):
self.check_package(package, package_dir)
module_files = (glob(os.path.join(package_dir, "*.py")) +
glob(os.path.join(package_dir, "*.ptl")))
modules = []
setup_script = os.path.abspath(self.distribution.script_name)
for f in module_files:
abs_f = os.path.abspath(f)
if abs_f != setup_script:
module = os.path.splitext(os.path.basename(f))[0]
modules.append((package, module, f))
else:
self.debug_print("excluding %s" % setup_script)
return modules
def build_module(self, module, module_file, package):
if type(package) is StringType:
package = string.split(package, '.')
elif type(package) not in (ListType, TupleType):
raise TypeError, \
"'package' must be a string (dot-separated), list, or tuple"
# Now put the module source file into the "build" area -- this is
# easy, we just copy it somewhere under self.build_lib (the build
# directory for Python source).
outfile = self.get_module_outfile(self.build_lib, package, module)
if module_file.endswith(".ptl"): # XXX hack for PTL
outfile = outfile[0:outfile.rfind('.')] + ".ptl"
dir = os.path.dirname(outfile)
self.mkpath(dir)
return self.copy_file(module_file, outfile, preserve_mode=0)

58
ptl/test/utest_ptl.py Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
from sancho.utest import UTest
from quixote.ptl.ptl_compile import compile_template
from cStringIO import StringIO
from quixote.html import TemplateIO, htmltext
def run_ptl(*source):
"""
Compile the given lines of source code using the ptl compiler
and run the resulting compiled code.
"""
# When the ptl compiler compiles a module, it places _q_TemplateIO
# and _q_htmltext into the globals of the module. Here, we don't
# have a module, but we provide these same globals for eval.
eval(compile_template(StringIO('\n'.join(source)), 'test'),
dict(_q_TemplateIO=TemplateIO, _q_htmltext=htmltext))
class Test (UTest):
def check_html(self):
run_ptl(
'from quixote.html import htmltext',
'def f [html] (a):',
' "&"',
' a',
'assert type(f(1)) == htmltext',
'assert f("") == "&"',
'assert f("&") == "&&amp;"',
'assert f(htmltext("&")) == "&&"')
def check_plain(self):
run_ptl(
'from quixote.html import htmltext',
'def f [plain] (a):',
' "&"',
' a',
'assert type(f(1)) == str',
'assert f("") == "&"',
'assert f("&") == "&&"',
'assert f(htmltext("&")) == "&&"',
'assert type(f(htmltext("&"))) == str')
def check_syntax(self):
run_ptl('def f(a):\n a')
try:
run_ptl('def f [] (a):\n a')
assert 0
except SyntaxError, e:
assert e.lineno == 1
try:
run_ptl('def f [HTML] (a):\n a')
assert 0
except SyntaxError, e:
assert e.lineno == 1
if __name__ == "__main__":
Test()

354
publish.py Normal file
View File

@ -0,0 +1,354 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/publish.py $
$Id: publish.py 27684 2005-11-10 15:25:17Z dbinger $
Logic for publishing modules and objects on the Web.
"""
import sys, traceback, StringIO
import time
import urlparse
import cgitb
from quixote.errors import PublishError, format_publish_error
from quixote import util
from quixote.config import Config
from quixote.http_response import HTTPResponse
from quixote.http_request import HTTPRequest
from quixote.logger import DefaultLogger
# Error message to dispay when DISPLAY_EXCEPTIONS in config file is not
# true. Note that SERVER_ADMIN must be fetched from the environment and
# plugged in here -- we can't do it now because the environment isn't
# really setup for us yet if running as a FastCGI script.
INTERNAL_ERROR_MESSAGE = """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
"http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head><title>Internal Server Error</title></head>
<body>
<h1>Internal Server Error</h1>
<p>An internal error occurred while handling your request.</p>
<p>The server administrator should have been notified of the problem.
You may wish to contact the server administrator (%s) and inform them of
the time the error occurred, and anything you might have done to trigger
the error.</p>
<p>If you are the server administrator, more information may be
available in either the server's error log or Quixote's error log.</p>
</body>
</html>
"""
class Publisher:
"""
The core of Quixote and of any Quixote application. This class is
responsible for converting each HTTP request into a traversal of the
application's directory tree and, ultimately, a call of a Python
function/method/callable object.
Each invocation of a driver script should have one Publisher
instance that lives for as long as the driver script itself. Eg. if
your driver script is plain CGI, each Publisher instance will handle
exactly one HTTP request; if you have a FastCGI driver, then each
Publisher will handle every HTTP request handed to that driver
script process.
Instance attributes:
root_directory : Directory
the root directory that will be searched for objects to fulfill
each request. This can be any object with a _q_traverse method
that acts like Directory._q_traverse.
logger : DefaultLogger
controls access log and error log behavior
session_manager : NullSessionManager
keeps track of sessions
config : Config
holds all configuration info for this application. If the
application doesn't provide values then default values
from the quixote.config module are used.
_request : HTTPRequest
the HTTP request currently being processed.
"""
def __init__(self, root_directory, logger=None, session_manager=None,
config=None, **kwargs):
global _publisher
if config is None:
self.config = Config(**kwargs)
else:
if kwargs:
raise ValueError("cannot provide both 'config' object and"
" config arguments")
self.config = config
if logger is None:
self.logger = DefaultLogger(error_log=self.config.error_log,
access_log=self.config.access_log,
error_email=self.config.error_email)
else:
self.logger = logger
if session_manager is not None:
self.session_manager = session_manager
else:
from quixote.session import NullSessionManager
self.session_manager = NullSessionManager()
if _publisher is not None:
raise RuntimeError, "only one instance of Publisher allowed"
_publisher = self
if not callable(getattr(root_directory, '_q_traverse')):
raise TypeError(
'Expected something with a _q_traverse method, got %r' %
root_directory)
self.root_directory = root_directory
self._request = None
def set_session_manager(self, session_manager):
self.session_manager = session_manager
def log(self, msg):
self.logger.log(msg)
def parse_request(self, request):
"""Parse the request information waiting in 'request'.
"""
request.process_inputs()
def start_request(self):
"""Called at the start of each request.
"""
self.session_manager.start_request()
def _set_request(self, request):
"""Set the current request object.
"""
self._request = request
def _clear_request(self):
"""Unset the current request object.
"""
self._request = None
def get_request(self):
"""Return the current request object.
"""
return self._request
def finish_successful_request(self):
"""Called at the end of a successful request.
"""
self.session_manager.finish_successful_request()
def format_publish_error(self, exc):
return format_publish_error(exc)
def finish_interrupted_request(self, exc):
"""
Called at the end of an interrupted request. Requests are
interrupted by raising a PublishError exception. This method
should return a string object which will be used as the result of
the request.
"""
if not self.config.display_exceptions and exc.private_msg:
exc.private_msg = None # hide it
request = get_request()
request.response = HTTPResponse(status=exc.status_code)
output = self.format_publish_error(exc)
self.session_manager.finish_successful_request()
return output
def finish_failed_request(self):
"""
Called at the end of an failed request. Any exception (other
than PublishError) causes a request to fail. This method should
return a string object which will be used as the result of the
request.
"""
# build new response to be safe
request = get_request()
original_response = request.response
request.response = HTTPResponse()
#self.log("caught an error (%s), reporting it." %
# sys.exc_info()[1])
(exc_type, exc_value, tb) = sys.exc_info()
error_summary = traceback.format_exception_only(exc_type, exc_value)
error_summary = error_summary[0][0:-1] # de-listify and strip newline
plain_error_msg = self._generate_plaintext_error(request,
original_response,
exc_type, exc_value,
tb)
if not self.config.display_exceptions:
# DISPLAY_EXCEPTIONS is false, so return the most
# secure (and cryptic) page.
request.response.set_header("Content-Type", "text/html")
user_error_msg = self._generate_internal_error(request)
elif self.config.display_exceptions == 'html':
# Generate a spiffy HTML display using cgitb
request.response.set_header("Content-Type", "text/html")
user_error_msg = self._generate_cgitb_error(request,
original_response,
exc_type, exc_value,
tb)
else:
# Generate a plaintext page containing the traceback
request.response.set_header("Content-Type", "text/plain")
user_error_msg = plain_error_msg
self.logger.log_internal_error(error_summary, plain_error_msg)
if exc_type is SystemExit:
raise
request.response.set_status(500)
self.session_manager.finish_failed_request()
return user_error_msg
def _generate_internal_error(self, request):
admin = request.get_environ('SERVER_ADMIN',
"<i>email address unknown</i>")
return INTERNAL_ERROR_MESSAGE % admin
def _generate_plaintext_error(self, request, original_response,
exc_type, exc_value, tb):
error_file = StringIO.StringIO()
# format the traceback
traceback.print_exception(exc_type, exc_value, tb, file=error_file)
# include request and response dumps
error_file.write('\n')
error_file.write(request.dump())
error_file.write('\n')
return error_file.getvalue()
def _generate_cgitb_error(self, request, original_response,
exc_type, exc_value, tb):
error_file = StringIO.StringIO()
hook = cgitb.Hook(file=error_file)
hook(exc_type, exc_value, tb)
error_file.write('<h2>Original Request</h2>')
error_file.write(str(util.dump_request(request)))
error_file.write('<h2>Original Response</h2><pre>')
original_response.write(error_file)
error_file.write('</pre>')
return error_file.getvalue()
def try_publish(self, request):
"""(request : HTTPRequest) -> object
The master method that does all the work for a single request.
Exceptions are handled by the caller.
"""
self.start_request()
path = request.get_environ('PATH_INFO', '')
if path[:1] != '/':
return redirect(
request.get_environ('SCRIPT_NAME', '') + '/' + path,
permanent=True)
components = path[1:].split('/')
output = self.root_directory._q_traverse(components)
# The callable ran OK, commit any changes to the session
self.finish_successful_request()
return output
def filter_output(self, request, output):
"""Hook for post processing the output. Subclasses may wish to
override (e.g. check HTML syntax).
"""
return output
def process_request(self, request):
"""(request : HTTPRequest) -> HTTPResponse
Process a single request, given an HTTPRequest object. The
try_publish() method will be called to do the work and
exceptions will be handled here.
"""
self._set_request(request)
start_time = time.time()
try:
self.parse_request(request)
output = self.try_publish(request)
except PublishError, exc:
# Exit the publishing loop and return a result right away.
output = self.finish_interrupted_request(exc)
except:
# Some other exception, generate error messages to the logs, etc.
output = self.finish_failed_request()
output = self.filter_output(request, output)
self.logger.log_request(request, start_time)
if output:
if self.config.compress_pages and request.get_encoding(["gzip"]):
compress = True
else:
compress = False
request.response.set_body(output, compress)
self._clear_request()
return request.response
def process(self, stdin, env):
"""(stdin : stream, env : dict) -> HTTPResponse
Process a single request, given a stream, stdin, containing the
incoming request and a dictionary, env, containing the web server's
environment.
An HTTPRequest object is created and the process_request() method is
called and passed the request object.
"""
request = HTTPRequest(stdin, env)
return self.process_request(request)
# Publisher singleton, only one of these per process.
_publisher = None
def get_publisher():
return _publisher
def get_request():
return _publisher.get_request()
def get_response():
return _publisher.get_request().response
def get_field(name, default=None):
return _publisher.get_request().get_field(name, default)
def get_cookie(name, default=None):
return _publisher.get_request().get_cookie(name, default)
def get_path(n=0):
return _publisher.get_request().get_path(n)
def redirect(location, permanent=False):
"""(location : string, permanent : boolean = false) -> string
Create a redirection response. If the location is relative, then it
will automatically be made absolute. The return value is an HTML
document indicating the new URL (useful if the client browser does
not honor the redirect).
"""
request = _publisher.get_request()
location = urlparse.urljoin(request.get_url(), str(location))
return request.response.redirect(location, permanent)
def get_session():
return _publisher.get_request().session
def get_session_manager():
return _publisher.session_manager
def get_user():
session = _publisher.get_request().session
if session is None:
return None
else:
return session.user

270
publish1.py Normal file
View File

@ -0,0 +1,270 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/publish1.py $
$Id: publish1.py 25664 2004-11-22 20:35:07Z nascheme $
Provides a publisher object that behaves like the Quixote 1 Publisher.
Specifically, arbitrary namespaces may be exported and the HTTPRequest
object is passed as the first argument to exported functions. Also,
the _q_lookup(), _q_resolve(), and _q_access() methods work as they did
in Quixote 1.
"""
import sys
import re
import types
import warnings
from quixote import errors, get_request, redirect
from quixote.publish import Publisher as _Publisher
from quixote.directory import Directory
from quixote.html import htmltext
class Publisher(_Publisher):
"""
Instance attributes:
namespace_stack : [ module | instance | class ]
"""
def __init__(self, root_namespace, config=None):
from quixote.config import Config
if type(root_namespace) is types.StringType:
root_namespace = _get_module(root_namespace)
self.namespace_stack = [root_namespace]
if config is None:
config = Config()
directory = RootDirectory(root_namespace, self.namespace_stack)
_Publisher.__init__(self, directory, config=config)
def debug(self, msg):
self.log(msg)
def get_namespace_stack(self):
"""get_namespace_stack() -> [ module | instance | class ]
"""
return self.namespace_stack
class RootDirectory(Directory):
def __init__(self, root_namespace, namespace_stack):
self.root_namespace = root_namespace
self.namespace_stack = namespace_stack
def _q_traverse(self, path):
# Initialize the publisher's namespace_stack
del self.namespace_stack[:]
request = get_request()
# Traverse package to a (hopefully-) callable object
object = _traverse_url(self.root_namespace, path, request,
self.namespace_stack)
# None means no output -- traverse_url() just issued a redirect.
if object is None:
return None
# Anything else must be either a string...
if isstring(object):
output = object
# ...or a callable.
elif callable(object):
output = object(request)
if output is None:
raise RuntimeError, 'callable %s returned None' % repr(object)
# Uh-oh: 'object' is neither a string nor a callable.
else:
raise RuntimeError(
"object is neither callable nor a string: %s" % repr(object))
return output
def _get_module(name):
"""Get a module object by name."""
__import__(name)
module = sys.modules[name]
return module
_slash_pat = re.compile("//*")
def _traverse_url(root_namespace, path_components, request, namespace_stack):
"""(root_namespace : any, path_components : [string],
request : HTTPRequest, namespace_stack : list) -> (object : any)
Perform traversal based on the provided path, starting at the root
object. It returns the script name and path info values for
the arrived-at object, along with the object itself and
a list of the namespaces traversed to get there.
It's expected that the final object is something callable like a
function or a method; intermediate objects along the way will
usually be packages or modules.
To prevent crackers from writing URLs that traverse private
objects, every package, module, or object along the way must have
a _q_exports attribute containing a list of publicly visible
names. Not having a _q_exports attribute is an error, though
having _q_exports be an empty list is OK. If a component of the path
isn't in _q_exports, that also produces an error.
Modifies the namespace_stack as it traverses the url, so that
any exceptions encountered along the way can be handled by the
nearest handler.
"""
path = '/' + '/'.join(path_components)
# If someone accesses a Quixote driver script without a trailing
# slash, we'll wind up here with an empty path. This won't
# work; relative references in the page generated by the root
# namespace's _q_index() will be off. Fix it by redirecting the
# user to the right URL; when the client follows the redirect,
# we'll wind up here again with path == '/'.
if not path:
return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1)
# Traverse starting at the root
object = root_namespace
namespace_stack.append(object)
# Loop over the components of the path
for component in path_components:
if component == "":
# "/q/foo/" == "/q/foo/_q_index"
component = "_q_index"
object = _get_component(object, component, request, namespace_stack)
if not (isstring(object) or callable(object)):
# We went through all the components of the path and ended up at
# something which isn't callable, like a module or an instance
# without a __call__ method.
if path[-1] != '/':
if not request.form:
# This is for the convenience of users who type in paths.
# Repair the path and redirect. This should not happen for
# URLs within the site.
return redirect(request.get_path() + "/", permanent=1)
else:
# Automatic redirects disabled or there is form data. If
# there is form data then the programmer is using the
# wrong path. A redirect won't work if the form data came
# from a POST anyhow.
raise errors.TraversalError(
"object is neither callable nor string "
"(missing trailing slash?)",
private_msg=repr(object),
path=path)
else:
raise errors.TraversalError(
"object is neither callable nor string",
private_msg=repr(object),
path=path)
return object
def _get_component(container, component, request, namespace_stack):
"""Get one component of a path from a namespace.
"""
# First security check: if the container doesn't even have an
# _q_exports list, fail now: all Quixote-traversable namespaces
# (modules, packages, instances) must have an export list!
if not hasattr(container, '_q_exports'):
raise errors.TraversalError(
private_msg="%r has no _q_exports list" % container)
# Second security check: call _q_access function if it's present.
if hasattr(container, '_q_access'):
# will raise AccessError if access failed
container._q_access(request)
# Third security check: make sure the current name component
# is in the export list or is '_q_index'. If neither
# condition is true, check for a _q_lookup() and call it.
# '_q_lookup()' 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
# PATHINFO after Quixote's done with it. But it is a
# compromise to security: it opens up the traversal algorithm
# to arbitrary names not listed in _q_exports!) If
# _q_lookup() doesn't exist or is None, a TraversalError is
# raised.
# Check if component is in _q_exports. The elements in
# _q_exports can be strings or 2-tuples mapping external names
# to internal names.
if component in container._q_exports or component == '_q_index':
internal_name = component
else:
# check for an explicit external to internal mapping
for value in container._q_exports:
if type(value) is types.TupleType:
if value[0] == component:
internal_name = value[1]
break
else:
internal_name = None
if internal_name is None:
# Component is not in exports list.
object = None
if hasattr(container, "_q_lookup"):
object = container._q_lookup(request, component)
elif hasattr(container, "_q_getname"):
warnings.warn("_q_getname() on %s used; should "
"be replaced by _q_lookup()" % type(container))
object = container._q_getname(request, component)
if object is None:
raise errors.TraversalError(
private_msg="object %r has no attribute %r" % (
container,
component))
# From here on, you can assume that the internal_name is not None
elif hasattr(container, internal_name):
# attribute is in _q_exports and exists
object = getattr(container, internal_name)
elif internal_name == '_q_index':
if hasattr(container, "_q_lookup"):
object = container._q_lookup(request, "")
else:
raise errors.AccessError(
private_msg=("_q_index not found in %r" % container))
elif hasattr(container, "_q_resolve"):
object = container._q_resolve(internal_name)
if object is None:
raise RuntimeError, ("component listed in _q_exports, "
"but not returned by _q_resolve(%r)"
% internal_name)
else:
# Set the object, so _q_resolve won't need to be called again.
setattr(container, internal_name, object)
elif type(container) is types.ModuleType:
# try importing it as a sub-module. If we get an ImportError
# here we don't catch it. It means that something that
# doesn't exist was exported or an exception was raised from
# deeper in the code.
mod_name = container.__name__ + '.' + internal_name
object = _get_module(mod_name)
else:
# a non-existent attribute is in _q_exports,
# and the container is not a module. Give up.
raise errors.TraversalError(
private_msg=("%r in _q_exports list, "
"but not found in %r" % (component,
container)))
namespace_stack.append(object)
return object
def isstring(x):
return isinstance(x, (str, unicode, htmltext))

267
sendmail.py Normal file
View File

@ -0,0 +1,267 @@
"""quixote.sendmail
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/sendmail.py $
$Id: sendmail.py 27540 2005-10-12 13:15:58Z dbinger $
Tools for sending mail from Quixote applications.
"""
import re
from types import ListType, TupleType
from smtplib import SMTP
import quixote
rfc822_specials_re = re.compile(r'[\(\)\<\>\@\,\;\:\\\"\.\[\]]')
class RFC822Mailbox:
"""
In RFC 822, a "mailbox" is either a bare e-mail address or a bare
e-mail address coupled with a chunk of text, most often someone's
name. Eg. the following are all "mailboxes" in the RFC 822 grammar:
luser@example.com
Joe Luser <luser@example.com>
Paddy O'Reilly <paddy@example.ie>
"Smith, John" <smith@example.com>
Dick & Jane <dickjane@example.net>
"Tom, Dick, & Harry" <tdh@example.org>
This class represents an (addr_spec, real_name) pair and takes care
of quoting the real_name according to RFC 822's rules for you.
Just use the format() method and it will spit out a properly-
quoted RFC 822 "mailbox".
"""
def __init__(self, *args):
"""RFC822Mailbox(addr_spec : string, name : string)
RFC822Mailbox(addr_spec : string)
RFC822Mailbox((addr_spec : string, name : string))
RFC822Mailbox((addr_spec : string))
Create a new RFC822Mailbox instance. The variety of call
signatures is purely for your convenience.
"""
if (len(args) == 1 and type(args[0]) is TupleType):
args = args[0]
if len(args) == 1:
addr_spec = args[0]
real_name = None
elif len(args) == 2:
(addr_spec, real_name) = args
else:
raise TypeError(
"invalid number of arguments: "
"expected 1 or 2 strings or "
"a tuple of 1 or 2 strings")
self.addr_spec = addr_spec
self.real_name = real_name
def __str__(self):
return self.addr_spec
def __repr__(self):
return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self)
def format(self):
if self.real_name and rfc822_specials_re.search(self.real_name):
return '"%s" <%s>' % (self.real_name.replace('"', '\\"'),
self.addr_spec)
elif self.real_name:
return '%s <%s>' % (self.real_name, self.addr_spec)
else:
return self.addr_spec
def _ensure_mailbox(s):
"""_ensure_mailbox(s : string |
(string,) |
(string, string) |
RFC822Mailbox |
None)
-> RFC822Mailbox | None
If s is a string, or a tuple of 1 or 2 strings, returns an
RFC822Mailbox encapsulating them as an addr_spec and real_name. If
s is already an RFC822Mailbox, returns s. If s is None, returns
None.
"""
if s is None or isinstance(s, RFC822Mailbox):
return s
else:
return RFC822Mailbox(s)
# Maximum number of recipients that will be explicitly listed in
# any single message header. Eg. if MAX_HEADER_RECIPIENTS is 10,
# there could be up to 10 "To" recipients and 10 "CC" recipients
# explicitly listed in the message headers.
MAX_HEADER_RECIPIENTS = 10
def _add_recip_headers(headers, field_name, addrs):
if not addrs:
return
addrs = [addr.format() for addr in addrs]
if len(addrs) == 1:
headers.append("%s: %s" % (field_name, addrs[0]))
elif len(addrs) <= MAX_HEADER_RECIPIENTS:
headers.append("%s: %s," % (field_name, addrs[0]))
for addr in addrs[1:-1]:
headers.append(" %s," % addr)
headers.append(" %s" % addrs[-1])
else:
headers.append("%s: (long recipient list suppressed) : ;" % field_name)
def sendmail(subject, msg_body, to_addrs,
from_addr=None, cc_addrs=None,
extra_headers=None,
smtp_sender=None, smtp_recipients=None,
config=None):
"""sendmail(subject : string,
msg_body : string,
to_addrs : [email_address],
from_addr : email_address = config.MAIL_SENDER,
cc_addrs : [email_address] = None,
extra_headers : [string] = None,
smtp_sender : email_address = (derived from from_addr)
smtp_recipients : [email_address] = (derived from to_addrs),
config : quixote.config.Config = (current publisher's config)):
Send an email message to a list of recipients via a local SMTP
server. In normal use, you supply a list of primary recipient
e-mail addresses in 'to_addrs', an optional list of secondary
recipient addresses in 'cc_addrs', and a sender address in
'from_addr'. sendmail() then constructs a message using those
addresses, 'subject', and 'msg_body', and mails the message to every
recipient address. (Specifically, it connects to the mail server
named in the MAIL_SERVER config variable -- default "localhost" --
and instructs the server to send the message to every recipient
address in 'to_addrs' and 'cc_addrs'.)
'from_addr' is optional because web applications often have a common
e-mail sender address, such as "webmaster@example.com". Just set
the Quixote config variable MAIL_FROM, and it will be used as the
default sender (both header and envelope) for all e-mail sent by
sendmail().
E-mail addresses can be specified a number of ways. The most
efficient is to supply instances of RFC822Mailbox, which bundles a
bare e-mail address (aka "addr_spec" from the RFC 822 grammar) and
real name together in a readily-formattable object. You can also
supply an (addr_spec, real_name) tuple, or an addr_spec on its own.
The latter two are converted into RFC822Mailbox objects for
formatting, which is why it may be more efficient to construct
RFC822Mailbox objects yourself.
Thus, the following are all equivalent in terms of who gets the
message:
sendmail(to_addrs=["joe@example.com"], ...)
sendmail(to_addrs=[("joe@example.com", "Joe User")], ...)
sendmail(to_addrs=[RFC822Mailbox("joe@example.com", "Joe User")], ...)
...although the "To" header will be slightly different. In the
first case, it will be
To: joe@example.com
while in the other two, it will be:
To: Joe User <joe@example.com>
which is a little more user-friendly.
In more advanced usage, you might wish to specify the SMTP sender
and recipient addresses separately. For example, if you want your
application to send mail to users that looks like it comes from a
real human being, but you don't want that human being to get the
bounce messages from the mailing, you might do this:
sendmail(to_addrs=user_list,
...,
from_addr=("realuser@example.com", "A Real User"),
smtp_sender="postmaster@example.com")
End users will see mail from "A Real User <realuser@example.com>" in
their inbox, but bounces will go to postmaster@example.com.
One use of different header and envelope recipients is for
testing/debugging. If you want to test that your application is
sending the right mail to bigboss@example.com without filling
bigboss' inbox with dross, you might do this:
sendmail(to_addrs=["bigboss@example.com"],
...,
smtp_recipients=["developers@example.com"])
This is so useful that it's a Quixote configuration option: just set
MAIL_DEBUG_ADDR to (eg.) "developers@example.com", and every message
that sendmail() would send out is diverted to the debug address.
Generally raises an exception on any SMTP errors; see smtplib (in
the standard library documentation) for details.
"""
if config is None:
from quixote import get_publisher
config = get_publisher().config
if not isinstance(to_addrs, ListType):
raise TypeError("'to_addrs' must be a list")
if not (cc_addrs is None or isinstance(cc_addrs, ListType)):
raise TypeError("'cc_addrs' must be a list or None")
# Make sure we have a "From" address
if from_addr is None:
from_addr = config.mail_from
if from_addr is None:
raise RuntimeError(
"no from_addr supplied, and MAIL_FROM not set in config file")
# Ensure all of our addresses are really RFC822Mailbox objects.
from_addr = _ensure_mailbox(from_addr)
to_addrs = map(_ensure_mailbox, to_addrs)
if cc_addrs:
cc_addrs = map(_ensure_mailbox, cc_addrs)
# Start building the message headers.
headers = ["From: %s" % from_addr.format(),
"Subject: %s" % subject]
_add_recip_headers(headers, "To", to_addrs)
if quixote.DEFAULT_CHARSET != 'iso-8859-1':
headers.append('Content-Type: text/plain; charset=%s' %
quixote.DEFAULT_CHARSET)
if cc_addrs:
_add_recip_headers(headers, "Cc", cc_addrs)
if extra_headers:
headers.extend(extra_headers)
if config.mail_debug_addr:
debug1 = ("[debug mode, message actually sent to %s]\n"
% config.mail_debug_addr)
if smtp_recipients:
debug2 = ("[original SMTP recipients: %s]\n"
% ", ".join(smtp_recipients))
else:
debug2 = ""
sep = ("-"*72) + "\n"
msg_body = debug1 + debug2 + sep + msg_body
smtp_recipients = [config.mail_debug_addr]
if smtp_sender is None:
smtp_sender = from_addr.addr_spec
else:
smtp_sender = _ensure_mailbox(smtp_sender).addr_spec
if smtp_recipients is None:
smtp_recipients = [addr.addr_spec for addr in to_addrs]
if cc_addrs:
smtp_recipients.extend([addr.addr_spec for addr in cc_addrs])
else:
smtp_recipients = [_ensure_mailbox(recip).addr_spec
for recip in smtp_recipients]
message = "\n".join(headers) + "\n\n" + msg_body
if quixote.DEFAULT_CHARSET != 'iso-8859-1':
message = message.encode(quixote.DEFAULT_CHARSET)
smtp = SMTP(config.mail_server)
smtp.sendmail(smtp_sender, smtp_recipients, message)
smtp.quit()

5
server/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/__init__.py $
$Id: __init__.py 25579 2004-11-11 20:56:32Z nascheme $
This package is for Quixote to server glue.
"""

463
server/_fcgi.py Normal file
View File

@ -0,0 +1,463 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/_fcgi.py $
$Id: _fcgi.py 26782 2005-05-11 20:32:01Z nascheme $
Derived from Robin Dunn's FastCGI module,
available at http://alldunn.com/python/#fcgi.
"""
#------------------------------------------------------------------------
# Copyright (c) 1998 by Total Control Software
# All Rights Reserved
#------------------------------------------------------------------------
#
# Module Name: fcgi.py
#
# Description: Handles communication with the FastCGI module of the
# web server without using the FastCGI developers kit, but
# will also work in a non-FastCGI environment, (straight CGI.)
# This module was originally fetched from someplace on the
# Net (I don't remember where and I can't find it now...) and
# has been significantly modified to fix several bugs, be more
# readable, more robust at handling large CGI data and return
# document sizes, and also to fit the model that we had previously
# used for FastCGI.
#
# WARNING: If you don't know what you are doing, don't tinker with this
# module!
#
# Creation Date: 1/30/98 2:59:04PM
#
# License: This is free software. You may use this software for any
# purpose including modification/redistribution, so long as
# this header remains intact and that you do not claim any
# rights of ownership or authorship of this software. This
# software has been tested, but no warranty is expressed or
# implied.
#
#------------------------------------------------------------------------
import os, sys, string, socket, errno, struct
from cStringIO import StringIO
import cgi
#---------------------------------------------------------------------------
# Set various FastCGI constants
# Maximum number of requests that can be handled
FCGI_MAX_REQS=1
FCGI_MAX_CONNS = 1
# Supported version of the FastCGI protocol
FCGI_VERSION_1 = 1
# Boolean: can this application multiplex connections?
FCGI_MPXS_CONNS=0
# Record types
FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3
FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6
FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9
FCGI_GET_VALUES_RESULT = 10
FCGI_UNKNOWN_TYPE = 11
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
# Types of management records
ManagementTypes = [FCGI_GET_VALUES]
FCGI_NULL_REQUEST_ID = 0
# Masks for flags component of FCGI_BEGIN_REQUEST
FCGI_KEEP_CONN = 1
# Values for role component of FCGI_BEGIN_REQUEST
FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3
# Values for protocolStatus component of FCGI_END_REQUEST
FCGI_REQUEST_COMPLETE = 0 # Request completed nicely
FCGI_CANT_MPX_CONN = 1 # This app can't multiplex
FCGI_OVERLOADED = 2 # New request rejected; too busy
FCGI_UNKNOWN_ROLE = 3 # Role value not known
error = 'fcgi.error'
#---------------------------------------------------------------------------
# The following function is used during debugging; it isn't called
# anywhere at the moment
def error(msg):
"Append a string to /tmp/err"
errf = open('/tmp/err', 'a+')
errf.write(msg+'\n')
errf.close()
#---------------------------------------------------------------------------
class record:
"Class representing FastCGI records"
def __init__(self):
self.version = FCGI_VERSION_1
self.recType = FCGI_UNKNOWN_TYPE
self.reqId = FCGI_NULL_REQUEST_ID
self.content = ""
#----------------------------------------
def readRecord(self, sock, unpack=struct.unpack):
(self.version, self.recType, self.reqId, contentLength,
paddingLength) = unpack(">BBHHBx", sock.recv(8))
content = ""
while len(content) < contentLength:
content = content + sock.recv(contentLength - len(content))
self.content = content
if paddingLength != 0:
padding = sock.recv(paddingLength)
# Parse the content information
if self.recType == FCGI_BEGIN_REQUEST:
(self.role, self.flags) = unpack(">HB", content[:3])
elif self.recType == FCGI_UNKNOWN_TYPE:
self.unknownType = ord(content[0])
elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
self.values = {}
pos = 0
while pos < len(content):
name, value, pos = readPair(content, pos)
self.values[name] = value
elif self.recType == FCGI_END_REQUEST:
(self.appStatus, self.protocolStatus) = unpack(">IB", content[0:5])
#----------------------------------------
def writeRecord(self, sock, pack=struct.pack):
content = self.content
if self.recType == FCGI_BEGIN_REQUEST:
content = pack(">HBxxxxx", self.role, self.flags)
elif self.recType == FCGI_UNKNOWN_TYPE:
content = pack(">Bxxxxxx", self.unknownType)
elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
content = ""
for i in self.values.keys():
content = content + writePair(i, self.values[i])
elif self.recType == FCGI_END_REQUEST:
content = pack(">IBxxx", self.appStatus, self.protocolStatus)
cLen = len(content)
eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary
padLen = eLen - cLen
hdr = pack(">BBHHBx", self.version, self.recType, self.reqId, cLen,
padLen)
##debug.write('Sending fcgi record: %s\n' % repr(content[:50]) )
sock.send(hdr + content + padLen*'\000')
#---------------------------------------------------------------------------
_lowbits = ~(1L << 31) # everything but the 31st bit
def readPair(s, pos):
nameLen = ord(s[pos]) ; pos = pos+1
if nameLen & 128:
pos = pos + 3
nameLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
valueLen = ord(s[pos]) ; pos = pos+1
if valueLen & 128:
pos = pos + 3
valueLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen],
pos+nameLen+valueLen )
#---------------------------------------------------------------------------
_highbit = (1L << 31)
def writePair(name, value):
l = len(name)
if l < 128:
s = chr(l)
else:
s = struct.pack(">I", l | _highbit)
l = len(value)
if l < 128:
s = s + chr(l)
else:
s = s + struct.pack(">I", l | _highbit)
return s + name + value
#---------------------------------------------------------------------------
def HandleManTypes(r, conn):
if r.recType == FCGI_GET_VALUES:
r.recType = FCGI_GET_VALUES_RESULT
v = {}
vars = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS,
'FCGI_MAX_REQS' : FCGI_MAX_REQS,
'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS}
for i in r.values.keys():
if vars.has_key(i): v[i] = vars[i]
r.values = vars
r.writeRecord(conn)
#---------------------------------------------------------------------------
#---------------------------------------------------------------------------
_isFCGI = 1 # assume it is until we find out for sure
def isFCGI():
return _isFCGI
#---------------------------------------------------------------------------
_init = None
_sock = None
class FCGI:
def __init__(self):
self.haveFinished = 0
if _init == None:
_startup()
if not _isFCGI:
self.haveFinished = 1
self.inp = sys.__stdin__
self.out = sys.__stdout__
self.err = sys.__stderr__
self.env = os.environ
return
if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'):
good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',')
good_addrs = map(string.strip, good_addrs) # Remove whitespace
else:
good_addrs = None
self.conn, addr = _sock.accept()
stdin, data = "", ""
self.env = {}
self.requestId = 0
remaining = 1
# Check if the connection is from a legal address
if good_addrs != None and addr not in good_addrs:
raise error, 'Connection from invalid server!'
while remaining:
r = record()
r.readRecord(self.conn)
if r.recType in ManagementTypes:
HandleManTypes(r, self.conn)
elif r.reqId == 0:
# Oh, poopy. It's a management record of an unknown
# type. Signal the error.
r2 = record()
r2.recType = FCGI_UNKNOWN_TYPE
r2.unknownType = r.recType
r2.writeRecord(self.conn)
continue # Charge onwards
# Ignore requests that aren't active
elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST:
continue
# If we're already doing a request, ignore further BEGIN_REQUESTs
elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0:
continue
# Begin a new request
if r.recType == FCGI_BEGIN_REQUEST:
self.requestId = r.reqId
if r.role == FCGI_AUTHORIZER: remaining = 1
elif r.role == FCGI_RESPONDER: remaining = 2
elif r.role == FCGI_FILTER: remaining = 3
elif r.recType == FCGI_PARAMS:
if r.content == "":
remaining = remaining-1
else:
for i in r.values.keys():
self.env[i] = r.values[i]
elif r.recType == FCGI_STDIN:
if r.content == "":
remaining = remaining-1
else:
stdin = stdin+r.content
elif r.recType == FCGI_DATA:
if r.content == "":
remaining = remaining-1
else:
data = data+r.content
# end of while remaining:
self.inp = StringIO(stdin)
self.err = StringIO()
self.out = StringIO()
self.data = StringIO(data)
def __del__(self):
self.Finish()
def Finish(self, status=0):
if not self.haveFinished:
self.haveFinished = 1
self.err.seek(0,0)
self.out.seek(0,0)
##global debug
##debug = open("/tmp/quixote-debug.log", "a+")
##debug.write("fcgi.FCGI.Finish():\n")
r = record()
r.recType = FCGI_STDERR
r.reqId = self.requestId
data = self.err.read()
##debug.write(" sending stderr (%s)\n" % `self.err`)
##debug.write(" data = %s\n" % `data`)
while data:
chunk, data = self.getNextChunk(data)
##debug.write(" chunk, data = %s, %s\n" % (`chunk`, `data`))
r.content = chunk
r.writeRecord(self.conn)
r.content = ""
r.writeRecord(self.conn) # Terminate stream
r.recType = FCGI_STDOUT
data = self.out.read()
##debug.write(" sending stdout (%s)\n" % `self.out`)
##debug.write(" data = %s\n" % `data`)
while data:
chunk, data = self.getNextChunk(data)
r.content = chunk
r.writeRecord(self.conn)
r.content = ""
r.writeRecord(self.conn) # Terminate stream
r = record()
r.recType = FCGI_END_REQUEST
r.reqId = self.requestId
r.appStatus = status
r.protocolStatus = FCGI_REQUEST_COMPLETE
r.writeRecord(self.conn)
self.conn.close()
#debug.close()
def getFieldStorage(self):
method = 'GET'
if self.env.has_key('REQUEST_METHOD'):
method = string.upper(self.env['REQUEST_METHOD'])
if method == 'GET':
return cgi.FieldStorage(environ=self.env, keep_blank_values=1)
else:
return cgi.FieldStorage(fp=self.inp,
environ=self.env,
keep_blank_values=1)
def getNextChunk(self, data):
chunk = data[:8192]
data = data[8192:]
return chunk, data
Accept = FCGI # alias for backwards compatibility
#---------------------------------------------------------------------------
def _startup():
global _isFCGI, _init, _sock
# This function won't work on Windows at all.
if sys.platform[:3] == 'win':
_isFCGI = 0
return
_init = 1
try:
s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET,
socket.SOCK_STREAM)
s.getpeername()
except socket.error, (err, errmsg):
if err != errno.ENOTCONN: # must be a non-fastCGI environment
_isFCGI = 0
return
_sock = s
#---------------------------------------------------------------------------
def _test():
counter = 0
try:
while isFCGI():
req = Accept()
counter = counter+1
try:
fs = req.getFieldStorage()
size = string.atoi(fs['size'].value)
doc = ['*' * size]
except:
doc = ['<HTML><HEAD>'
'<TITLE>FCGI TestApp</TITLE>'
'</HEAD>\n<BODY>\n']
doc.append('<H2>FCGI TestApp</H2><P>')
doc.append('<b>request count</b> = %d<br>' % counter)
doc.append('<b>pid</b> = %s<br>' % os.getpid())
if req.env.has_key('CONTENT_LENGTH'):
cl = string.atoi(req.env['CONTENT_LENGTH'])
doc.append('<br><b>POST data (%s):</b><br><pre>' % cl)
keys = fs.keys()
keys.sort()
for k in keys:
val = fs[k]
if type(val) == type([]):
doc.append(' <b>%-15s :</b> %s\n'
% (k, val))
else:
doc.append(' <b>%-15s :</b> %s\n'
% (k, val.value))
doc.append('</pre>')
doc.append('<P><HR><P><pre>')
keys = req.env.keys()
keys.sort()
for k in keys:
doc.append('<b>%-20s :</b> %s\n' % (k, req.env[k]))
doc.append('\n</pre><P><HR>\n')
doc.append('</BODY></HTML>\n')
doc = string.join(doc, '')
req.out.write('Content-length: %s\r\n'
'Content-type: text/html\r\n'
'Cache-Control: no-cache\r\n'
'\r\n'
% len(doc))
req.out.write(doc)
req.Finish()
except:
import traceback
f = open('traceback', 'w')
traceback.print_exc( file = f )
# f.write('%s' % doc)
if __name__ == '__main__':
#import pdb
#pdb.run('_test()')
_test()

25
server/cgi_server.py Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/cgi_server.py $
$Id: cgi_server.py 27684 2005-11-10 15:25:17Z dbinger $
"""
import sys
import os
def run(create_publisher):
if sys.platform == "win32":
# on Windows, stdin and stdout are in text mode by default
import msvcrt
msvcrt.setmode(sys.__stdin__.fileno(), os.O_BINARY)
msvcrt.setmode(sys.__stdout__.fileno(), os.O_BINARY)
publisher = create_publisher()
response = publisher.process(sys.__stdin__, os.environ)
try:
response.write(sys.__stdout__)
except IOError, err:
publisher.log("IOError while sending response ignored: %s" % err)
if __name__ == '__main__':
from quixote.demo import create_publisher
run(create_publisher)

26
server/fastcgi_server.py Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/fastcgi_server.py $
$Id: fastcgi_server.py 27684 2005-11-10 15:25:17Z dbinger $
Server for Quixote applications that use FastCGI. It should work
for CGI too but the cgi_server module is preferred as it is more
portable.
"""
from quixote.server import _fcgi
def run(create_publisher):
publisher = create_publisher()
while _fcgi.isFCGI():
f = _fcgi.FCGI()
response = publisher.process(f.inp, f.env)
try:
response.write(f.out)
except IOError, err:
publisher.log("IOError while sending response ignored: %s" % err)
f.Finish()
if __name__ == '__main__':
from quixote.demo import create_publisher
run(create_publisher)

114
server/medusa_server.py Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/medusa_server.py $
$Id: medusa_server.py 27684 2005-11-10 15:25:17Z dbinger $
An HTTP handler for Medusa that publishes a Quixote application.
"""
import asyncore, rfc822, socket, urllib
from StringIO import StringIO
from medusa import http_server, xmlrpc_handler
import quixote
class StreamProducer:
def __init__(self, chunks):
self.chunks = chunks # a generator
def more(self):
try:
return self.chunks.next()
except StopIteration:
return ''
class QuixoteHandler:
def __init__(self, publisher, server):
self.publisher = publisher
self.server = server
def match(self, request):
# Always match, since this is the only handler there is.
return True
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': 'Quixote/%s' % quixote.__version__,
}
for title, header in msg.items():
envname = 'HTTP_' + title.replace('-', '_').upper()
environ[envname] = header
stdin = StringIO(data)
qresponse = self.publisher.process(stdin, environ)
# 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)
request.push(StreamProducer(qresponse.generate_body_chunks()))
request.done()
def run(create_publisher, host='', port=80):
"""Runs a Medusa HTTP server that publishes a Quixote
application.
"""
server = http_server.http_server(host, port)
publisher = create_publisher()
handler = QuixoteHandler(publisher, server)
server.install_handler(handler)
asyncore.loop()
if __name__ == '__main__':
from quixote.server.util import main
main(run)

View File

@ -0,0 +1,103 @@
"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/mod_python_handler.py $
$Id: mod_python_handler.py 27684 2005-11-10 15:25:17Z dbinger $
This needs testing.
mod_python configuration
------------------------
mod_python is an Apache module for embedding a Python interpreter into
the Apache server. To use mod_python as the interface layer between
Apache and Quixote, add something like this to your httpd.conf::
LoadModule python_module /usr/lib/apache/1.3/mod_python.so
<LocationMatch "^/qdemo(/|$)">
SetHandler python-program
PythonHandler quixote.server.mod_python_handler
PythonOption quixote-publisher-factory quixote.demo.create_publisher
PythonInterpreter quixote.demo
PythonDebug On
</LocationMatch>
This will attach URLs starting with ``/qdemo`` to the Quixote demo.
When you use mod_python, there's no need for rewrite rules (because of
the pattern in the ``LocationMatch`` directive), and no need for a
driver script.
mod_python support was contributed to Quixote (1) by Erno Kuusela
<erno@iki.fi> and the Quixote 2 port comes from Clint.
"""
import sys
from mod_python import apache
from quixote.publish import Publisher
from quixote.util import import_object
class ErrorLog:
def __init__(self, publisher):
self.publisher = publisher
def write(self, msg):
self.publisher.log(msg)
def close(self):
pass
class ModPythonPublisher(Publisher):
def __init__(self, package, **kwargs):
Publisher.__init__(self, package, **kwargs)
# may be overwritten
self.logger.error_log = self.__error_log = ErrorLog(self)
self.__apache_request = None
def log(self, msg):
if self.logger.error_log is self.__error_log:
try:
self.__apache_request.log_error(msg)
except AttributeError:
apache.log_error(msg)
else:
Publisher.log(self, msg)
def publish_modpython(self, req):
"""publish_modpython() -> None
Entry point from mod_python.
"""
self.__apache_request = req
try:
self.publish(apache.CGIStdin(req),
apache.CGIStdout(req),
sys.stderr,
apache.build_cgi_env(req))
return apache.OK
finally:
self.__apache_request = None
name2publisher = {}
def run(publisher, req):
response = publisher.process(apache.CGIStdin(req),
apache.build_cgi_env(req))
try:
response.write(apache.CGIStdout(req))
except IOError, err:
publisher.log("IOError while sending response ignored: %s" % err)
return apache.OK
def handler(req):
opts = req.get_options()
try:
factory = opts['quixote-publisher-factory']
except KeyError:
apache.log_error('quixote-publisher-factory setting required')
return apache.HTTP_INTERNAL_SERVER_ERROR
pub = name2publisher.get(factory)
if pub is None:
factory_fcn = import_object(factory)
pub = factory_fcn()
name2publisher[factory] = pub
return run(pub, req)

82
server/scgi_server.py Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/scgi_server.py $
$Id: scgi_server.py 27684 2005-11-10 15:25:17Z dbinger $
A SCGI server that uses Quixote to publish dynamic content.
"""
from scgi import scgi_server
class QuixoteHandler(scgi_server.SCGIHandler):
def __init__(self, parent_fd, create_publisher, script_name=None):
scgi_server.SCGIHandler.__init__(self, parent_fd)
self.publisher = create_publisher()
self.script_name = script_name
def handle_connection(self, conn):
input = conn.makefile("r")
output = conn.makefile("w")
env = self.read_env(input)
if self.script_name is not None:
# mod_scgi doesn't know SCRIPT_NAME :-(
prefix = self.script_name
path = env['SCRIPT_NAME']
assert path[:len(prefix)] == prefix, (
"path %r doesn't start with script_name %r" % (path, prefix))
env['SCRIPT_NAME'] = prefix
env['PATH_INFO'] = path[len(prefix):] + env.get('PATH_INFO', '')
response = self.publisher.process(input, env)
try:
response.write(output)
input.close()
output.close()
conn.close()
except IOError, err:
self.publisher.log("IOError while sending response "
"ignored: %s" % err)
def run(create_publisher, host='', port=3000, script_name=None, max_children=5):
def create_handler(parent_fd):
return QuixoteHandler(parent_fd, create_publisher, script_name)
s = scgi_server.SCGIServer(create_handler, host=host, port=port,
max_children=max_children)
s.serve()
def main():
from optparse import OptionParser
from quixote.util import import_object
parser = OptionParser()
parser.set_description(run.__doc__)
default_host = 'localhost'
parser.add_option(
'--host', dest="host", default=default_host, type="string",
help="Host interface to listen on. (default=%s)" % default_host)
default_port = 3000
parser.add_option(
'--port', dest="port", default=default_port, type="int",
help="Port to listen on. (default=%s)" % default_port)
default_maxchild = 5
parser.add_option(
'--max-children', dest="maxchild", default=default_maxchild,
type="string",
help="Maximum number of children to spawn. (default=%s)" %
default_maxchild)
parser.add_option(
'--script-name', dest="script_name", default=None, type="string",
help="Value of SCRIPT_NAME (only needed if using mod_scgi)")
default_factory = 'quixote.demo.create_publisher'
parser.add_option(
'--factory', dest="factory",
default=default_factory,
help="Path to factory function to create the site Publisher. "
"(default=%s)" % default_factory)
(options, args) = parser.parse_args()
run(import_object(options.factory), host=options.host, port=options.port,
script_name=options.script_name, max_children=options.maxchild)
if __name__ == '__main__':
main()

117
server/simple_server.py Executable file
View File

@ -0,0 +1,117 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/simple_server.py $
$Id: simple_server.py 27684 2005-11-10 15:25:17Z dbinger $
A simple, single threaded, synchronous HTTP server.
"""
import sys
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import urllib
import quixote
from quixote import get_publisher
from quixote.util import import_object
class HTTPRequestHandler(BaseHTTPRequestHandler):
required_cgi_environment = {}
def get_cgi_env(self, method):
env = dict(
SERVER_SOFTWARE="Quixote/%s" % quixote.__version__,
SERVER_NAME=self.server.server_name,
GATEWAY_INTERFACE='CGI/1.1',
SERVER_PROTOCOL=self.protocol_version,
SERVER_PORT=str(self.server.server_port),
REQUEST_METHOD=method,
REMOTE_ADDR=self.client_address[0],
SCRIPT_NAME='')
if '?' in self.path:
env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?', 1)
else:
env['PATH_INFO'] = self.path
env['PATH_INFO'] = urllib.unquote(env['PATH_INFO'])
if self.headers.typeheader is None:
env['CONTENT_TYPE'] = self.headers.type
else:
env['CONTENT_TYPE'] = self.headers.typeheader
env['CONTENT_LENGTH'] = self.headers.getheader('content-length') or "0"
for name, value in self.headers.items():
header_name = 'HTTP_' + name.upper().replace('-', '_')
env[header_name] = value
accept = []
for line in self.headers.getallmatchingheaders('accept'):
if line[:1] in "\t\n\r ":
accept.append(line.strip())
else:
accept = accept + line[7:].split(',')
env['HTTP_ACCEPT'] = ','.join(accept)
co = filter(None, self.headers.getheaders('cookie'))
if co:
env['HTTP_COOKIE'] = ', '.join(co)
env.update(self.required_cgi_environment)
return env
def process(self, env, include_body=True):
response = get_publisher().process(self.rfile, env)
try:
self.send_response(response.get_status_code(),
response.get_reason_phrase())
response.write(self.wfile, include_status=False,
include_body=include_body)
except IOError, err:
print "IOError while sending response ignored: %s" % err
def do_POST(self):
return self.process(self.get_cgi_env('POST'))
def do_GET(self):
return self.process(self.get_cgi_env('GET'))
def do_HEAD(self):
return self.process(self.get_cgi_env('HEAD'), include_body=False)
def send_response(self, code, message=None):
"""
Copied, with regret, from BaseHTTPRequestHandler, except that the line
that adds the 'Date' header is removed to avoid duplicating the one
that Quixote adds.
"""
self.log_request(code)
if message is None:
if code in self.responses:
message = self.responses[code][0]
else:
message = ''
if self.request_version != 'HTTP/0.9':
self.wfile.write("%s %d %s\r\n" %
(self.protocol_version, code, message))
self.send_header('Server', self.version_string())
def run(create_publisher, host='', port=80, https=False):
"""Runs a simple, single threaded, synchronous HTTP server that
publishes a Quixote application.
"""
if https:
HTTPRequestHandler.required_cgi_environment['HTTPS'] = 'on'
httpd = HTTPServer((host, port), HTTPRequestHandler)
def handle_error(request, client_address):
HTTPServer.handle_error(httpd, request, client_address)
if sys.exc_info()[0] is SystemExit:
raise
httpd.handle_error = handle_error
publisher = create_publisher()
httpd.serve_forever()
if __name__ == '__main__':
from quixote.server.util import get_server_parser
parser = get_server_parser(run.__doc__)
parser.add_option(
'--https', dest="https", default=False, action="store_true",
help=("Force the scheme for all requests to be https. "
"Not that this is for running the simple server "
"through a proxy or tunnel that provides real SSL "
"support. The simple server itself does not. "))
(options, args) = parser.parse_args()
run(import_object(options.factory), host=options.host, port=options.port,
https=options.https)

146
server/twisted_server.py Executable file
View File

@ -0,0 +1,146 @@
#!/usr/bin/env python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/twisted_server.py $
$Id: twisted_server.py 27684 2005-11-10 15:25:17Z dbinger $
An HTTP server for Twisted that publishes a Quixote application.
"""
import urllib
from twisted.protocols import http
from twisted.web import server
from twisted.python import threadable
from twisted.internet import reactor
class QuixoteFactory(http.HTTPFactory):
def __init__(self, publisher):
self.publisher = publisher
http.HTTPFactory.__init__(self, None)
def buildProtocol(self, addr):
protocol = http.HTTPFactory.buildProtocol(self, addr)
protocol.requestFactory = QuixoteRequest
return protocol
class QuixoteRequest(server.Request):
def process(self):
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)
qxresponse = self.channel.factory.publisher.process(self.content,
environ)
self.setResponseCode(qxresponse.status_code)
for name, value in qxresponse.generate_headers():
if name != 'Set-Cookie':
self.setHeader(name, value)
# Cookies get special treatment since it seems Twisted cannot handle
# multiple Set-Cookie headers.
for name, attrs in qxresponse.cookies.items():
attrs = attrs.copy()
value = attrs.pop('value')
self.addCookie(name, value, **attrs)
QuixoteProducer(qxresponse, self)
def create_environment(self):
"""
Borrowed heavily from twisted.web.twcgi
"""
# Twisted doesn't decode the path for us, so let's do it here.
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',
'SERVER_PROTOCOL': 'HTTP/1.1',
}
for env_var, header in [('ACCEPT_ENCODING', 'Accept-encoding'),
('CONTENT_TYPE', 'Content-type'),
('HTTP_COOKIE', 'Cookie'),
('HTTP_REFERER', 'Referer'),
('HTTP_USER_AGENT', 'User-agent')]:
value = self.getHeader(header)
if value is not None:
env[env_var] = value
client = self.getClient()
if client is not None:
env['REMOTE_HOST'] = client
ip = self.getClientIP()
if ip is not None:
env['REMOTE_ADDR'] = ip
_, _, 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 QuixoteProducer:
"""
Produce the Quixote response for twisted.
"""
def __init__(self, qxresponse, request):
self.request = request
self.size = qxresponse.get_content_length()
self.stream = qxresponse.generate_body_chunks()
request.registerProducer(self, 0)
def resumeProducing(self):
if self.request:
try:
chunk = self.stream.next()
except StopIteration:
self.request.unregisterProducer()
self.request.finish()
self.request = None
else:
self.request.write(chunk)
def pauseProducing(self):
pass
def stopProducing(self):
self.request = None
synchronized = ['resumeProducing', 'stopProducing']
threadable.synchronize(QuixoteProducer)
def run(create_publisher, host='', port=80):
"""Runs a Twisted HTTP server server that publishes a Quixote
application."""
publisher = create_publisher()
factory = QuixoteFactory(publisher)
reactor.listenTCP(port, factory, interface=host)
reactor.run()
if __name__ == '__main__':
from quixote.server.util import main
main(run)

32
server/util.py Normal file
View File

@ -0,0 +1,32 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/util.py $
$Id: util.py 26427 2005-03-30 18:03:32Z dbinger $
Miscellaneous utility functions shared by servers.
"""
from optparse import OptionParser
from quixote.util import import_object
def get_server_parser(doc):
parser = OptionParser()
parser.set_description(doc)
default_host = 'localhost'
parser.add_option(
'--host', dest="host", default=default_host, type="string",
help="Host interface to listen on. (default=%s)" % default_host)
default_port = 8080
parser.add_option(
'--port', dest="port", default=default_port, type="int",
help="Port to listen on. (default=%s)" % default_port)
default_factory = 'quixote.demo.create_publisher'
parser.add_option(
'--factory', dest="factory",
default=default_factory,
help="Path to factory function to create the site Publisher. "
"(default=%s)" % default_factory)
return parser
def main(run):
parser = get_server_parser(run.__doc__)
(options, args) = parser.parse_args()
run(import_object(options.factory), host=options.host, port=options.port)

567
session.py Normal file
View File

@ -0,0 +1,567 @@
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/session.py $
$Id: session.py 26524 2005-04-08 10:22:34Z dbinger $
Quixote session management. There are two levels to Quixote's
session management system:
- SessionManager
- Session
A SessionManager is responsible for creating sessions, setting and reading
session cookies, maintaining the collection of all sessions, and so forth.
There is one SessionManager instance per Quixote process.
A Session is the umbrella object for a single session (notionally, a (user,
host, browser_process) triple). Simple applications can probably get away
with putting all session data into a Session object (or, better, into an
application-specific subclass of Session).
The default implementation provided here is not persistent: when the
Quixote process shuts down, all session data is lost. See
doc/session-mgmt.txt for information on session persistence.
"""
from time import time, localtime, strftime
from quixote import get_publisher, get_cookie, get_response, get_request, \
get_session
from quixote.util import randbytes
class NullSessionManager:
"""A session manager that does nothing. It is the default session manager.
"""
def start_request(self):
"""
Called near the beginning of each request: after the HTTPRequest
object has been built, but before we traverse the URL or call the
callable object found by URL traversal.
"""
def finish_successful_request(self):
"""Called near the end of each successful request. Not called if
there were any errors processing the request.
"""
def finish_failed_request(self):
"""Called near the end of a failed request (i.e. a exception that was
not a PublisherError was raised.
"""
class SessionManager:
"""
SessionManager acts as a dictionary of all sessions, mapping session
ID strings to individual session objects. Session objects are
instances of Session (or a custom subclass for your application).
SessionManager is also responsible for creating and destroying
sessions, for generating and interpreting session cookies, and for
session persistence (if any -- this implementation is not
persistent).
Most applications can just use this class directly; sessions will
be kept in memory-based dictionaries, and will be lost when the
Quixote process dies. Alternatively an application can subclass
SessionManager to implement specific behaviour, such as persistence.
Instance attributes:
session_class : class
the class that is instantiated to create new session objects
(in new_session())
sessions : mapping { session_id:string : Session }
the collection of sessions managed by this SessionManager
"""
ACCESS_TIME_RESOLUTION = 1 # in seconds
def __init__(self, session_class=None, session_mapping=None):
"""(session_class : class = Session, session_mapping : mapping = None)
Create a new session manager. There should be one session
manager per publisher, ie. one per process
session_class is used by the new_session() method -- it returns
an instance of session_class.
"""
self.sessions = {}
if session_class is None:
self.session_class = Session
else:
self.session_class = session_class
if session_mapping is None:
self.sessions = {}
else:
self.sessions = session_mapping
def __repr__(self):
return "<%s at %x>" % (self.__class__.__name__, id(self))
# -- Mapping interface ---------------------------------------------
# (subclasses shouldn't need to override any of this, unless
# your application passes in a session_mapping object that
# doesn't provide all of the mapping methods needed here)
def keys(self):
"""() -> [string]
Return the list of session IDs of sessions in this session manager.
"""
return self.sessions.keys()
def sorted_keys(self):
"""() -> [string]
Return the same list as keys(), but sorted.
"""
keys = self.keys()
keys.sort()
return keys
def values(self):
"""() -> [Session]
Return the list of sessions in this session manager.
"""
return self.sessions.values()
def items(self):
"""() -> [(string, Session)]
Return the list of (session_id, session) pairs in this session
manager.
"""
return self.sessions.items()
def get(self, session_id, default=None):
"""(session_id : string, default : any = None) -> Session
Return the session object identified by 'session_id', or None if
no such session.
"""
return self.sessions.get(session_id, default)
def __getitem__(self, session_id):
"""(session_id : string) -> Session
Return the session object identified by 'session_id'. Raise KeyError
if no such session.
"""
return self.sessions[session_id]
def has_key(self, session_id):
"""(session_id : string) -> boolean
Return true if a session identified by 'session_id' exists in
the session manager.
"""
return self.sessions.has_key(session_id)
# has_session() is a synonym for has_key() -- if you override
# has_key(), be sure to repeat this alias!
has_session = has_key
def __setitem__(self, session_id, session):
"""(session_id : string, session : Session)
Store 'session' in the session manager under 'session_id'.
"""
if not isinstance(session, self.session_class):
raise TypeError("session not an instance of %r: %r"
% (self.session_class, session))
assert session.id is not None, "session ID not set"
assert session_id == session.id, "session ID mismatch"
self.sessions[session_id] = session
def __delitem__(self, session_id):
"""(session_id : string) -> Session
Remove the session object identified by 'session_id' from the session
manager. Raise KeyError if no such session.
"""
del self.sessions[session_id]
# -- Transactional interface ---------------------------------------
# Useful for applications that provide a transaction-oriented
# persistence mechanism. You'll still need to provide a mapping
# object that works with your persistence mechanism; these two
# methods let you hook into your transaction machinery after a
# request is finished processing.
def abort_changes(self, session):
"""(session : Session)
Placeholder for subclasses that implement transactional
persistence: forget about saving changes to the current
session. Called by the publisher when a request fails,
ie. when it catches an exception other than PublishError.
"""
pass
def commit_changes(self, session):
"""(session : Session)
Placeholder for subclasses that implement transactional
persistence: commit changes to the current session. Called by
the publisher when a request completes successfully, or is
interrupted by a PublishError exception.
"""
pass
# -- Session management --------------------------------------------
# these build on the storage mechanism implemented by the
# above mapping methods, and are concerned with all the high-
# level details of managing web sessions
def new_session(self, id):
"""(id : string) -> Session
Return a new session object, ie. an instance of the session_class
class passed to the constructor (defaults to Session).
"""
return self.session_class(id)
def _get_session_id(self, config):
"""() -> string
Find the ID of the current session by looking for the session
cookie in the request. Return None if no such cookie or the
cookie has been expired, otherwise return the cookie's value.
"""
id = get_cookie(config.session_cookie_name)
if id == "" or id == "*del*":
return None
else:
return id
def _make_session_id(self):
# Generate a session ID, which is just the value of the session
# cookie we are about to drop on the user. (It's also the key
# used with the session manager mapping interface.)
id = None
while id is None or self.has_session(id):
id = randbytes(8) # 64-bit random number
return id
def _create_session(self):
# Create a new session object, with no ID for now - one will
# be assigned later if we save the session.
return self.new_session(None)
def get_session(self):
"""() -> Session
Fetch or create a session object for the current session, and
return it. If a session cookie is found in the HTTP request
object, use it to look up and return an existing session object.
If no session cookie is found, create a new session.
Note that this method does *not* cause the new session to be
stored in the session manager, nor does it drop a session cookie
on the user. Those are both the responsibility of
maintain_session(), called at the end of a request.
"""
config = get_publisher().config
id = self._get_session_id(config)
session = self.get(id) or self._create_session()
session._set_access_time(self.ACCESS_TIME_RESOLUTION)
return session
def maintain_session(self, session):
"""(session : Session)
Maintain session information. This method is called after servicing
an HTTP request, just before the response is returned. If a session
contains information it is saved and a cookie dropped on the client.
If not, the session is discarded and the client will be instructed
to delete the session cookie (if any).
"""
if not session.has_info():
# Session has no useful info -- forget it. If it previously
# had useful information and no longer does, we have to
# explicitly forget it.
if session.id and self.has_session(session.id):
del self[session.id]
self.revoke_session_cookie()
return
if session.id is None:
# This is the first time this session has had useful
# info -- store it and set the session cookie.
session.id = self._make_session_id()
self[session.id] = session
self.set_session_cookie(session.id)
elif session.is_dirty():
# We have already stored this session, but it's dirty
# and needs to be stored again. This will never happen
# with the default Session class, but it's there for
# applications using a persistence mechanism that requires
# repeatedly storing the same object in the same mapping.
self[session.id] = session
def _set_cookie(self, value, **attrs):
config = get_publisher().config
name = config.session_cookie_name
if config.session_cookie_path:
path = config.session_cookie_path
else:
path = get_request().get_environ('SCRIPT_NAME')
if not path.endswith("/"):
path += "/"
domain = config.session_cookie_domain
get_response().set_cookie(name, value, domain=domain,
path=path, **attrs)
return name
def set_session_cookie(self, session_id):
"""(session_id : string)
Ensure that a session cookie with value 'session_id' will be
returned to the client via the response object.
"""
self._set_cookie(session_id)
def revoke_session_cookie(self):
"""
Remove the session cookie from the remote user's session by
resetting the value and maximum age in the response object. Also
remove the cookie from the request so that further processing of
this request does not see the cookie's revoked value.
"""
cookie_name = self._set_cookie("", max_age=0)
if get_cookie(cookie_name) is not None:
del get_request().cookies[cookie_name]
def expire_session(self):
"""
Expire the current session, ie. revoke the session cookie from
the client and remove the session object from the session
manager and from the current request.
"""
self.revoke_session_cookie()
request = get_request()
try:
del self[request.session.id]
except KeyError:
# This can happen if the current session hasn't been saved
# yet, eg. if someone tries to leave a session with no
# interesting data. That's not a big deal, so ignore it.
pass
request.session = None
def has_session_cookie(self, must_exist=False):
"""(must_exist : boolean = false) -> bool
Return true if the request already has a cookie identifying a
session object. If 'must_exist' is true, the cookie must
correspond to a currently existing session; otherwise (the
default), we just check for the existence of the session cookie
and don't inspect its content at all.
"""
config = get_publisher().config
id = get_cookie(config.session_cookie_name)
if id is None:
return False
if must_exist:
return self.has_session(id)
else:
return True
# -- Hooks into the Quixote main loop ------------------------------
def start_request(self):
"""
Called near the beginning of each request: after the HTTPRequest
object has been built, but before we traverse the URL or call the
callable object found by URL traversal.
"""
session = self.get_session()
get_request().session = session
session.start_request()
def finish_successful_request(self):
"""Called near the end of each successful request. Not called if
there were any errors processing the request.
"""
session = get_session()
if session is not None:
self.maintain_session(session)
self.commit_changes(session)
def finish_failed_request(self):
"""Called near the end of a failed request (i.e. a exception that was
not a PublisherError was raised.
"""
self.abort_changes(get_session())
class Session:
"""
Holds information about the current session. The only information
that is likely to be useful to applications is the 'user' attribute,
which applications can use as they please.
Instance attributes:
id : string
the session ID (generated by SessionManager and used as the
value of the session cookie)
user : any
an object to identify the human being on the other end of the
line. It's up to you whether to store just a string in 'user',
or some more complex data structure or object.
_remote_address : string
IP address of user owning this session (only set when the
session is created)
_creation_time : float
_access_time : float
two ways of keeping track of the "age" of the session.
Note that '__access_time' is maintained by the SessionManager that
owns this session, using _set_access_time().
_form_tokens : [string]
outstanding form tokens. This is used as a queue that can grow
up to MAX_FORM_TOKENS. Tokens are removed when forms are submitted.
Feel free to access 'id' and 'user' directly, but do not modify
'id'. The preferred way to set 'user' is with the set_user() method
(which you might want to override for type-checking).
"""
MAX_FORM_TOKENS = 16 # maximum number of outstanding form tokens
def __init__(self, id):
self.id = id
self.user = None
self._remote_address = get_request().get_environ("REMOTE_ADDR")
self._creation_time = self._access_time = time()
self._form_tokens = [] # queue
def __repr__(self):
return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.id)
def __str__(self):
if self.user:
return "session %s (user %s)" % (self.id, self.user)
else:
return "session %s (no user)" % self.id
def has_info(self):
"""() -> boolean
Return true if this session contains any information that must
be saved.
"""
return self.user or self._form_tokens
def is_dirty(self):
"""() -> boolean
Return true if this session has changed since it was last saved
such that it needs to be saved again.
Default implementation always returns false since the default
storage mechanism is an in-memory dictionary, and you don't have
to put the same object into the same slot of a dictionary twice.
If sessions are stored to, eg., files in a directory or slots in
a hash file, is_dirty() should probably be an alias or wrapper
for has_info(). See doc/session-mgmt.txt.
"""
return False
def dump(self, file=None, header=True, deep=True):
time_fmt = "%Y-%m-%d %H:%M:%S"
ctime = strftime(time_fmt, localtime(self._creation_time))
atime = strftime(time_fmt, localtime(self._access_time))
if header:
file.write('session %s:' % self.id)
file.write(' user %s' % self.user)
file.write(' _remote_address: %s' % self._remote_address)
file.write(' created %s, last accessed %s' % (ctime, atime))
file.write(' _form_tokens: %s\n' % self._form_tokens)
def start_request(self):
"""
Called near the beginning of each request: after the HTTPRequest
object has been built, but before we traverse the URL or call the
callable object found by URL traversal.
"""
if self.user is not None:
get_request().environ['REMOTE_USER'] = str(self.user)
# -- Simple accessors and modifiers --------------------------------
def set_user(self, user):
self.user = user
def get_user(self):
return self.user
def get_remote_address(self):
"""Return the IP address (dotted-quad string) that made the
initial request in this session.
"""
return self._remote_address
def get_creation_time(self):
"""Return the time that this session was created (seconds
since epoch).
"""
return self._creation_time
def get_access_time(self):
"""Return the time that this session was last accessed (seconds
since epoch).
"""
return self._access_time
def get_creation_age(self, _now=None):
"""Return the number of seconds since session was created."""
# _now arg is not strictly necessary, but there for consistency
# with get_access_age()
return (_now or time()) - self._creation_time
def get_access_age(self, _now=None):
"""Return the number of seconds since session was last accessed."""
# _now arg is for SessionManager's use
return (_now or time()) - self._access_time
# -- Methods for SessionManager only -------------------------------
def _set_access_time(self, resolution):
now = time()
if now - self._access_time > resolution:
self._access_time = now
# -- Form token methods --------------------------------------------
def create_form_token(self):
"""() -> string
Create a new form token and add it to a queue of outstanding form
tokens for this session. A maximum of MAX_FORM_TOKENS are saved.
The new token is returned.
"""
token = randbytes(8)
self._form_tokens.append(token)
extra = len(self._form_tokens) - self.MAX_FORM_TOKENS
if extra > 0:
del self._form_tokens[:extra]
return token
def has_form_token(self, token):
"""(token : string) -> boolean
Return true if 'token' is in the queue of outstanding tokens.
"""
return token in self._form_tokens
def remove_form_token(self, token):
"""(token : string)
Remove 'token' from the queue of outstanding tokens.
"""
self._form_tokens.remove(token)

63
setup.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
#$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/setup.py $
#$Id: setup.py 27720 2005-12-12 21:13:41Z dbinger $
# Setup script for Quixote
import sys, os
from distutils import core
from distutils.extension import Extension
from ptl.qx_distutils import qx_build_py
# a fast htmltext type
htmltext = Extension(name="quixote.html._c_htmltext",
sources=["html/_c_htmltext.c"])
# faster import hook for PTL modules
cimport = Extension(name="quixote.ptl.cimport",
sources=["ptl/cimport.c"])
kw = {'name': "Quixote",
'version': "2.4",
'description': "A highly Pythonic Web application framework",
'author': "MEMS Exchange",
'author_email': "quixote@mems-exchange.org",
'url': "http://www.mems-exchange.org/software/quixote/",
'license': "CNRI Open Source License (see LICENSE.txt)",
'package_dir': {'quixote':os.curdir},
'packages': ['quixote', 'quixote.demo', 'quixote.form',
'quixote.html', 'quixote.ptl',
'quixote.server'],
'ext_modules': [],
'cmdclass': {'build_py': qx_build_py},
}
build_extensions = sys.platform != 'win32'
if build_extensions:
# The _c_htmltext module requires Python 2.2 features.
if sys.hexversion >= 0x20200a1:
kw['ext_modules'].append(htmltext)
kw['ext_modules'].append(cimport)
# If we're running Python 2.3, add extra information
if hasattr(core, 'setup_keywords'):
if 'classifiers' in core.setup_keywords:
kw['classifiers'] = ['Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'License :: OSI Approved :: Python License (CNRI Python License)',
'Intended Audience :: Developers',
'Operating System :: Unix',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS :: MacOS X',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
]
if 'download_url' in core.setup_keywords:
kw['download_url'] = ('http://www.mems-exchange.org/software/files'
'/quixote/Quixote-%s.tar.gz' % kw['version'])
core.setup(**kw)

2
test/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Empty file to make this directory a package

29
test/ua_test.py Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# Test Quixote's ability to parse the "User-Agent" header, ie.
# the 'guess_browser_version()' method of HTTPRequest.
#
# Reads User-Agent strings on stdin, and writes Quixote's interpretation
# of each on stdout. This is *not* an automated test!
import sys, os
from copy import copy
from quixote.http_request import HTTPRequest
if __name__ == '__main__':
env = copy(os.environ)
file = sys.stdin
while 1:
line = file.readline()
if not line:
break
if line[-1] == "\n":
line = line[:-1]
env["HTTP_USER_AGENT"] = line
req = HTTPRequest(None, env)
(name, version) = req.guess_browser_version()
if name is None:
print "%s -> ???" % line
else:
print "%s -> (%s, %s)" % (line, name, version)

43
test/utest_request.py Executable file
View File

@ -0,0 +1,43 @@
from sancho.utest import UTest
from quixote.http_request import parse_cookies
class ParseCookiesTest (UTest):
def check_basic(self):
assert parse_cookies('a') == {'a': ''}
assert parse_cookies('a = ') == {'a': ''}
assert parse_cookies('a = ""') == {'a': ''}
assert parse_cookies(r'a = "\""') == {'a': '"'}
assert parse_cookies('a, b; c') == {'a': '', 'b': '', 'c': ''}
assert parse_cookies('a, b=1') == {'a': '', 'b': '1'}
assert parse_cookies('a = ";, \t";') == {'a': ';, \t'}
def check_rfc2109_example(self):
s = ('$Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"; '
'Part_Number="Rocket_Launcher_0001"; $Path="/acme"')
result = {'Customer': 'WILE_E_COYOTE',
'Part_Number': 'Rocket_Launcher_0001',
}
assert parse_cookies(s) == result
def check_other(self):
s = 'PREF=ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU'
result = {'PREF': 'ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU'}
assert parse_cookies(s) == result
s = 'pageColor=White; pageWidth=990; fontSize=12; fontFace=1; E=E'
assert parse_cookies(s) == {'pageColor': 'White',
'pageWidth': '990',
'fontSize': '12',
'fontFace': '1',
'E': 'E'}
s = 'userid="joe"; QX_session="58a3ced39dcd0d"'
assert parse_cookies(s) == {'userid': 'joe',
'QX_session': '58a3ced39dcd0d'}
def check_invalid(self):
parse_cookies('a="123')
parse_cookies('a=123"')
if __name__ == "__main__":
ParseCookiesTest()

390
util.py Normal file
View File

@ -0,0 +1,390 @@
"""quixote.util
$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/util.py $
$Id: util.py 26523 2005-04-08 10:20:19Z dbinger $
Contains various useful functions and classes:
xmlrpc(request, func) : Processes the body of an XML-RPC request, and calls
'func' with the method name and parameters.
StaticFile : Wraps a file from a filesystem as a
Quixote resource.
StaticDirectory : Wraps a directory containing static files as
a Quixote directory.
StaticFile and StaticDirectory were contributed by Hamish Lawson.
See doc/static-files.txt for examples of their use.
"""
import sys
import os
import time
import binascii
import mimetypes
import urllib
import xmlrpclib
from rfc822 import formatdate
import quixote
from quixote import errors
from quixote.directory import Directory
from quixote.html import htmltext, TemplateIO
from quixote.http_response import Stream
if hasattr(os, 'urandom'):
# available in Python 2.4 and also works on win32
def randbytes(bytes):
"""Return bits of random data as a hex string."""
return binascii.hexlify(os.urandom(bytes))
elif os.path.exists('/dev/urandom'):
# /dev/urandom is just as good as /dev/random for cookies (assuming
# SHA-1 is secure) and it never blocks.
def randbytes(bytes):
"""Return bits of random data as a hex string."""
return binascii.hexlify(open("/dev/urandom").read(bytes))
else:
# this is much less secure than the above function
import sha
class _PRNG:
def __init__(self):
self.state = sha.new(str(time.time() + time.clock()))
self.count = 0
def _get_bytes(self):
self.state.update('%s %d' % (time.time() + time.clock(),
self.count))
self.count += 1
return self.state.hexdigest()
def randbytes(self, bytes):
"""Return bits of random data as a hex string."""
s = ""
chars = 2*bytes
while len(s) < chars:
s += self._get_bytes()
return s[:chars]
randbytes = _PRNG().randbytes
def import_object(name):
i = name.rfind('.')
if i != -1:
module_name = name[:i]
object_name = name[i+1:]
__import__(module_name)
return getattr(sys.modules[module_name], object_name)
else:
__import__(name)
return sys.modules[name]
def xmlrpc(request, func):
"""xmlrpc(request:Request, func:callable) : string
Processes the body of an XML-RPC request, and calls 'func' with
two arguments, a string containing the method name and a tuple of
parameters.
"""
# Get contents of POST body
if request.get_method() != 'POST':
request.response.set_status(405, "Only the POST method is accepted")
return "XML-RPC handlers only accept the POST method."
length = int(request.environ['CONTENT_LENGTH'])
data = request.stdin.read(length)
# Parse arguments
params, method = xmlrpclib.loads(data)
try:
result = func(method, params)
except xmlrpclib.Fault, exc:
result = exc
except:
# report exception back to client
result = xmlrpclib.dumps(
xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value))
)
else:
result = (result,)
result = xmlrpclib.dumps(result, methodresponse=1)
request.response.set_content_type('text/xml')
return result
class FileStream(Stream):
CHUNK_SIZE = 20000
def __init__(self, fp, size=None):
self.fp = fp
self.length = size
def __iter__(self):
return self
def next(self):
chunk = self.fp.read(self.CHUNK_SIZE)
if not chunk:
raise StopIteration
return chunk
class StaticFile:
"""
Wrapper for a static file on the filesystem.
"""
def __init__(self, path, follow_symlinks=False,
mime_type=None, encoding=None, cache_time=None):
"""StaticFile(path:string, follow_symlinks:bool)
Initialize instance with the absolute path to the file. If
'follow_symlinks' is true, symbolic links will be followed.
'mime_type' specifies the MIME type, and 'encoding' the
encoding; if omitted, the MIME type will be guessed,
defaulting to text/plain.
Optional cache_time parameter indicates the number of
seconds a response is considered to be valid, and will
be used to set the Expires header in the response when
quixote gets to that part. If the value is None then
the Expires header will not be set.
"""
# Check that the supplied path is absolute and (if a symbolic link) may
# be followed
self.path = path
if not os.path.isabs(path):
raise ValueError, "Path %r is not absolute" % path
# Decide the Content-Type of the file
guess_mime, guess_enc = mimetypes.guess_type(os.path.basename(path),
strict=False)
self.mime_type = mime_type or guess_mime or 'text/plain'
self.encoding = encoding or guess_enc or None
self.cache_time = cache_time
self.follow_symlinks = follow_symlinks
def __call__(self):
if not self.follow_symlinks and os.path.islink(self.path):
raise errors.TraversalError(private_msg="Path %r is a symlink"
% self.path)
request = quixote.get_request()
response = quixote.get_response()
if self.cache_time is None:
response.set_expires(None) # don't set the Expires header
else:
# explicitly allow client to cache page by setting the Expires
# header, this is even more efficient than the using
# Last-Modified/If-Modified-Since since the browser does not need
# to contact the server
response.set_expires(seconds=self.cache_time)
stat = os.stat(self.path)
last_modified = formatdate(stat.st_mtime)
if last_modified == request.get_header('If-Modified-Since'):
# handle exact match of If-Modified-Since header
response.set_status(304)
return ''
# Set the Content-Type for the response and return the file's contents.
response.set_content_type(self.mime_type)
if self.encoding:
response.set_header("Content-Encoding", self.encoding)
response.set_header('Last-Modified', last_modified)
return FileStream(open(self.path, 'rb'), stat.st_size)
class StaticDirectory(Directory):
"""
Wrap a filesystem directory containing static files as a Quixote directory.
"""
_q_exports = ['']
FILE_CLASS = StaticFile
def __init__(self, path, use_cache=False, list_directory=False,
follow_symlinks=False, cache_time=None, file_class=None,
index_filenames=None):
"""(path:string, use_cache:bool, list_directory:bool,
follow_symlinks:bool, cache_time:int,
file_class=None, index_filenames:[string])
Initialize instance with the absolute path to the file.
If 'use_cache' is true, StaticFile instances will be cached in memory.
If 'list_directory' is true, users can request a directory listing.
If 'follow_symlinks' is true, symbolic links will be followed.
Optional parameter cache_time allows setting of Expires header in
response object (see note for StaticFile for more detail).
Optional parameter 'index_filenames' specifies a list of
filenames to be used as index files in the directory. First
file found searching left to right is returned.
"""
# Check that the supplied path is absolute
self.path = path
if not os.path.isabs(path):
raise ValueError, "Path %r is not absolute" % path
self.use_cache = use_cache
self.cache = {}
self.list_directory = list_directory
self.follow_symlinks = follow_symlinks
self.cache_time = cache_time
if file_class is not None:
self.file_class = file_class
else:
self.file_class = self.FILE_CLASS
self.index_filenames = index_filenames
def _render_header(self, title):
r = TemplateIO(html=True)
r += htmltext('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 '
'Transitional//EN" '
'"http://www.w3.org/TR/REC-html40/loose.dtd">')
r += htmltext('<html>')
r += htmltext('<head><title>%s</title></head>') % title
r += htmltext('<body>')
r += htmltext("<h1>%s</h1>") % title
return r.getvalue()
def _render_footer(self):
return htmltext('</body></html>')
def _q_index(self):
"""
If directory listings are allowed, generate a simple HTML
listing of the directory's contents with each item hyperlinked;
if the item is a subdirectory, place a '/' after it. If not allowed,
return a page to that effect.
"""
if self.index_filenames:
for name in self.index_filenames:
try:
obj = self._q_lookup(name)
except errors.TraversalError:
continue
if not isinstance(obj, StaticDirectory) and callable(obj):
return obj()
r = TemplateIO(html=True)
if self.list_directory:
r += self._render_header('Index of %s' % quixote.get_path())
template = htmltext('<a href="%s">%s</a>%s\n')
r += htmltext('<pre>')
r += template % ('..', '..', '')
files = os.listdir(self.path)
files.sort()
for filename in files:
filepath = os.path.join(self.path, filename)
marker = os.path.isdir(filepath) and "/" or ""
r += template % (urllib.quote(filename), filename, marker)
r += htmltext('</pre>')
r += self._render_footer()
else:
r += self._render_header('Directory listing denied')
r += htmltext('<p>This directory does not allow its contents '
'to be listed.</p>')
r += self._render_footer()
return r.getvalue()
def _q_lookup(self, name):
"""
Get a file from the filesystem directory and return the StaticFile
or StaticDirectory wrapper of it; use caching if that is in use.
"""
if name in ('.', '..'):
raise errors.TraversalError(private_msg="Attempt to use '.', '..'")
if self.cache.has_key(name):
# Get item from cache
item = self.cache[name]
else:
# Get item from filesystem; cache it if caching is in use.
item_filepath = os.path.join(self.path, name)
while os.path.islink(item_filepath):
if not self.follow_symlinks:
raise errors.TraversalError
else:
dest = os.readlink(item_filepath)
item_filepath = os.path.join(self.path, dest)
if os.path.isdir(item_filepath):
item = self.__class__(item_filepath, self.use_cache,
self.list_directory,
self.follow_symlinks, self.cache_time,
self.file_class, self.index_filenames)
elif os.path.isfile(item_filepath):
item = self.file_class(item_filepath, self.follow_symlinks,
cache_time=self.cache_time)
else:
raise errors.TraversalError
if self.use_cache:
self.cache[name] = item
return item
class Redirector:
"""
A simple class that can be used from inside _q_lookup() to redirect
requests.
"""
_q_exports = []
def __init__(self, location, permanent=False):
self.location = location
self.permanent = permanent
def _q_lookup(self, component):
return self
def __call__(self):
return quixote.redirect(self.location, self.permanent)
def dump_request(request=None):
if request is None:
request = quixote.get_request()
"""Dump an HTTPRequest object as HTML."""
row_fmt = htmltext('<tr><th>%s</th><td>%s</td></tr>')
r = TemplateIO(html=True)
r += htmltext('<h3>form</h3>'
'<table>')
for k, v in request.form.items():
r += row_fmt % (k, v)
r += htmltext('</table>'
'<h3>cookies</h3>'
'<table>')
for k, v in request.cookies.items():
r += row_fmt % (k, v)
r += htmltext('</table>'
'<h3>environ</h3>'
'<table>')
for k, v in request.environ.items():
r += row_fmt % (k, v)
r += htmltext('</table>')
return r.getvalue()
def get_directory_path():
"""() -> [object]
Return the list of traversed instances.
"""
path = []
frame = sys._getframe()
while frame:
if frame.f_code.co_name == '_q_traverse':
self = frame.f_locals.get('self', None)
if path[:1] != [self]:
path.insert(0, self)
frame = frame.f_back
return path