Import Quixote 2.4.
This commit is contained in:
commit
d6b73c5768
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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
|
|
@ -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/
|
|
@ -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).
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
|
@ -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.
|
|
@ -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)
|
|
@ -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()
|
|
@ -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))
|
|
@ -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)
|
|
@ -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')
|
|
@ -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
|
|
@ -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 "import os, quixote; print os.path.dirname(quixote.__file__)"</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>
|
|
@ -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.
|
||||
|
|
@ -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) .
|
|
@ -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):
|
||||
"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
|
||||
"."
|
||||
</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):
|
||||
"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
|
||||
</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
|
||||
" " # 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">"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">"</span></tt>. You can
|
||||
also have conditional logic or exception blocks:</p>
|
||||
<pre class="literal-block">
|
||||
def international_hello [plain] (language):
|
||||
if language == "english":
|
||||
"hello"
|
||||
elif language == "french":
|
||||
"bonjour"
|
||||
else:
|
||||
raise ValueError, "I don't speak %s" % 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 '<' 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).</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">
|
||||
>>> 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'>
|
||||
</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 "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."</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] ():
|
||||
'<html><body>' # does not get escaped
|
||||
htmltext(open("myfile.html").read())
|
||||
'</body></html>'
|
||||
</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):
|
||||
'<title>%s</title>' % title
|
||||
'<meta name="description" content="%s">\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"><title>%s</title></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">
|
||||
>>> 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'>
|
||||
>>>
|
||||
</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>
|
|
@ -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 & 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 <HEAD></title>
|
||||
<meta name="description"
|
||||
content="Discusses the "LINK" and "META" 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.
|
||||
|
|
@ -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 }
|
|
@ -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 "Welcome ..." page. In your terminal window, you will see a
|
||||
"localhost - - ..." 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 "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'.</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 "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
|
||||
<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
|
||||
"Internal Server Error" 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 "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.</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 "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.</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
|
||||
"[html]" 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 ("favicon.ico",
|
||||
"favicon_ico") to designate "favicon_ico" as the attribute name
|
||||
corresponding the the "favicon.ico" 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
|
||||
"resolve" 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>
|
|
@ -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 $
|
|
@ -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'] < 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'] < 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>
|
|
@ -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)
|
|
@ -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>
|
|
@ -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 $
|
|
@ -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
|
||||
"publishing loop", 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 "GET /catalog/ HTTP/1.1"
|
||||
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 "-" 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>
|
|
@ -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.
|
||||
|
||||
|
|
@ -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 "session" 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="928F82A9B8FA92FD"
|
||||
</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="928F82A9B8FA92FD"
|
||||
</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 "Session persistence" 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('<p>%s</p>' % href('login', 'login'))
|
||||
else:
|
||||
content += htmltext(
|
||||
'<p>Hello, %s.</p>') % get_user()
|
||||
content += htmltext('<p>%s</p>' % 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">&</span></tt> or
|
||||
<tt class="literal"><span class="pre"><</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("name"):
|
||||
session = get_session()
|
||||
session.set_user(get_field("name")) # 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 "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.</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 = "foo_session"
|
||||
</pre>
|
||||
<p>and the second application might have</p>
|
||||
<pre class="literal-block">
|
||||
SESSION_COOKIE_NAME = "bar_session"
|
||||
</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="928F82A9B8FA92FD"
|
||||
</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 = "/"
|
||||
</pre>
|
||||
<p>which will cause Quixote to set the cookie like this:</p>
|
||||
<pre class="literal-block">
|
||||
Set-Cookie: QX_session="928F82A9B8FA92FD"; Path="/"
|
||||
</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 "/", 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.)</p>
|
||||
<pre class="literal-block">
|
||||
SESSION_COOKIE_PATH = "/q/"
|
||||
</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 = ".example.com"</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("/tmp/quixote-sessions")
|
||||
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 "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, <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>
|
|
@ -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.
|
||||
|
||||
|
|
@ -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(
|
||||
"/htdocs/legacy_app/stylesheet.css",
|
||||
follow_symlinks=1, mime_type="text/css")
|
||||
</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("/htdocs/legacy_app/notes")
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -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")
|
||||
|
||||
|
|
@ -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
|
||||
"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.</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 "" to
|
||||
_q_exports or make sure that _q_lookup handles "" 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 '&' will be converted to '&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"><option</span> <span class="pre">value="..."></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 "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.</p>
|
||||
<p>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.</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 "__" 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>
|
|
@ -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 '&'.
|
||||
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``.
|
||||
|
||||
|
||||
|
||||
|
|
@ -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 "virtual directory" 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"><VirtualHost></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 "real"
|
||||
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">
|
||||
<Location />
|
||||
SCGIServer 127.0.0.1 3001
|
||||
SCGIHandler On
|
||||
</Location>
|
||||
</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>
|
|
@ -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).
|
||||
|
||||
|
|
@ -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><int></td>
|
||||
<td>int</td>
|
||||
</tr>
|
||||
<tr><td><double></td>
|
||||
<td>float</td>
|
||||
</tr>
|
||||
<tr><td><string></td>
|
||||
<td>string</td>
|
||||
</tr>
|
||||
<tr><td><array></td>
|
||||
<td>list</td>
|
||||
</tr>
|
||||
<tr><td><struct></td>
|
||||
<td>dict</td>
|
||||
</tr>
|
||||
<tr><td><boolean></td>
|
||||
<td>xmlrpclib.Boolean</td>
|
||||
</tr>
|
||||
<tr><td><base64></td>
|
||||
<td>xmlrpclib.Binary</td>
|
||||
</tr>
|
||||
<tr><td><dateTime></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">
|
||||
>>> import xmlrpclib
|
||||
>>> 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">
|
||||
>>> 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}]
|
||||
>>>
|
||||
</pre>
|
||||
<p>This call results in the following XML being sent:</p>
|
||||
<pre class="literal-block">
|
||||
<?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>
|
||||
</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://<hostname>/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, "Unknown XML-RPC method: %r" % 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, "Unknown XML-RPC method: %r" % 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>
|
|
@ -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 $
|
|
@ -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 "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 <em>HTML: The Definitive
|
||||
Guide</em> by Chuck Musciano & 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 "name"
|
||||
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"><input</span> <span class="pre">type="text"></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"><input></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">
|
||||
>>> StringWidget("foo", value="hello").render(request)
|
||||
'<input name="foo" type="text" value="hello">'
|
||||
|
||||
>>> StringWidget("foo", size=10, maxlength=20).render(request)
|
||||
'<input name="foo" type="text" size="10" maxlength="20">'
|
||||
</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"><textarea></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 = "physical")
|
||||
</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"><input</span> <span class="pre">type="checkbox"></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">
|
||||
>>> CheckboxWidget("foo", value=0).render(request)
|
||||
'<input name="foo" type="checkbox" value="yes">'
|
||||
|
||||
>>> CheckboxWidget("foo", value="you bet").render(request)
|
||||
'<input name="foo" type="checkbox" value="yes" checked>'
|
||||
</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"><input</span>
|
||||
<span class="pre">type="radio"></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 = "\n")
|
||||
</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"><input</span> <span class="pre">type="radio"></span></tt> tags to generate and the
|
||||
values for each. Eg. <tt class="literal"><span class="pre">allowed_values=["foo",</span> <span class="pre">"bar"]</span></tt> will result in
|
||||
(roughly):</p>
|
||||
<pre class="last literal-block">
|
||||
<input type="radio" value="foo">
|
||||
<input type="radio" value="bar">
|
||||
</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">""</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">&</span></tt> or <tt class="literal"><span class="pre"><</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"><input></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">
|
||||
>>> colours = ["red", "green", "blue", "pink"]
|
||||
>>> widget = RadiobuttonsWidget("foo", allowed_values=colours)
|
||||
>>> print widget.render(request)
|
||||
<input name="foo" type="radio" value="0">red</input>
|
||||
<input name="foo" type="radio" value="1">green</input>
|
||||
<input name="foo" type="radio" value="2">blue</input>
|
||||
<input name="foo" type="radio" value="3">pink</input>
|
||||
</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">"1"</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">
|
||||
>>> values = [val1, val2, val3]
|
||||
>>> descs = ["thing <b>1</b>",
|
||||
"thing <b>2</b>",
|
||||
"thing <b>3</b>"]
|
||||
>>> widget = RadiobuttonsWidget("bar",
|
||||
allowed_values=values,
|
||||
descriptions=descs,
|
||||
value=val3,
|
||||
delim="<br>\n",
|
||||
quote=0)
|
||||
>>> print widget.render(request)
|
||||
<input name="bar" type="radio" value="0">thing <b>1</b></input><br>
|
||||
<input name="bar" type="radio" value="1">thing <b>2</b></input><br>
|
||||
<input name="bar" type="radio" value="2" checked="checked">thing <b>3</b></input>
|
||||
</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"><select>...</select></span></tt> tag, with one
|
||||
<tt class="literal"><span class="pre"><option></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"><option></span></tt> tags that will go inside the
|
||||
<tt class="literal"><span class="pre"><select></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"><select></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">
|
||||
>>> widget = SingleSelectWidget("foo",
|
||||
allowed_values=["abc", 123, 5.5])
|
||||
>>> print widget.render(request)
|
||||
<select name="foo">
|
||||
<option value="0">abc
|
||||
<option value="1">123
|
||||
<option value="2">5.5
|
||||
</select>
|
||||
|
||||
>>> widget = SingleSelectWidget("bar",
|
||||
value=val2,
|
||||
allowed_values=[val1, val2, val3],
|
||||
descriptions=["foo", "bar", "foo & bar"],
|
||||
size=3)
|
||||
>>> print widget.render(request)
|
||||
<select name="bar" size="3">
|
||||
<option value="0">foo
|
||||
<option selected value="1">bar
|
||||
<option value="2">foo &amp; bar
|
||||
</select>
|
||||
</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"><select</span> <span class="pre">multiple>...</select></span></tt>
|
||||
tag, with one <tt class="literal"><span class="pre"><option></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">
|
||||
>>> SubmitButtonWidget(value="Submit Form").render(request)
|
||||
'<input type="submit" value="Submit Form">'
|
||||
</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 "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.)</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">
|
||||
>>> HiddenWidget("form_id", "2452345135").render(request)
|
||||
'<input type="hidden" name="form_id" value="2452345135">'
|
||||
</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">
|
||||
>>> IntWidget("num", value=37, size=5).render(request)
|
||||
'<input type="string" name="num" value="37" size="5">'
|
||||
</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 "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.)</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>
|
|
@ -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 & 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.
|
||||
|
||||
|
|
@ -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.
|
|
@ -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())
|
|
@ -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
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
||||
"""
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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(' *')
|
||||
r = TemplateIO(html=1)
|
||||
r += htmltext('<tr><th colspan="3" align="left">')
|
||||
r += title
|
||||
r += htmltext('</th></tr>'
|
||||
'<tr><td> </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)
|
|
@ -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)
|
|
@ -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©=0' # INCORRECT
|
||||
url = 'http://example.com/?a=1&copy=0' # CORRECT
|
||||
...
|
||||
'<a href="%s">do something</a>' % url
|
||||
|
||||
Old browsers would treat "©" 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))
|
File diff suppressed because it is too large
Load Diff
|
@ -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("&", "&")
|
||||
s = s.replace("<", "<")
|
||||
s = s.replace(">", ">")
|
||||
s = s.replace('"', """)
|
||||
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("&", "&") # must be done first
|
||||
s = s.replace("<", "<")
|
||||
s = s.replace(">", ">")
|
||||
s = s.replace('"', """)
|
||||
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))
|
|
@ -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 = '<>&"'
|
||||
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&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& 'foo&' bar&"
|
||||
assert htmltext('%(a)s %(a)r %(b)s') % args == result
|
||||
assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&"
|
||||
assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext)
|
||||
assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&'}"
|
||||
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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||
))
|
|
@ -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 & 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 <HEAD></title>
|
||||
<meta name="description"
|
||||
content="Discusses the "LINK" and "META" 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.)
|
||||
'''
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import quixote.ptl.ptl_import
|
||||
quixote.ptl.ptl_import.install()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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])
|
||||
|
|
@ -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)
|
|
@ -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("&") == "&&"',
|
||||
'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()
|
||||
|
|
@ -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
|
|
@ -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))
|
|
@ -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()
|
||||
|
||||
|
|
@ -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.
|
||||
"""
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
# Empty file to make this directory a package
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
Loading…
Reference in New Issue