nulib: corriger l'affichage du menu

This commit is contained in:
Jephté Clain 2018-05-08 21:45:53 +04:00
parent 4d2bf89e23
commit c7286d1406
3 changed files with 125 additions and 86 deletions

View File

@ -2,7 +2,7 @@
__all__ = ( __all__ = (
'_weburl', '_ensure_session', '_weburl', '_ensure_session',
'u', 'uval', 'u', 'uval', 'uval_would_escape',
) )
from types import UnicodeType, StringType from types import UnicodeType, StringType
@ -28,11 +28,21 @@ def u(text):
text = unicode(text, "utf-8") text = unicode(text, "utf-8")
return text return text
def uval(value): def uval_would_escape(value):
"""Transformer value en unicode, en particulier s'il est callable ou s'il """Tester si uval() mettrait value en échappement
a une méthode render() """
if value is None: return False
if hasattr(value, 'render'): return False
elif callable(value): return False
return True
def uval(value, escape=True):
"""Transformer value en unicode pour affichage.
Si value est un objet qui a une méthode render() ou qui est callable(),
alors utiliser le résultat comme valeur sinon utiliser web.websafe()
""" """
if value is None: return None if value is None: return None
if hasattr(value, 'render'): value = value.render() if hasattr(value, 'render'): value = value.render()
if callable(value): value = value() elif callable(value): value = value()
elif escape: value = web.websafe(value)
return u(value) return u(value)

View File

