nulib/web: ajout de redirect() et set_profile(). améliorer la lisibilité des arguments

This commit is contained in:
Jephté Clain 2018-05-07 10:49:00 +04:00
parent c58a6ccaf7
commit 1a3550ed9a
4 changed files with 174 additions and 96 deletions

View File

@ -46,14 +46,20 @@ def load(prefix=None, icons=True, charset="utf-8"):
################################################################################
# Menu
NAVBAR_TYPE_MAP = {
'fixed': 'fixed-top',
'static': 'static-top',
}
class Menu(ui.Menu):
"""Un menu"""
def __init__(self, title, mitems=None, profiles=None,
id=None, p=None, c=None, t=None, **kw):
super(Menu, self).__init__(title, mitems, profiles, id, p, c, **kw)
if t is None: t = 'static'
self.navbar_type = t
id=None, in_profiles=None, css=None, navbar_type=None, **kw):
if navbar_type is None: navbar_type = kw.pop('t', None)
super(Menu, self).__init__(title, mitems, profiles, id, in_profiles, css, **kw)
if navbar_type is None: navbar_type = 'static'
self.navbar_type = navbar_type
def __mitem(self, mitem, sel_profile):
# ne pas afficher un menu qui n'est pas dans le bon profil
@ -87,16 +93,20 @@ class Menu(ui.Menu):
lines.append(u"""</ul>""")
return lines
def __call__(self, s=None, p=None, t=None):
if t is None: t = self.navbar_type
if s is not None: self.select(s, p)
def render(self, select=None, profile=None, navbar_type=None, **kw):
if select is None: select = kw.pop('s', None)
if profile is None: profile = kw.pop('p', None)
if navbar_type is None: navbar_type = kw.pop('t', None)
if navbar_type is None: navbar_type = self.navbar_type
if select is not None: self.select(select, profile)
selection = self.get_mitem()
lines = []
if t == 'fixed' or t == 'fixed-top':
if navbar_type == 'fixed' or navbar_type == 'fixed-top':
lines.append(u"""<style type="text/css">body { margin-top: 50px; }</style>""")
css = u''
css = ui.addclassif('navbar-fixed-top', t == 'fixed' or t == 'fixed-top', css)
css = ui.addclassif('navbar-static-top', t == 'static' or t == 'static-top', css)
css = ui.addclassif('navbar-fixed-top', navbar_type == 'fixed' or navbar_type == 'fixed-top', css)
css = ui.addclassif('navbar-static-top', navbar_type == 'static' or navbar_type == 'static-top', css)
if self.profiles is not None and self.sel_profile is not None:
css = ui.addclassif('%s-profile' % self.sel_profile, None, css)
css = ui.addclassif(self.css, None, css)
@ -145,10 +155,9 @@ class Menu(ui.Menu):
</div>
</div>""")
return u"\n".join(lines)
def __unicode__(self): return self.render()
def __unicode__(self): return self()
def set_menu(menu):
def set_menu(menu, **params):
"""Décorateur qui permet d'initialiser une instance de menu dans la session
"""
def decorator(method):
@ -159,16 +168,24 @@ def set_menu(menu):
return wrapper
return decorator
def menu(id=None, p=None):
"""Décorateur qui permet de mettre à jour la sélection et le profil dans le menu
def menu(id=None, profile=None, **params):
"""Décorateur qui permet de mettre à jour la sélection et le profil dans
le menu
par défaut, prendre le profil courant dans la session. le profil courant
est initialisé par le décorateur @set_profile
"""
if profile is None: profile = params.pop('p', None)
def decorator(method):
def wrapper(self, *args, **kw):
session = _ensure_session(self)
if 'menu' not in session:
raise ValueError("menu is required")
raise ValueError("menu est requis")
sel_profile = profile
if sel_profile is None and 'profile' in session:
sel_profile = session.profile
if id is not None:
session.menu.select(id, p)
session.menu.select(id, sel_profile)
return method(self, *args, **kw)
return wrapper
return decorator
@ -229,64 +246,74 @@ class Alert(object):
action = None
showtb = None
def __init__(self, msg=None, e=Undef, t="error", c=False, x=None, action=None, showtb=True):
self(msg, e, t, c, x, action, showtb)
def __init__(self, msg=None, exc_info=Undef, type="error", closeable=False, escape=None, action=None, showtb=True, **kw):
self(msg, exc_info, type, closeable, escape, action, showtb, **kw)
def __call__(self, msg=Undef, exc_info=Undef, type=Undef, closeable=Undef, escape=Undef, action=Undef, showtb=Undef, **kw):
if exc_info is Undef: exc_info = kw.pop('e', Undef)
if type is Undef: type = kw.pop('t', Undef)
if closeable is Undef: closeable = kw.pop('c', Undef)
if escape is Undef: escape = kw.pop('x', Undef)
def __call__(self, msg=Undef, e=Undef, t=Undef, c=Undef, x=Undef, action=Undef, showtb=Undef):
if msg is Undef:
# si on ne spécifie pas de message, alors prendre la valeur actuelle
msg = self.msg
if e is Undef: e = self.exc_info
if exc_info is Undef: exc_info = self.exc_info
else:
# si on spécifie un message, alors prendre aussi l'exception courante
if e is Undef: e = sys.exc_info()
if e == (None, None, None): e = None
if t is Undef: t = self.type
if c is Undef: c = self.closeable
if x is Undef: x = self.escape
if exc_info is Undef: exc_info = sys.exc_info()
if exc_info == (None, None, None): exc_info = None
if type is Undef: type = self.type
if closeable is Undef: closeable = self.closeable
if escape is Undef: escape = self.escape
if action is Undef: action = self.action
if showtb is Undef: showtb = self.showtb
self.msg = msg
self.exc_info = e
self.type = ALERT_TYPE_MAP.get(t, t)
self.closeable = c
self.escape = x
self.exc_info = exc_info
self.type = ALERT_TYPE_MAP.get(type, type)
self.closeable = closeable
self.escape = escape
self.action = action
self.showtb = showtb
return self
def render(self, msg=Undef, e=Undef, t=Undef, c=Undef, x=Undef, action=Undef, showtb=Undef):
def render(self, msg=Undef, exc_info=Undef, type=Undef, closeable=Undef, escape=Undef, action=Undef, showtb=Undef, **kw):
if exc_info is Undef: exc_info = kw.pop('e', Undef)
if type is Undef: type = kw.pop('t', Undef)
if closeable is Undef: closeable = kw.pop('c', Undef)
if escape is Undef: escape = kw.pop('x', Undef)
if msg is Undef:
# si on ne spécifie pas de message, alors prendre la valeur initiale
msg = self.msg
if e is Undef: e = self.exc_info
if exc_info is Undef: exc_info = self.exc_info
else:
# si on spécifie un message, alors prendre aussi l'exception courante
if e is Undef: e = sys.exc_info()
if t is Undef: t = self.type
if c is Undef: c = self.closeable
if x is Undef: x = self.escape
if exc_info is Undef: exc_info = sys.exc_info()
if type is Undef: type = self.type
if closeable is Undef: closeable = self.closeable
if escape is Undef: escape = self.escape
if action is Undef: action = self.action
if showtb is Undef: showtb = self.showtb
if callable(msg):
# si msg est callable, par défaut ne pas mettre le résultat en
# échappement
if x is None: x = False
if escape is None: escape = False
msg = msg()
if x is None: x = True
if escape is None: escape = True
if msg is None and e is not None:
if msg is None and exc_info is not None:
msg = u"Une erreur inattendue s'est produite"
if msg is None: return u""
if x: msg = web.websafe(msg)
if escape: msg = web.websafe(msg)
lines = []
css = u"alert alert-%s" % t
css = ui.addclassif("alert-dismissible", c, css)
css = u"alert alert-%s" % ALERT_TYPE_MAP.get(type, type)
css = ui.addclassif("alert-dismissible", closeable, css)
lines.append(u"""<div class="%s" role="alert">""" % css)
if c:
if closeable:
lines.append(u"""<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>""")
lines.append(p(msg))
if action is not None:
@ -296,19 +323,19 @@ class Alert(object):
title = web.websafe(action.title or u"Cliquez ici pour continuer")
action = u"""<p><a href="%s" accesskey="r">%s</a></p>""" % (url, title)
lines.append(p(action))
if e is not None:
if exc_info is not None:
lines.append(u"""<div class="small" style="margin-top: 3em; color: gray;">Pour information, le message d'erreur technique est&nbsp;&nbsp;&nbsp;""")
lines.extend(traceback.format_exception_only(*e[:2]))
lines.extend(traceback.format_exception_only(*exc_info[:2]))
if showtb:
lines.append(u"<!--")
lines.append("".join(traceback.format_exception(*e)))
lines.append("".join(traceback.format_exception(*exc_info)))
lines.append(u"-->")
lines.append(u"""</div>""")
lines.append(u"""</div>""")
return u"\n".join([u(line) for line in lines])
def __unicode__(self): return self.render()
def set_alert(template=None, action=Undef, delay=None):
def set_alert(template=None, action=Undef, delay=None, **params):
"""Décorateur qui permet de gérer automatiquement une instance de Alert dans la
page.
@ -357,10 +384,14 @@ def set_alert(template=None, action=Undef, delay=None):
lines.append(u"""\
<title>%(title)s</title>
</head>
<body>
<body>""" % locals())
if web.config.have_session:
session = web.config._session
if 'menu' in session: lines.append(session.menu.render())
lines.append(u"""\
<div class="container">
<h1>%(title)s</h1>""" % locals())
lines.append(alert())
lines.append(alert.render())
lines.append(u"""\
</div>
</body>

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
__all__ = (
'nocache', 'auth', 'defaults',
'nocache', 'auth', 'defaults', 'redirect',
'reset_session', 'set_session', 'session', 'check_session',
'set_profile',
'Page', 'Application',
)
@ -45,7 +46,7 @@ class MetaPage(type):
_fix_PATH(cls, name)
HANDLER_CLASSES.append(cls)
def nocache(method):
def nocache(method, **params):
"""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):
@ -55,7 +56,7 @@ def nocache(method):
return method(self, *args, **kw)
return wrapper
def auth(authenticator=None, realm='nulib'):
def auth(authenticator=None, realm='nulib', **params):
"""Décorateur pour s'assurer que la méthode web est authentifiée.
La fonction authenticator avec la signature (username, password) permet
@ -75,6 +76,22 @@ def auth(authenticator=None, realm='nulib'):
return wrapper
return decorator
def redirect(desturl, **params):
"""rediriger vers l'url spécifiée si la méthode retourne None.
L'url est enregistrée dans l'objet avec le nom desturl au cas on
voudrait s'en servir dans la page retournée.
"""
def decorator(method):
def wrapper(self, *args, **kw):
self.desturl = desturl
self.desturl_params = params
result = method(self, *args, **kw)
if result is not None: return result
else: return web.redirect(web.url(desturl, **params))
return wrapper
return decorator
def defaults(*required, **defaults):
"""Initialiser dans l'objet courant des variables à des valeurs par
défaut, ou en les prenant parmi les paramètres de la requête.
@ -207,6 +224,22 @@ def check_session(_onerror=None, **validators):
return wrapper
return decorator
def set_profile(profile=None, **params):
"""mettre à jour la valeur du profil courant
s'assurer que l'objet courant contient la variable session. si les sessions
n'ont pas été configurées au niveau de l'application, une exception est
levée
"""
if profile is None: profile = params.pop('p', None)
def decorator(method):
def wrapper(self, *args, **kw):
session = _ensure_session(self)
session.profile = profile
return method(self, *args, **kw)
return wrapper
return decorator
def _fix_PATH(cls, name):
if cls.PATH is None:
if cls.PREFIX is None:
@ -491,6 +524,8 @@ class Application(object):
session = web.session.Session(self.webapp, store, initializer=initializer)
self.session = web.config._session = session
web.config.have_session = session is not None
tg = self.template_globals
# session et application comme variables globales du template
tg.setdefault("S", session)
@ -529,7 +564,7 @@ class Application(object):
if args is None: args = []
options, args = get_args(map(_u, args),
'S:s:H:P:Dp:' + (self.OPTIONS or ''),
['server-type=', 'server-socket=', 'host=', 'port=', 'debug', 'profile='] + list(self.LONG_OPTIONS or ()))
['server-type=', 'server-socket=', 'host=', 'port=', 'debug', 'no-debug', 'profile='] + list(self.LONG_OPTIONS or ()))
server_type = None
server_socket = None
for option, value in options:
@ -542,6 +577,8 @@ class Application(object):
elif option in ('-D', '--debug'):
self.DEBUG = True
set_verbosity('--debug')
elif option in ('--no-debug',):
self.DEBUG = False
elif option in ('-p', '--profile'): self.PROFILE = value
self.args = args
self.process_args(args)

View File

@ -82,11 +82,11 @@ class Action(odict):
url: cible de l'action ou du lien (avec querystring)
title: titre du lien ou de l'élément de menu
id: identifiant dans un menu
p (in_profiles): profils dans lequel l'action est valide. le premier profil
in_profiles (p): profils dans lequel l'action est valide. le premier profil
est le profil par défaut
c (css): classe css à appliquer au lien
ak (accesskey): accesskey à installer sur le lien
q (query): paramètres de la requête, sous forme de dictionnaire ou de chaine
css (c): classe css à appliquer au lien
accesskey (ak): accesskey à installer sur le lien
query (q): paramètres de la requête, sous forme de dictionnaire ou de chaine
method: type de requête, get ou post
PROFILE_url, PROFILE_query: valeurs spécifiques à certains profils, PROFILE
étant une valeur de in_profiles
@ -121,74 +121,79 @@ class Action(odict):
self[querykey] = query
self[urlkey] = url
def __init__(self, url, title=None, id=None, p=None, c=None, ak=None, q=None, method=None, **kw):
def __init__(self, url, title=None, id=None, in_profiles=None, css=None, accesskey=None, query=None, method=None, **kw):
super(Action, self).__init__(**kw)
if isseq(url):
if url[1:2]: title = url[1]
if url[2:3]: id = url[2]
if url[3:4]: p = url[3]
if url[4:5]: c = url[4]
if url[5:6]: ak = url[5]
if url[3:4]: in_profiles = url[3]
if url[4:5]: css = url[4]
if url[5:6]: accesskey = url[5]
if url[6:7]: method = url[6]
url = url[0] if url[0:1] else None
if url is None: raise ValueError("url is required")
if title is None: title = url
if p is None: p = kw.get('in_profiles', None)
if c is None: c = kw.get('css', None)
if ak is None: ak = kw.get('accesskey', None)
if q is None: q = kw.get('query', None)
if in_profiles is None: in_profiles = kw.pop('p', None)
if css is None: css = kw.pop('c', None)
if accesskey is None: accesskey = kw.pop('ak', None)
if query is None: query = kw.pop('q', None)
if method is None: method = 'get'
self.title = title
self.id = id
self.css = c
self.accesskey = ak
self.css = css
self.accesskey = accesskey
self.method = method
self.url = url
self.query = q
self.query = query
self.__update_url('url', 'query', 'baseurl')
self.in_profiles = seqof(p, None)
self.in_profiles = seqof(in_profiles, None)
if self.in_profiles is None: return
for p in self.in_profiles:
self.__update_url(*['%s_%s' % (p, key) for key in ('url', 'query', 'baseurl')])
self.__update_url(*['to_%s_%s' % (p, key) for key in ('url', 'query', 'baseurl')])
for profile in self.in_profiles:
self.__update_url(*['%s_%s' % (profile, key) for key in ('url', 'query', 'baseurl')])
self.__update_url(*['to_%s_%s' % (profile, key) for key in ('url', 'query', 'baseurl')])
def __get_key(self, key, p=None, to=False):
if p is not None:
pkey = '%s%s_%s' % ('to_' if to else '', p, key)
def __get_key(self, key, profile=None, to=False):
if profile is not None:
pkey = '%s%s_%s' % ('to_' if to else '', profile, key)
value = self.get(pkey, None)
if value is not None: return value
return self[key]
def get_baseurl(self, p=None, to=False):
def get_baseurl(self, profile=None, to=False, **kw):
"""obtenir l'url de base
"""
return self.__get_key('baseurl', p, to)
def get_query(self, p=None, to=False):
if profile is None: profile = kw.pop('p', None)
return self.__get_key('baseurl', profile, to)
def get_query(self, profile=None, to=False, **kw):
"""obtenir les paramètres de la requête sous forme de dictionnaire
"""
return self.__get_key('query', p, to)
def get_url(self, p=None, to=False):
if profile is None: profile = kw.pop('p', None)
return self.__get_key('query', profile, to)
def get_url(self, profile=None, to=False, **kw):
"""obtenir l'url
"""
return self.__get_key('url', p, to)
if profile is None: profile = kw.pop('p', None)
return self.__get_key('url', profile, to)
def get_qs(self, p=None, to=False, sep=None):
def get_qs(self, profile=None, to=False, sep=None, **kw):
"""obtenir les paramètres de la requête sous forme de query-string
sep vaut par défaut '?' mais peut valoir '&'
"""
query = self.get_query(p, to)
if profile is None: profile = kw.pop('p', None)
query = self.get_query(profile, to)
if sep is None: sep = '?'
return '%s%s' % (sep, urllib.urlencode(query, True))
qs = property(get_qs)
get_querystring = get_qs; querystring = property(get_qs)
def get_inputs(self, p=None, to=False):
def get_inputs(self, profile=None, to=False, **kw):
"""obtenir les paramètres de la requête sous forme d'une liste de définitions
d'éléments de formulaire de type hidden
"""
query = self.get_query(p, to)
if profile is None: profile = kw.pop('p', None)
query = self.get_query(profile, to)
Hidden = web.form.Hidden
inputs = []
for name, value in query.items():
@ -212,7 +217,9 @@ class Menu(odict):
return nextid
def __init__(self, title, mitems=None, profiles=None,
id=None, p=None, c=None, **kw):
id=None, in_profiles=None, css=None, **kw):
if in_profiles is None: in_profiles = kw.pop('p', None)
if css is None: css = kw.pop('c', None)
super(Menu, self).__init__(**kw)
self.__dict__['nextid'] = 0
self.__dict__['idmap'] = {}
@ -225,7 +232,7 @@ class Menu(odict):
self.default_profile = profiles[0] if profiles is not None else None
self.id = str(id)
self.in_profiles = seqof(p, None)
self.css = c
self.css = css
self.sel_id = None
self.sel_profile = None
self.active = False
@ -244,7 +251,7 @@ class Menu(odict):
self.mitems.append(mitem)
self.idmap[mitem.id] = mitem
def reset_selection(self):
def reset_selection(self, **kw):
self.sel_id = None
self.sel_profile = self.default_profile
self.active = False
@ -254,7 +261,8 @@ class Menu(odict):
else:
mitem.active = False
def select(self, id, p=None):
def select(self, id, profile=None, **kw):
if profile is None: profile = kw.pop('p', None)
# d'abord déselectionner tout le monde
self.reset_selection()
# ensuite chercher le mitem à sélectionner
@ -262,10 +270,10 @@ class Menu(odict):
mitem = self.idmap.get(id, None)
if mitem is not None:
self.sel_id = id
if p is None and mitem.in_profiles:
p = mitem.in_profiles[0]
if p is None: p = self.default_profile
self.sel_profile = p
if profile is None and mitem.in_profiles:
profile = mitem.in_profiles[0]
if profile is None: profile = self.default_profile
self.sel_profile = profile
mitem.active = self.active = True
return True
else:
@ -278,7 +286,7 @@ class Menu(odict):
return True
return False
def get_mitem(self, id=None):
def get_mitem(self, id=None, **kw):
"""retourner l'élément de menu correspondant à la sélection courante
"""
if id is None: id = self.sel_id

View File

@ -2,11 +2,13 @@
__all__ = (
'web', 'Page', 'bs',
'nocache', 'auth', 'defaults',
'nocache', 'auth', 'defaults', 'redirect',
'reset_session', 'set_session', 'session', 'check_session',
'set_profile',
'config',
)
from nulib.web import web, Page, bs, nocache, auth, defaults
from nulib.web import web, Page, bs, nocache, auth, defaults, redirect
from nulib.web import reset_session, set_session, session, check_session
from nulib.web import set_profile
from nulib.web.config_loader import config