512 lines
19 KiB
Python
512 lines
19 KiB
Python
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
|
import ulib.base.i_need_py24
|
|
|
|
__all__ = ('nocache', 'auth', 'defaults', 'Page', 'Application')
|
|
|
|
import os, sys, base64, logging
|
|
from os import path
|
|
|
|
from ulib.base.base import isstr, isbool, isnum, isflt, seqof, make_prop
|
|
from ulib.base.uio import _u, Utf8IO
|
|
from ulib.base.output import enote
|
|
from ulib.base.args import get_args
|
|
from ulib.base.dates import rfc2822
|
|
from ulib.base.functions import apply_args
|
|
from ulib.base.words import plural
|
|
from ulib.formats import unicodeF, booleanF, integerF, floatF
|
|
from ulib.ext import web
|
|
|
|
def checked(b):
|
|
return b and u'checked="checked"' or u''
|
|
|
|
def favicon(href):
|
|
return ur'<link rel="shortcut icon" href="%s" />' % href
|
|
def css(href, media=None):
|
|
if media is not None:
|
|
media = ur' media="%s"' % media
|
|
return ur'<link rel="stylesheet" href="%s" type="text/css"%s />' % (href, media or u'')
|
|
def js(href):
|
|
return ur'<script type="text/javascript" src="%s"></script>' % href
|
|
def jscss(href, min=False):
|
|
if href.endswith(".min.js"):
|
|
jshref = href
|
|
csshref = "%s.css" % href[:-len(".min.js")]
|
|
elif href.endswith(".js"):
|
|
jshref = href
|
|
csshref = "%s.css" % href[:-len(".js")]
|
|
elif href.endswith(".css"):
|
|
jshref = "%s.js" % href[:-len(".css")]
|
|
csshref = href
|
|
return u"%s\n%s" % (js(jshref), css(csshref))
|
|
|
|
BPSUPPL_CSS = "bpsuppl.css"
|
|
def blueprintcss(prefix=None, suppl=True):
|
|
if prefix is None:
|
|
prefix = ""
|
|
bpsuppl = BPSUPPL_CSS
|
|
else:
|
|
pprefix = path.split(prefix)[0]
|
|
bpsuppl = path.join(pprefix, BPSUPPL_CSS)
|
|
if not prefix.endswith("/"): prefix = prefix + "/"
|
|
|
|
return u"\n".join([css("%sscreen.css" % prefix, "screen, projection"),
|
|
css("%sprint.css" % prefix, "print"),
|
|
u"<!--[if lt IE 8]>",
|
|
css("%sie.css" % prefix, "screen, projection"),
|
|
u"<![endif]-->",
|
|
css(bpsuppl),
|
|
])
|
|
DEFAULT_TEMPLATE_GLOBALS = {'_u': _u,
|
|
'checked': checked,
|
|
'favicon': favicon,
|
|
'css': css,
|
|
'js': js,
|
|
'jscss': jscss,
|
|
'blueprintcss': blueprintcss,
|
|
'plural': plural,
|
|
}
|
|
|
|
HANDLER_CLASSES = []
|
|
class MetaPage(type):
|
|
u"""Méta-classe qui suit la définition des classes dérivées de Page, afin
|
|
de construire automatiquement la liste des classes handler.
|
|
"""
|
|
def __init__(cls, name, bases, attrs):
|
|
type.__init__(cls, name, bases, attrs)
|
|
abstract = cls.__dict__.get('__abstract_page__', False)
|
|
if not abstract:
|
|
_fix_PATH(cls, name)
|
|
HANDLER_CLASSES.append(cls)
|
|
|
|
def nocache(method):
|
|
u"""Décorateur pour s'assurer que le résultat de la méthode web n'est pas mis en cache
|
|
"""
|
|
def wrapper(self, *args, **kw):
|
|
web.header("Expires", rfc2822(0))
|
|
web.header("Pragma", "no-cache")
|
|
web.header("Cache-control", "max-age=0, no-cache, must-revalidate")
|
|
return method(self, *args, **kw)
|
|
return wrapper
|
|
|
|
def auth(authenticator=None, realm='pyutools'):
|
|
u"""Décorateur pour s'assurer que la méthode web est authentifiée.
|
|
|
|
La fonction authenticator avec la signature (username, password) permet
|
|
d'authentifier l'utilisateur connecté.
|
|
"""
|
|
def decorator(method):
|
|
def wrapper(self, *args, **kw):
|
|
env = web.ctx.environ
|
|
if env.has_key('HTTP_AUTHORIZATION'):
|
|
auth = env['HTTP_AUTHORIZATION'].split()
|
|
if auth[0:1] and auth[0].lower() == 'basic':
|
|
username, password = base64.b64decode(auth[1]).split(':')
|
|
if authenticator is None or apply_args(authenticator, username, password):
|
|
return method(self, *args, **kw)
|
|
web.header('WWW-Authenticate', 'Basic realm="%s"' % realm, True)
|
|
return web.HTTPError('401 Unauthorized')
|
|
return wrapper
|
|
return decorator
|
|
|
|
def defaults(*required, **defaults):
|
|
u"""Initialiser dans l'objet courant des variables à des valeurs par
|
|
défaut, ou en les prenant parmi les paramètres de la requête.
|
|
|
|
Si la valeur par défaut est respectivement une chaine, un booléen, un entier,
|
|
une valeur flottante, la valeur de la requête est automatiquement convertie
|
|
et la valeur effective sera toujours respectivement une chaine, un booléen,
|
|
un entier, une valeur flottante
|
|
"""
|
|
def decorator(method):
|
|
def wrapper(*args, **kw):
|
|
self = args[0:1] and args[0] or None
|
|
if self is not None:
|
|
self.defaults = web.input(*required, **defaults)
|
|
def fix(name, format):
|
|
value = self.defaults[name] or None
|
|
if format is not None:
|
|
self.defaults[name] = format.parse(value)
|
|
for name, value in defaults.items():
|
|
if isstr(value): fix(name, unicodeF)
|
|
elif isbool(value): fix(name, booleanF)
|
|
elif isnum(value): fix(name, integerF)
|
|
elif isflt(value): fix(name, floatF)
|
|
else: fix(name, None)
|
|
return method(*args, **kw)
|
|
return wrapper
|
|
return decorator
|
|
|
|
class Msg(object):
|
|
DEFAULT_MSG_CLASSES = (u"success", )
|
|
|
|
_msg_value, msg_value = make_prop('_msg_value')[:2]
|
|
_msg_classes, msg_classes = make_prop('_msg_classes', DEFAULT_MSG_CLASSES)[:2]
|
|
msg_class = property(lambda self: u" ".join(self._msg_classes))
|
|
_level, level = make_prop('_level')[:2]
|
|
|
|
def __fix_msg_class(self, msg_class):
|
|
if msg_class is None: msg_class = self.DEFAULT_MSG_CLASSES[0]
|
|
return _u(msg_class)
|
|
|
|
def __init__(self, msg_value=None, msg_classes=None, level=None):
|
|
if msg_value is not None: self._msg_value = _u(msg_value)
|
|
if msg_classes is not None:
|
|
self._msg_classes = tuple(map(self.__fix_msg_class, seqof(msg_classes)))
|
|
if level is not None: self._level = level
|
|
|
|
NO_MSG = Msg()
|
|
|
|
def _fix_PATH(cls, name):
|
|
if cls.PATH is None:
|
|
if cls.PREFIX is None:
|
|
cls.PREFIX = '/%s' % name
|
|
if cls.PREFIX == '/': cls.PATH = '/(.*)'
|
|
else: cls.PATH = '%s(?:/(.*))?' % cls.PREFIX
|
|
|
|
class Page(object):
|
|
__metaclass__ = MetaPage
|
|
__abstract_page__ = True
|
|
|
|
app = None # instance de Application
|
|
uio = None # instance de Utf8IO
|
|
session = property(lambda self: self.app.session)
|
|
_render = None # instance de web.template.render
|
|
|
|
TEMPLATEDIR = None # répertoire relatif des templates pour cette classe
|
|
TEMPLATENAME = None # nom du template à utiliser pour cette classe
|
|
# par défaut, TEMPLATENAME = le_nom_de_la_classe
|
|
|
|
PREFIX = None # url relatif d'accès aux méthodes de cette classe
|
|
# par défaut, PREFIX = '/%s' % le_nom_de_la_classe
|
|
PATH = None # expression régulière qui matche les urls traités par cette classe
|
|
# par défaut, PATH = '%s/(.*)' % PREFIX
|
|
|
|
defaults = None
|
|
def __getattr__(self, name):
|
|
defaults = self.defaults
|
|
if defaults is not None:
|
|
if defaults.has_key(name):
|
|
return defaults[name]
|
|
raise AttributeError(name)
|
|
|
|
def init(self):
|
|
u"""Initialiser l'objet
|
|
"""
|
|
|
|
msgs = property(lambda self: self.app.msgs)
|
|
def clear_msgs(self, level=None): return self.app.clear_msgs(level)
|
|
def add_msg(self, msg_value, msg_class=None, level=None):
|
|
return self.app.add_msg(msg_value, msg_class, level)
|
|
last_msg = property(lambda self: self.app.last_msg)
|
|
msg_value = property(lambda self: self.app.msg_value)
|
|
msg_class = property(lambda self: self.app.msg_class)
|
|
|
|
def get_last_error_msg(self, exc_info=None):
|
|
u"""Obtenir le message d'erreur de la dernière exception, ainsi que la
|
|
classe CSS associée.
|
|
|
|
msg vaut None s'il n'y a pas d'erreur. msgclass peut valoir success,
|
|
notice ou error.
|
|
|
|
@return: msg, msgclass
|
|
"""
|
|
if exc_info is None: exc_info = sys.exc_info()[:2]
|
|
type, value = exc_info[:2]
|
|
if type is None:
|
|
return None, u"success"
|
|
elif isinstance(value, Warning):
|
|
return u"%s: %s" % (_u(type.__name__), _u(value)), u"notice"
|
|
elif isinstance(value, Exception):
|
|
return u"%s: %s" % (_u(type.__name__), _u(value)), u"error"
|
|
last_error_msg = property(get_last_error_msg)
|
|
|
|
def add_last_error_msg(self, log=True):
|
|
msg_value, msg_class = self.get_last_error_msg()
|
|
if log: logging.exception(msg_value)
|
|
self.add_msg(msg_value, msg_class)
|
|
|
|
def redirect(self, path, **kw):
|
|
return web.redirect(web.url(path, **kw))
|
|
|
|
def r(self, name, *args, **kw):
|
|
templatedir = getattr(self, 'TEMPLATEDIR', None)
|
|
if templatedir is not None:
|
|
name = path.join(templatedir, name)
|
|
render = getattr(self._render, name)
|
|
return render(*args, **kw)
|
|
|
|
def render(self, *args, **kw):
|
|
name = getattr(self, 'TEMPLATENAME', None)
|
|
if name is None: name = self.__class__.__name__
|
|
return self.r(name, *args, **kw)
|
|
|
|
NAME = None # nom de la méthode à appeler, ou None s'il n'y a pas de méthode définie
|
|
METHOD = None # GET ou POST
|
|
def __response(self, name, method):
|
|
self.NAME = name
|
|
self.METHOD = method
|
|
if not name: name = self.__class__.__name__
|
|
m = getattr(self, name, None)
|
|
if m is None: m = self.error
|
|
# XXX Ajouter un argument en fonction de la valeur du header Accept:, pour text/plain,
|
|
# text/html, application/xml et application/xhtml+xml
|
|
return apply_args(m, method)
|
|
|
|
def GET(self, name=None): return self.__response(name, 'GET')
|
|
def POST(self, name=None): return self.__response(name, 'POST')
|
|
def error(self): raise web.notfound()
|
|
def index(self): return self.render()
|
|
|
|
class Application(object):
|
|
HOST = '0.0.0.0'
|
|
PORT = 12345
|
|
DEBUG = False
|
|
|
|
webapp = None # instance de web.application
|
|
uio = Utf8IO()
|
|
session = None # instance de web.session.Session
|
|
|
|
basedir = None # répertoire de base pour les fichiers servis
|
|
|
|
render = None # instance de web.template.render
|
|
templatedir = None # répertoire des templates
|
|
template_globals = None # dictionnaire global pour render
|
|
|
|
args = None # arguments qui restent sur la ligne de commande
|
|
|
|
# messages de l'application
|
|
_msgs, msgs = make_prop('_msgs')[:2]
|
|
|
|
def __init__(self, basedir=None, templatedir=None, host=None, port=None, debug=None):
|
|
if host is not None: self.HOST = host
|
|
if port is not None: self.PORT = port
|
|
if debug is not None: self.DEBUG = debug
|
|
|
|
if self.basedir is None or basedir is not None:
|
|
if basedir is None:
|
|
# par défaut, basedir est répertoire qui contient la classe
|
|
module_name = self.__class__.__module__
|
|
module = sys.modules.get(module_name, None)
|
|
if module is not None:
|
|
file = module.__file__
|
|
if file is not None:
|
|
basedir = path.abspath(path.split(file)[0])
|
|
if basedir is None:
|
|
basedir = os.getcwd()
|
|
self.basedir = basedir
|
|
if self.templatedir is None or templatedir is not None:
|
|
if templatedir is None:
|
|
templatedir = path.join(self.basedir, 'templates')
|
|
self.templatedir = templatedir
|
|
|
|
tg = self.template_globals
|
|
if tg is None: tg = {}
|
|
tg.update(DEFAULT_TEMPLATE_GLOBALS)
|
|
self.template_globals = tg
|
|
|
|
self.clear_msgs()
|
|
|
|
OPTIONS = None # options
|
|
LONG_OPTIONS = None
|
|
def process_option(self, option, value):
|
|
u"""Traiter une option et retourner True, ou False si l'option n'est
|
|
pas reconnue.
|
|
|
|
Cette méthode est prévue pour être dérivée.
|
|
"""
|
|
return False
|
|
|
|
# process_option() s'appelait auparavant is_option(). Cette méthode est
|
|
# gardée pour compatibilité
|
|
def is_option(self, option, value): return False
|
|
|
|
def process_args(self, args):
|
|
u"""Traiter les arguments après les options.
|
|
|
|
Cette méthode est appelée avant que soit initialisé le serveur web, et
|
|
est prévue pour être dérivée.
|
|
"""
|
|
|
|
def before_start(self):
|
|
u"""Effectuer un traitement avant de lancer le serveur web standalone.
|
|
|
|
Cette méthode est appelée juste avant que soit lancé le serveur web, et
|
|
est prévue pour être dérivée.
|
|
"""
|
|
if web.config.debug:
|
|
enote(u"Lancement du serveur sur http://%s:%i" % (self.HOST, self.PORT))
|
|
|
|
def new_render(self, templatedir=None):
|
|
u"""Retourner une nouvelle instance de web.template.render
|
|
"""
|
|
if templatedir is None: templatedir = self.templatedir
|
|
return web.template.render(templatedir,
|
|
cache=not self.DEBUG,
|
|
globals=self.template_globals)
|
|
|
|
def _new_application(self):
|
|
u"""Créer une nouvelle instance de web.application
|
|
"""
|
|
return web.application()
|
|
|
|
def _new_session(self):
|
|
u"""Si les sessions ne sont pas supportées, retourner None.
|
|
|
|
Sinon, retourner un tuple (store, initializer) où:
|
|
- store est une instance de web.session.Store, qui sert à stocker les
|
|
sessions.
|
|
- initializer est le dictionnaire initial, et peut être omis
|
|
|
|
Exemple:
|
|
return web.session.DiskStore('sessions'), {}
|
|
"""
|
|
return None
|
|
|
|
def _configure_handler_class(self, handler_class):
|
|
if getattr(handler_class, 'uio', None) is None:
|
|
handler_class.uio = self.uio
|
|
self.webapp.add_mapping(handler_class.PATH, handler_class)
|
|
|
|
def _configure_handler(self, handler):
|
|
handler.app = self
|
|
handler._render = self.render
|
|
handler.init()
|
|
|
|
def _wsgifunc(self):
|
|
u"""Obtenir la fonction pour servir les requêtes wsgi
|
|
"""
|
|
return self.webapp.wsgifunc()
|
|
|
|
def _start_server(self, server_type=None, server_socket=None):
|
|
u"""Démarrer le serveur web
|
|
"""
|
|
func = self._wsgifunc()
|
|
|
|
if os.environ.has_key('SERVER_SOFTWARE'): # cgi
|
|
os.environ['FCGI_FORCE_CGI'] = 'Y'
|
|
|
|
if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
|
|
or os.environ.has_key('SERVER_SOFTWARE')):
|
|
return web.wsgi.runfcgi(func, None)
|
|
|
|
if server_type in ('fcgi', 'fastcgi'):
|
|
return web.wsgi.runfcgi(func, server_socket or (self.HOST, self.PORT))
|
|
elif server_type in ('scgi', ):
|
|
return web.wsgi.runscgi(func, server_socket or (self.HOST, self.PORT))
|
|
else:
|
|
return web.httpserver.runsimple(func, (self.HOST, self.PORT))
|
|
|
|
def __configure(self):
|
|
web.config.debug = self.DEBUG
|
|
web.config.BASEDIR = self.basedir
|
|
|
|
self.webapp = self._new_application()
|
|
self.webapp.set_handler_configurator(self._configure_handler)
|
|
for handler_class in HANDLER_CLASSES:
|
|
self._configure_handler_class(handler_class)
|
|
|
|
session = None
|
|
tmp = self._new_session()
|
|
if tmp is not None:
|
|
tmp = seqof(tmp)
|
|
store = tmp[0:1] and tmp[0] or None
|
|
initializer = tmp[1:2] and tmp[1] or None
|
|
if store is not None:
|
|
session = web.config.get("_session", None)
|
|
if session is None:
|
|
session = web.session.Session(self.webapp, store, initializer=initializer)
|
|
web.config._session = session
|
|
self.session = session
|
|
|
|
tg = self.template_globals
|
|
if session is not None and not tg.has_key("session"):
|
|
tg["session"] = session
|
|
|
|
self.render = web.template.render(self.templatedir, cache=not self.DEBUG, globals=tg)
|
|
|
|
def wsgiapp(self):
|
|
u"""Retourner l'instance à utiliser pour mod_wsgi.
|
|
|
|
mod_wsgi s'attend à trouver une variable nommée application dans le
|
|
module spécifié dans la configuration. Il faut donc utiliser cette
|
|
méthode de cette façon:
|
|
|
|
class MyApp(Application):
|
|
...
|
|
|
|
application = MyApp().wsgiapp()
|
|
"""
|
|
self.__configure()
|
|
return self._wsgifunc()
|
|
|
|
def run(self, args=None, **ignored):
|
|
u"""Démarrer le serveur, en standalone, fastcgi ou scgi suivant les
|
|
arguments de la ligne de commande.
|
|
|
|
Utiliser cette méthode de cette façon:
|
|
|
|
class MyApp(Application):
|
|
...
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
MyApp().run(sys.argv[1:])
|
|
"""
|
|
if args is None: args = []
|
|
options, args = get_args(map(_u, args),
|
|
'S:s:H:P:D' + (self.OPTIONS or ''),
|
|
['server-type=', 'server-socket=', 'host=', 'port=', 'debug'] + list(self.LONG_OPTIONS or ()))
|
|
server_type = None
|
|
server_socket = None
|
|
for option, value in options:
|
|
if self.process_option(option, value): pass
|
|
elif self.is_option(option, value): pass #compatibilité
|
|
elif option in ('-S', '--server-type'): server_type = value
|
|
elif option in ('-s', '--server-socket'): server_socket = value
|
|
elif option in ('-H', '--host'): self.HOST = value
|
|
elif option in ('-P', '--port'): self.PORT = int(value)
|
|
elif option in ('-D', '--debug'): self.DEBUG = True
|
|
self.args = args
|
|
self.process_args(args)
|
|
self.__configure()
|
|
self.before_start()
|
|
self._start_server(server_type, server_socket)
|
|
|
|
def cgirun(self):
|
|
u"""Retourner un handler CGI. A utiliser avec Google AppEngine
|
|
|
|
Utiliser cette méthode de cette façon:
|
|
|
|
class MyApp(Application):
|
|
...
|
|
|
|
main = MyApp().cgirun()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
"""
|
|
self.__configure()
|
|
wsgifunc = self._wsgifunc()
|
|
try:
|
|
from google.appengine.ext.webapp.util import run_wsgi_app
|
|
return run_wsgi_app(wsgifunc)
|
|
except ImportError:
|
|
# we're not running from within Google App Engine
|
|
import wsgiref
|
|
return wsgiref.handlers.CGIHandler().run(wsgifunc)
|
|
|
|
# gestion des messages
|
|
def clear_msgs(self, level=None):
|
|
if level is not None:
|
|
self._msgs = [msg for msg in self._msgs if msg.level > level]
|
|
else: self._msgs = []
|
|
return self._msgs
|
|
def add_msg(self, msg_value, msg_class=None, level=None):
|
|
if msg_value is None and msg_class is None: return False
|
|
self._msgs.append(Msg(msg_value, msg_class, level))
|
|
return True
|
|
|
|
last_msg = property(lambda self: self._msgs and self._msgs[-1] or NO_MSG)
|
|
msg_value = property(lambda self: self.get_last_msg().msg_value)
|
|
msg_class = property(lambda self: self.get_last_msg().msg_class)
|