@ -3,7 +3,7 @@
__all__ = ( __all__ = (
'load', 'load',
'table', 'tr', 'td', 'table', 'tr', 'td',
'Menu', 'Action', 'Menu', 'Action', 'litteral', 'p',
'About', 'About',
'Alert', 'set_alert', 'Alert', 'set_alert',
) )
@ -15,33 +15,31 @@ from ..base import isseq, seqof, Undef
from ..ext import web from ..ext import web
from . import ui from . import ui
from .api import _ensure_session, u, uval from .api import _ensure_session, u, uval, uval_would_escape
from .model import Row, TableSchema from .model import Row, TableSchema
from .ui import Action, p from .ui import Action, litteral, p
Undef = Undef() # spécifique à ce module
def load(prefix=None, icons=True, charset="utf-8"): def load(prefix=None, icons=True, charset="utf-8"):
if prefix is None: prefix = "/static/bootstrap/" if prefix is None: prefix = "/static/bootstrap/"
elif not prefix.endswith("/"): prefix = prefix + "/" elif not prefix.endswith("/"): prefix = prefix + "/"
lines = [ lines = [
'<meta charset="%s" />' % charset, u"""<meta charset="%s" />""" % charset,
'<meta http-equiv="X-UA-Compatible" content="IE=edge" />', u"""<meta http-equiv="X-UA-Compatible" content="IE=edge" />""",
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />', u"""<meta name="viewport" content="width=device-width, initial-scale=1.0" />""",
] ]
if icons: if icons:
lines.extend([ lines.extend([
'<link rel="shortcut" href="%sfavicon.ico" />' % prefix, u"""<link rel="shortcut" href="%sfavicon.ico" />""" % prefix,
'<link rel="icon apple-touch-icon" href="%sicon.png" />' % prefix, u"""<link rel="icon apple-touch-icon" href="%sicon.png" />""" % prefix,
]) ])
lines.extend([ lines.extend([
ui.css("%sbootstrap/css/bootstrap.min.css" % prefix), ui.css(u"%sbootstrap/css/bootstrap.min.css" % prefix),
ui.css("%sbslocal.css" % prefix), ui.css(u"%sbslocal.css" % prefix),
ui.js("%sjquery.min.js" % prefix), ui.js(u"%sjquery.min.js" % prefix),
ui.js("%sbootstrap/js/bootstrap.min.js" % prefix), ui.js(u"%sbootstrap/js/bootstrap.min.js" % prefix),
u"<!--[if lt IE 9]>", u"<!--[if lt IE 9]>",
ui.js("%shtml5shiv/html5shiv.js" % prefix), ui.js(u"%shtml5shiv/html5shiv.js" % prefix),
ui.js("%srespond/respond.min.js" % prefix), ui.js(u"%srespond/respond.min.js" % prefix),
u"<![endif]-->", u"<![endif]-->",
]) ])
return u"\n".join(lines) return u"\n".join(lines)
@ -63,7 +61,7 @@ def trd_joinc(css):
classes = [] classes = []
for css in seqof(css): for css in seqof(css):
classes.append(TRD_CLASS_MAP.get(css, css)) classes.append(TRD_CLASS_MAP.get(css, css))
css = ' '.join(classes) css = " ".join(classes)
return css return css
def td(value, schema=None, css=Undef, tag=Undef, **kw): def td(value, schema=None, css=Undef, tag=Undef, **kw):
@ -72,9 +70,9 @@ def td(value, schema=None, css=Undef, tag=Undef, **kw):
if schema: if schema:
value = schema.html(value) value = schema.html(value)
if css is Undef: css = schema.css(value) if css is Undef: css = schema.css(value)
if tag is Undef: tag = 'td' if tag is Undef: tag = "td"
css = trd_joinc(css) css = trd_joinc(css)
return u'<%s%s>%s</%s>' % (tag, ui.classif(css), value, tag) return u"<%s%s>%s</%s>" % (tag, ui.classif(css), value, tag)
def tr(values, schema=None, css=Undef, celltag=Undef, **kw): def tr(values, schema=None, css=Undef, celltag=Undef, **kw):
css = Undef.sa(css, kw, 'c') css = Undef.sa(css, kw, 'c')
@ -84,7 +82,7 @@ def tr(values, schema=None, css=Undef, celltag=Undef, **kw):
tdschema = lambda value, index: schema.cell_schema(value, index) tdschema = lambda value, index: schema.cell_schema(value, index)
css = trd_joinc(css) css = trd_joinc(css)
lines = [] lines = []
lines.append(u'<tr%s>' % ui.classif(css)) lines.append(u"<tr%s>" % ui.classif(css))
index = 0 index = 0
if isinstance(values, dict) or isinstance(values, Row): if isinstance(values, dict) or isinstance(values, Row):
if schema is not None: if schema is not None:
@ -100,8 +98,8 @@ def tr(values, schema=None, css=Undef, celltag=Undef, **kw):
for value in values: for value in values:
lines.append(td(value, tdschema(value, index), tag=celltag)) lines.append(td(value, tdschema(value, index), tag=celltag))
index += 1 index += 1
lines.append(u'</tr>') lines.append(u"</tr>")
return u'\n'.join(lines) return u"\n".join(lines)
TABLE_CLASS_MAP = dict( TABLE_CLASS_MAP = dict(
auto="table-auto", a="table-auto", auto="table-auto", a="table-auto",
@ -114,10 +112,10 @@ def table_joinc(css):
if css is Undef: css = None if css is Undef: css = None
if type(css) is StringType: if type(css) is StringType:
css = css.split(",") css = css.split(",")
classes = ['table'] classes = ["table"]
for css in seqof(css): for css in seqof(css):
classes.append(TABLE_CLASS_MAP.get(css, css)) classes.append(TABLE_CLASS_MAP.get(css, css))
css = ' '.join(classes) css = " ".join(classes)
return css return css
def table(rows, schema=None, css=Undef, **kw): def table(rows, schema=None, css=Undef, **kw):
@ -127,17 +125,17 @@ def table(rows, schema=None, css=Undef, **kw):
if css is Undef: css = schema.css(rows) if css is Undef: css = schema.css(rows)
css = table_joinc(css) css = table_joinc(css)
lines = [] lines = []
lines.append(u'<table%s>' % ui.classif(css)) lines.append(u"<table%s>" % ui.classif(css))
lines.append(u'<thead>') lines.append(u"<thead>")
lines.append(tr(schema.headers(), schema.header_row_schema(), celltag='th')) lines.append(tr(schema.headers(), schema.header_row_schema(), celltag="th"))
lines.append(u'</thead><tbody>') lines.append(u"</thead><tbody>")
index = 0 index = 0
for row in rows: for row in rows:
lines.append(tr(row, schema.body_row_schema(index))) lines.append(tr(row, schema.body_row_schema(index)))
index += 1 index += 1
lines.append(u'</tbody>') lines.append(u"</tbody>")
lines.append(u'</table>') lines.append(u"</table>")
return u'\n'.join(lines) return u"\n".join(lines)
################################################################################ ################################################################################
# Menu # Menu
@ -150,13 +148,13 @@ NAVBAR_TYPE_MAP = {
class Menu(ui.Menu): class Menu(ui.Menu):
"""Un menu""" """Un menu"""
def __init__(self, title, mitems=None, profiles=None, def __init__(self, text=None, mitems=None, profiles=None,
id=None, in_profiles=None, css=None, id=None, in_profiles=Undef, css=Undef, title=Undef,
navbar_type=None, container_fluid=None, **kw): navbar_type=Undef, container_fluid=Undef, **kw):
if navbar_type is None: navbar_type = kw.pop('t', None) navbar_type = Undef.sa(navbar_type, kw, 't', None)
if container_fluid is None: container_fluid = kw.pop('f', None) container_fluid = Undef.sa(container_fluid, kw, 'f', None)
super(Menu, self).__init__(title, mitems, profiles, id, in_profiles, css, **kw) super(Menu, self).__init__(text, mitems, profiles, id, in_profiles, css, title, **kw)
if navbar_type is None: navbar_type = 'static' if navbar_type is None: navbar_type = "static"
self.navbar_type = navbar_type self.navbar_type = navbar_type
if container_fluid is None: container_fluid = True if container_fluid is None: container_fluid = True
self.container_fluid = container_fluid self.container_fluid = container_fluid
@ -169,12 +167,12 @@ class Menu(ui.Menu):
elif mitem.in_profiles is not None: elif mitem.in_profiles is not None:
if sel_profile not in mitem.in_profiles: if sel_profile not in mitem.in_profiles:
return [] return []
active = ' class="active"' if mitem.active else '' li_class = ui.classif("active", mitem.active)
url = web.websafe(mitem.url) url = uval(mitem.url)
css = ui.classif(mitem.css) a_class = ui.classif(mitem.css)
accesskey = ui.accesskeyif(mitem.accesskey) accesskey = ui.accesskeyif(mitem.accesskey)
title = web.websafe(mitem.title) text = uval(mitem.text)
return [u"""<li%s><a href="%s"%s%s>%s</a></li>""" % (active, url, css, accesskey, title)] return [u"""<li%s><a href="%s"%s%s>%s</a></li>""" % (li_class, url, a_class, accesskey, text)]
def __dropdown(self, menu, profile): def __dropdown(self, menu, profile):
children = [] children = []
@ -183,11 +181,11 @@ class Menu(ui.Menu):
if not children: if not children:
# ne pas afficher un menu vide # ne pas afficher un menu vide
return [] return []
active = ' active' if menu.active else '' li_class = ui.addclassif("active", menu.active, "dropdown")
lines = [u"""<li class="dropdown%s">""" % active] lines = [u"""<li class="%s">""" % li_class]
firsturl = web.websafe(menu.mitems[0].url) firsturl = uval(menu.mitems[0].url)
title = web.websafe(menu.title) text = uval(menu.text)
lines.append(u"""<a href="%s" class="dropdown-toggle" data-toggle="dropdown">%s <b class="caret"></b></a>""" % (firsturl, title)) lines.append(u"""<a href="%s" class="dropdown-toggle" data-toggle="dropdown">%s <b class="caret"></b></a>""" % (firsturl, text))
lines.append(u"""<ul class="dropdown-menu">""") lines.append(u"""<ul class="dropdown-menu">""")
lines.extend(children) lines.extend(children)
lines.append(u"""</ul>""") lines.append(u"""</ul>""")
@ -212,10 +210,10 @@ class Menu(ui.Menu):
if self.__is_navbar_fixed(navbar_type): if self.__is_navbar_fixed(navbar_type):
lines.append(u"""<style type="text/css">body { margin-top: 50px; }</style>""") lines.append(u"""<style type="text/css">body { margin-top: 50px; }</style>""")
css = u'' css = u''
css = ui.addclassif('navbar-fixed-top', self.__is_navbar_fixed(navbar_type), css) css = ui.addclassif("navbar-fixed-top", self.__is_navbar_fixed(navbar_type), css)
css = ui.addclassif('navbar-static-top', self.__is_navbar_static(navbar_type), css) css = ui.addclassif("navbar-static-top", self.__is_navbar_static(navbar_type), css)
if self.profiles is not None and self.sel_profile is not None: 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("%s-profile" % self.sel_profile, None, css)
css = ui.addclassif(self.css, None, css) css = ui.addclassif(self.css, None, css)
lines.append(u"""<div class="navbar navbar-default%s" role="navigation">""" % css) lines.append(u"""<div class="navbar navbar-default%s" role="navigation">""" % css)
lines.append(u"""<div class="container%s"> lines.append(u"""<div class="container%s">
@ -225,10 +223,16 @@ class Menu(ui.Menu):
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>""" % (u'-fluid' if container_fluid else ''))
</div> text = self.text
if text is not None:
if uval_would_escape(text):
lines.append(u"""<span class="navbar-brand">%s</span>""" % uval(text))
else:
lines.append(uval(text))
lines.append(u"""</div>
<div class="navbar-collapse collapse"> <div class="navbar-collapse collapse">
<ul class="nav navbar-nav">""" % ('-fluid' if container_fluid else '')) <ul class="nav navbar-nav">""")
if self.profiles is not None: if self.profiles is not None:
# gestion des profils # gestion des profils
lines.append(u"""<li class="navbar-text"><form class="form-inline" method="get">""") lines.append(u"""<li class="navbar-text"><form class="form-inline" method="get">""")
@ -239,7 +243,7 @@ class Menu(ui.Menu):
and profile in selection.in_profiles: and profile in selection.in_profiles:
lines.append(u"""<button class="btn btn-xs btn-primary active" type="submit" formaction="%s" formmethod="%s"><span>%s</span></button>""" % (selection.url, selection.method, profile)) lines.append(u"""<button class="btn btn-xs btn-primary active" type="submit" formaction="%s" formmethod="%s"><span>%s</span></button>""" % (selection.url, selection.method, profile))
continue continue
profile_ref = '%s_ref' % profile profile_ref = "%s_ref" % profile
profile_mitem = None profile_mitem = None
if profile_ref in selection: if profile_ref in selection:
profile_mitem = self.get_mitem(selection[profile_ref]) profile_mitem = self.get_mitem(selection[profile_ref])
@ -254,9 +258,8 @@ class Menu(ui.Menu):
else: else:
lines.extend(self.__mitem(mitem, self.sel_profile)) lines.extend(self.__mitem(mitem, self.sel_profile))
lines.append(u"""</ul>""") lines.append(u"""</ul>""")
title = self.title if selection is not None:
if title is not None: title = uval(selection.title)
title = web.websafe(title)
lines.append(u"""<p class="navbar-text"><b><em>%s</em></b></p>""" % title) lines.append(u"""<p class="navbar-text"><b><em>%s</em></b></p>""" % title)
lines.append(u"""</div> lines.append(u"""</div>
</div> </div>
@ -312,7 +315,7 @@ jQuery.noConflict()(function($) {
if onShowDetails is not None: if onShowDetails is not None:
if callable(onShowDetails): onShowDetails = onShowDetails() if callable(onShowDetails): onShowDetails = onShowDetails()
lines.append(onShowDetails) lines.append(onShowDetails)
lines.append("""\ lines.append(u"""\
return false; return false;
}); });
}); });
@ -432,8 +435,8 @@ class Alert(object):
if callable(action): action = action() if callable(action): action = action()
if isinstance(action, Action): if isinstance(action, Action):
url = web.websafe(action.url) url = web.websafe(action.url)
title = web.websafe(action.title or u"Cliquez ici pour continuer") text = web.websafe(action.text or u"Cliquez ici pour continuer")
action = u"""<p><a href="%s" accesskey="r">%s</a></p>""" % (url, title) action = u"""<p><a href="%s" accesskey="r">%s</a></p>""" % (url, text)
lines.append(p(action)) lines.append(p(action))
if exc_info 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.append(u"""<div class="small" style="margin-top: 3em; color: gray;">Pour information, le message d'erreur technique est&nbsp;&nbsp;&nbsp;""")

View File

@ -5,14 +5,15 @@ __all__ = (
'accesskeyif', 'accesskeyif',
'classif', 'addclassif', 'classif', 'addclassif',
'favicon', 'css', 'js', 'jscss', 'favicon', 'css', 'js', 'jscss',
'p', 'litteral', 'p',
'Action', 'Menu', 'Action', 'Menu',
) )
import urlparse, urllib import urlparse, urllib
from nulib.base import odict, isseq, seqof from ..base import Undef, odict, isseq, seqof
from nulib.web import web from ..ext import web
from .api import u from .api import u
def checked(b): def checked(b):
@ -57,8 +58,17 @@ def jscss(href):
csshref = href csshref = href
return u"%s\n%s" % (js(jshref), css(csshref)) return u"%s\n%s" % (js(jshref), css(csshref))
class litteral(object):
"""Objet proxy vers un texte littéral.
à utiliser avec uval() pour ne pas mettre en échappement la valeur
"""
def __init__(self, text): self.text = text
def __call__(self): return self.text
__unicode__ = __call__
def p(text): def p(text):
"""Si text commence par un tag, le laisser tel quel, sinon le mettre entre <p> et </p> """Si text commence par un tag, le laisser tel quel, sinon le mettre entre <p> et </p>
text doit avoir le cas échéant déjà été traité avec uval
""" """
text = u(text) text = u(text)
if text[0:1] == '<': return text if text[0:1] == '<': return text
@ -71,7 +81,8 @@ class Action(odict):
"""Un lien ou une action d'un formulaire """Un lien ou une action d'un formulaire
url: cible de l'action ou du lien (avec querystring) url: cible de l'action ou du lien (avec querystring)
title: titre du lien ou de l'élément de menu text: titre du lien ou de l'élément de menu
title: description de l'action. vaut text par défaut
id: identifiant dans un menu id: identifiant dans un menu
in_profiles (p): 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 est le profil par défaut
@ -112,28 +123,39 @@ class Action(odict):
self[querykey] = query self[querykey] = query
self[urlkey] = url self[urlkey] = url
def __init__(self, url, title=None, id=None, in_profiles=None, css=None, accesskey=None, query=None, method=None, **kw): def __init__(self, url, text=Undef, id=Undef, in_profiles=Undef, css=Undef, accesskey=Undef, query=Undef, title=Undef, method=Undef, **kw):
text = Undef.sa(text, kw, 't', None)
id = Undef.sa(id, kw, None, None)
in_profiles = Undef.sa(in_profiles, kw, 'p', None)
css = Undef.sa(css, kw, 'c', None)
accesskey = Undef.sa(accesskey, kw, 'ak', None)
query = Undef.sa(query, kw, 'q', None)
title = Undef.sa(title, kw, 'nt')
method = Undef.sa(method, kw, 'm', None)
super(Action, self).__init__(**kw) super(Action, self).__init__(**kw)
if isseq(url): if isseq(url):
if url[1:2]: title = url[1] if url[1:2]: text = url[1]
if url[2:3]: id = url[2] if url[2:3]: id = url[2]
if url[3:4]: in_profiles = url[3] if url[3:4]: in_profiles = url[3]
if url[4:5]: css = url[4] if url[4:5]: css = url[4]
if url[5:6]: accesskey = url[5] if url[5:6]: accesskey = url[5]
if url[6:7]: method = url[6] if url[6:7]: title = url[6]
if url[7:8]: method = url[7]
url = url[0] if url[0:1] else None url = url[0] if url[0:1] else None
if url is None: raise ValueError("url is required") if url is None: raise ValueError("url is required")
if title is None: title = url if text is None: text = url
if in_profiles is None: in_profiles = kw.pop('p', None) if in_profiles is None: in_profiles = kw.pop('p', None)
if css is None: css = kw.pop('c', None) if css is None: css = kw.pop('c', None)
if accesskey is None: accesskey = kw.pop('ak', None) if accesskey is None: accesskey = kw.pop('ak', None)
if query is None: query = kw.pop('q', None) if query is None: query = kw.pop('q', None)
if title is Undef: title = text
if method is None: method = 'get' if method is None: method = 'get'
self.title = title self.text = text
self.id = id self.id = id
self.css = css self.css = css
self.accesskey = accesskey self.accesskey = accesskey
self.title = title
self.method = method self.method = method
self.url = url self.url = url
@ -207,26 +229,30 @@ class Menu(odict):
self.nextid += 1 self.nextid += 1
return nextid return nextid
def __init__(self, title, mitems=None, profiles=None, def __init__(self, text=None, mitems=None, profiles=None,
id=None, in_profiles=None, css=None, **kw): id=None, in_profiles=Undef, css=Undef, title=Undef, **kw):
if in_profiles is None: in_profiles = kw.pop('p', None) in_profiles = Undef.sa(in_profiles, kw, 'p', None)
if css is None: css = kw.pop('c', None) css = Undef.sa(css, kw, 'c', None)
title = Undef.sa(title, kw, 'nt')
super(Menu, self).__init__(**kw) super(Menu, self).__init__(**kw)
self.__dict__['nextid'] = 0 self.__dict__['nextid'] = 0
self.__dict__['idmap'] = {} self.__dict__['idmap'] = {}
if title is None: title = u""
if text is None: text = u""
if id is None: id = self.__nextid() if id is None: id = self.__nextid()
self.title = title if title is Undef: title = text
self.mitems = [] self.text = text
profiles = seqof(profiles, None) profiles = seqof(profiles, None)
self.profiles = profiles self.profiles = profiles
self.default_profile = profiles[0] if profiles is not None else None self.default_profile = profiles[0] if profiles is not None else None
self.id = str(id) self.id = str(id)
self.in_profiles = seqof(p, None) self.in_profiles = seqof(p, None)
self.css = css self.css = css
self.title = title
self.sel_id = None self.sel_id = None
self.sel_profile = None self.sel_profile = None
self.active = False self.active = False
self.mitems = []
for mitem in seqof(mitems): for mitem in seqof(mitems):
self.add(mitem) self.add(mitem)
@ -252,8 +278,8 @@ class Menu(odict):
else: else:
mitem.active = False mitem.active = False
def select(self, id, profile=None, **kw): def select(self, id, profile=Undef, **kw):
if profile is None: profile = kw.pop('p', None) profile = Undef.sa(profile, kw, 'p', None)
# d'abord déselectionner tout le monde # d'abord déselectionner tout le monde
self.reset_selection() self.reset_selection()
# ensuite chercher le mitem à sélectionner # ensuite chercher le mitem à sélectionner
@ -277,10 +303,10 @@ class Menu(odict):
return True return True
return False return False
def get_mitem(self, id=None, **kw): def get_mitem(self, id=Undef, **kw):
"""retourner l'élément de menu correspondant à la sélection courante """retourner l'élément de menu correspondant à la sélection courante
""" """
if id is None: id = self.sel_id id = Undef.sa(id, kw, None, self.sel_id)
if id is None: return None if id is None: return None
id = str(id) id = str(id)
mitem = self.idmap.get(id, None) mitem = self.idmap.get(id, None)