2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
|
|
"""Simple form generator/validator
|
|
|
|
|
|
|
|
E. Viennet 2005 - 2008
|
|
|
|
|
2021-07-11 18:27:02 +02:00
|
|
|
v 1.3 (python3)
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2021-09-15 22:31:16 +02:00
|
|
|
import html
|
2022-01-10 12:00:02 +01:00
|
|
|
import re
|
2023-03-19 10:26:03 +01:00
|
|
|
|
|
|
|
import flask_wtf
|
|
|
|
import wtforms
|
|
|
|
from app import log
|
|
|
|
from app.scodoc.sco_exceptions import ScoInvalidCSRF
|
2022-08-29 15:23:44 +02:00
|
|
|
import app.scodoc.sco_utils as scu
|
2022-01-10 12:00:02 +01:00
|
|
|
|
|
|
|
# re validant dd/mm/yyyy
|
|
|
|
DMY_REGEXP = re.compile(
|
|
|
|
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
def TrivialFormulator(
|
|
|
|
form_url,
|
|
|
|
values,
|
|
|
|
formdescription=(),
|
2023-03-19 10:26:03 +01:00
|
|
|
initvalues=None,
|
2020-09-26 16:19:37 +02:00
|
|
|
method="post",
|
|
|
|
enctype=None,
|
|
|
|
submitlabel="OK",
|
|
|
|
name=None,
|
|
|
|
formid="tf",
|
2022-09-03 20:48:56 +02:00
|
|
|
form_attrs="",
|
2020-09-26 16:19:37 +02:00
|
|
|
cssclass="",
|
|
|
|
cancelbutton=None,
|
|
|
|
submitbutton=True,
|
2023-03-19 10:26:03 +01:00
|
|
|
submitbuttonattributes=None,
|
2020-09-26 16:19:37 +02:00
|
|
|
top_buttons=False, # place buttons at top of form
|
|
|
|
bottom_buttons=True, # buttons after form
|
|
|
|
html_foot_markup="",
|
|
|
|
readonly=False,
|
|
|
|
is_submitted=False,
|
2022-10-02 23:43:29 +02:00
|
|
|
title="",
|
|
|
|
after_table="",
|
|
|
|
before_table="{title}",
|
2020-09-26 16:19:37 +02:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
form_url : URL for this form
|
|
|
|
initvalues : dict giving default values
|
|
|
|
values : dict with all HTML form variables (may start empty)
|
|
|
|
is_submitted: handle form as if already submitted
|
|
|
|
|
|
|
|
Returns (status, HTML form, values)
|
|
|
|
status = 0 (html to display),
|
|
|
|
1 (ok, validated values in "values")
|
|
|
|
-1 cancel (if cancelbutton specified)
|
|
|
|
HTML form: html string (form to insert in your web page)
|
|
|
|
values: None or, when the form is submitted and correctly filled,
|
2021-02-05 22:16:30 +01:00
|
|
|
a dictionnary with the requeted values.
|
2020-09-26 16:19:37 +02:00
|
|
|
formdescription: sequence [ (field, description), ... ]
|
|
|
|
where description is a dict with following (optional) keys:
|
|
|
|
default : default value for this field ('')
|
|
|
|
title : text titre (default to field name)
|
|
|
|
allow_null : if true, field can be left empty (default true)
|
|
|
|
type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
|
|
|
|
readonly : default False. if True, no form element, display current value.
|
2021-11-14 18:09:20 +01:00
|
|
|
convert_numbers: convert int and float values (from string)
|
2020-09-26 16:19:37 +02:00
|
|
|
allowed_values : list of possible values (default: any value)
|
|
|
|
validator : function validating the field (called with (value,field)).
|
|
|
|
min_value : minimum value (for floats and ints)
|
|
|
|
max_value : maximum value (for floats and ints)
|
|
|
|
explanation: text string to display next the input widget
|
|
|
|
title_buble: help bubble on field title (needs bubble.js or equivalent)
|
|
|
|
comment : comment, showed under input widget
|
|
|
|
withcheckbox: if true, place a checkbox at the left of the input
|
|
|
|
elem. Checked items will be returned in 'tf-checked'
|
|
|
|
attributes: a liste of strings to put in the HTML form element
|
2021-02-05 22:16:30 +01:00
|
|
|
template: HTML template for element
|
2020-09-26 16:19:37 +02:00
|
|
|
HTML elements:
|
|
|
|
input_type : 'text', 'textarea', 'password',
|
|
|
|
'radio', 'menu', 'checkbox',
|
2022-10-02 23:43:29 +02:00
|
|
|
'hidden', 'separator', 'table_separator',
|
|
|
|
'file', 'date', 'datedmy' (avec validation),
|
2022-01-25 10:45:13 +01:00
|
|
|
'boolcheckbox', 'text_suggest',
|
|
|
|
'color'
|
2020-09-26 16:19:37 +02:00
|
|
|
(default text)
|
|
|
|
size : text field width
|
|
|
|
rows, cols: textarea geometry
|
|
|
|
labels : labels for radio or menu lists (associated to allowed_values)
|
|
|
|
vertical: for checkbox; if true, vertical layout
|
|
|
|
disabled_items: for checkbox, dict such that disabled_items[i] true if disabled checkbox
|
|
|
|
To use text_suggest elements, one must:
|
|
|
|
- specify options in text_suggest_options (a dict)
|
|
|
|
- HTML page must load JS AutoSuggest.js and CSS autosuggest_inquisitor.css
|
|
|
|
- bodyOnLoad must call JS function init_tf_form(formid)
|
|
|
|
"""
|
|
|
|
method = method.lower()
|
|
|
|
if method == "get":
|
|
|
|
enctype = None
|
|
|
|
t = TF(
|
|
|
|
form_url,
|
|
|
|
values,
|
|
|
|
formdescription,
|
2023-03-19 10:26:03 +01:00
|
|
|
initvalues or {},
|
2020-09-26 16:19:37 +02:00
|
|
|
method,
|
|
|
|
enctype,
|
|
|
|
submitlabel,
|
|
|
|
name,
|
|
|
|
formid,
|
2022-09-03 20:48:56 +02:00
|
|
|
form_attrs=form_attrs,
|
|
|
|
cssclass=cssclass,
|
2020-09-26 16:19:37 +02:00
|
|
|
cancelbutton=cancelbutton,
|
|
|
|
submitbutton=submitbutton,
|
2023-03-19 10:26:03 +01:00
|
|
|
submitbuttonattributes=submitbuttonattributes or [],
|
2020-09-26 16:19:37 +02:00
|
|
|
top_buttons=top_buttons,
|
|
|
|
bottom_buttons=bottom_buttons,
|
|
|
|
html_foot_markup=html_foot_markup,
|
|
|
|
readonly=readonly,
|
|
|
|
is_submitted=is_submitted,
|
2022-10-02 23:43:29 +02:00
|
|
|
title=title,
|
|
|
|
after_table=after_table,
|
|
|
|
before_table=before_table,
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
form = t.getform()
|
|
|
|
if t.canceled():
|
|
|
|
res = -1
|
|
|
|
elif t.submitted() and t.result:
|
|
|
|
res = 1
|
|
|
|
else:
|
|
|
|
res = 0
|
|
|
|
return res, form, t.result
|
|
|
|
|
|
|
|
|
2021-07-09 23:31:16 +02:00
|
|
|
class TF(object):
|
2020-09-26 16:19:37 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
form_url,
|
|
|
|
values,
|
2023-03-19 10:26:03 +01:00
|
|
|
formdescription=None,
|
|
|
|
initvalues=None,
|
2020-09-26 16:19:37 +02:00
|
|
|
method="POST",
|
|
|
|
enctype=None,
|
|
|
|
submitlabel="OK",
|
|
|
|
name=None,
|
|
|
|
formid="tf",
|
2022-09-03 20:48:56 +02:00
|
|
|
form_attrs="",
|
2020-09-26 16:19:37 +02:00
|
|
|
cssclass="",
|
|
|
|
cancelbutton=None,
|
|
|
|
submitbutton=True,
|
2023-03-19 10:26:03 +01:00
|
|
|
submitbuttonattributes=None,
|
2020-09-26 16:19:37 +02:00
|
|
|
top_buttons=False, # place buttons at top of form
|
|
|
|
bottom_buttons=True, # buttons after form
|
|
|
|
html_foot_markup="", # html snippet put at the end, just after the table
|
|
|
|
readonly=False,
|
|
|
|
is_submitted=False,
|
2022-10-02 23:43:29 +02:00
|
|
|
title="",
|
|
|
|
after_table="",
|
|
|
|
before_table="{title}",
|
2020-09-26 16:19:37 +02:00
|
|
|
):
|
|
|
|
self.form_url = form_url
|
2021-09-27 10:20:10 +02:00
|
|
|
self.values = values.copy()
|
2023-03-19 10:26:03 +01:00
|
|
|
self.formdescription = list(formdescription or [])
|
|
|
|
self.initvalues = initvalues or {}
|
2020-09-26 16:19:37 +02:00
|
|
|
self.method = method
|
|
|
|
self.enctype = enctype
|
|
|
|
self.submitlabel = submitlabel
|
|
|
|
if name:
|
|
|
|
self.name = name
|
|
|
|
else:
|
|
|
|
self.name = formid # 'tf'
|
|
|
|
self.formid = formid
|
2022-09-03 20:48:56 +02:00
|
|
|
self.form_attrs = form_attrs
|
2020-09-26 16:19:37 +02:00
|
|
|
self.cssclass = cssclass
|
|
|
|
self.cancelbutton = cancelbutton
|
|
|
|
self.submitbutton = submitbutton
|
2023-03-19 10:26:03 +01:00
|
|
|
self.submitbuttonattributes = submitbuttonattributes or []
|
2020-09-26 16:19:37 +02:00
|
|
|
self.top_buttons = top_buttons
|
|
|
|
self.bottom_buttons = bottom_buttons
|
|
|
|
self.html_foot_markup = html_foot_markup
|
2022-10-02 23:43:29 +02:00
|
|
|
self.title = title
|
|
|
|
self.after_table = after_table
|
|
|
|
self.before_table = before_table
|
2020-09-26 16:19:37 +02:00
|
|
|
self.readonly = readonly
|
|
|
|
self.result = None
|
|
|
|
self.is_submitted = is_submitted
|
|
|
|
if readonly:
|
|
|
|
self.top_buttons = self.bottom_buttons = False
|
|
|
|
self.cssclass += " readonly"
|
|
|
|
|
|
|
|
def submitted(self):
|
|
|
|
"true if form has been submitted"
|
|
|
|
if self.is_submitted:
|
|
|
|
return True
|
2023-03-19 10:26:03 +01:00
|
|
|
form_submitted = self.values.get(f"{self.formid}_submitted", False)
|
|
|
|
if form_submitted:
|
|
|
|
self.check_csrf()
|
|
|
|
return form_submitted
|
|
|
|
|
|
|
|
def check_csrf(self):
|
|
|
|
"""check token for POST forms.
|
|
|
|
Raises ScoInvalidCSRF on failure.
|
|
|
|
"""
|
|
|
|
if self.method == "post":
|
|
|
|
token = self.values.get("csrf_token")
|
|
|
|
try:
|
|
|
|
flask_wtf.csrf.validate_csrf(token)
|
|
|
|
except wtforms.validators.ValidationError as exc:
|
|
|
|
log(f"Form.check_csrf: invalid CSRF token\n{exc.args}")
|
|
|
|
raise ScoInvalidCSRF() from exc
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def canceled(self):
|
|
|
|
"true if form has been canceled"
|
2023-03-19 10:26:03 +01:00
|
|
|
return self.values.get(f"{self.formid}_cancel", False)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def getform(self):
|
|
|
|
"return HTML form"
|
|
|
|
R = []
|
|
|
|
msg = None
|
|
|
|
self.setdefaultvalues()
|
|
|
|
if self.submitted() and not self.readonly:
|
|
|
|
msg = self.checkvalues()
|
|
|
|
# display error message
|
|
|
|
R.append(tf_error_message(msg))
|
|
|
|
# form or view
|
|
|
|
if self.readonly:
|
|
|
|
R = R + self._ReadOnlyVersion(self.formdescription)
|
|
|
|
else:
|
|
|
|
R = R + self._GenForm()
|
|
|
|
#
|
|
|
|
return "\n".join(R)
|
|
|
|
|
|
|
|
__str__ = getform
|
|
|
|
__repr__ = getform
|
|
|
|
|
|
|
|
def setdefaultvalues(self):
|
|
|
|
"set default values and convert numbers to strings"
|
|
|
|
for (field, descr) in self.formdescription:
|
|
|
|
# special case for boolcheckbox
|
|
|
|
if descr.get("input_type", None) == "boolcheckbox" and self.submitted():
|
2021-07-09 17:47:06 +02:00
|
|
|
if field not in self.values:
|
2020-09-26 16:19:37 +02:00
|
|
|
self.values[field] = 0
|
|
|
|
else:
|
|
|
|
self.values[field] = 1
|
2021-07-09 17:47:06 +02:00
|
|
|
if field not in self.values:
|
2022-05-22 03:26:39 +02:00
|
|
|
if (descr.get("input_type", None) == "checkbox") and self.submitted():
|
|
|
|
# aucune case cochée
|
|
|
|
self.values[field] = []
|
|
|
|
else:
|
|
|
|
if "default" in descr: # first: default in form description
|
|
|
|
self.values[field] = descr["default"]
|
|
|
|
else: # then: use initvalues dict
|
|
|
|
self.values[field] = self.initvalues.get(field, "")
|
|
|
|
if self.values[field] is None:
|
|
|
|
self.values[field] = ""
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-08-09 10:25:45 +02:00
|
|
|
# convert numbers, except ids
|
|
|
|
if field.endswith("id") and self.values[field]:
|
|
|
|
# enforce integer ids:
|
2021-08-11 00:36:07 +02:00
|
|
|
try:
|
|
|
|
self.values[field] = int(self.values[field])
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2021-08-09 10:25:45 +02:00
|
|
|
elif isinstance(self.values[field], (int, float)):
|
2020-09-26 16:19:37 +02:00
|
|
|
self.values[field] = str(self.values[field])
|
|
|
|
#
|
2021-07-09 17:47:06 +02:00
|
|
|
if "tf-checked" not in self.values:
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.submitted():
|
|
|
|
# si rien n'est coché, tf-checked n'existe plus dans la reponse
|
|
|
|
self.values["tf-checked"] = []
|
|
|
|
else:
|
|
|
|
self.values["tf-checked"] = self.initvalues.get("tf-checked", [])
|
|
|
|
self.values["tf-checked"] = [str(x) for x in self.values["tf-checked"]]
|
|
|
|
|
|
|
|
def checkvalues(self):
|
|
|
|
"check values. Store .result and returns msg"
|
|
|
|
ok = 1
|
|
|
|
msg = []
|
|
|
|
for (field, descr) in self.formdescription:
|
|
|
|
val = self.values[field]
|
|
|
|
# do not check "unckecked" items
|
|
|
|
if descr.get("withcheckbox", False):
|
|
|
|
if not field in self.values["tf-checked"]:
|
|
|
|
continue
|
|
|
|
# null values
|
|
|
|
allow_null = descr.get("allow_null", True)
|
|
|
|
if not allow_null:
|
|
|
|
if val == "" or val == None:
|
|
|
|
msg.append(
|
|
|
|
"Le champ '%s' doit être renseigné" % descr.get("title", field)
|
|
|
|
)
|
|
|
|
ok = 0
|
2022-01-10 12:00:02 +01:00
|
|
|
elif val == "" or val == None:
|
|
|
|
continue # allowed empty field, skip
|
2020-09-26 16:19:37 +02:00
|
|
|
# type
|
|
|
|
typ = descr.get("type", "string")
|
2022-02-14 16:05:43 +01:00
|
|
|
if val != "" and val is not None:
|
2020-09-26 16:19:37 +02:00
|
|
|
# check only non-null values
|
|
|
|
if typ[:3] == "int":
|
|
|
|
try:
|
|
|
|
val = int(val)
|
|
|
|
self.values[field] = val
|
2022-02-14 16:05:43 +01:00
|
|
|
except ValueError:
|
2020-09-26 16:19:37 +02:00
|
|
|
msg.append(
|
|
|
|
"La valeur du champ '%s' doit être un nombre entier" % field
|
|
|
|
)
|
|
|
|
ok = 0
|
|
|
|
elif typ == "float" or typ == "real":
|
|
|
|
self.values[field] = self.values[field].replace(",", ".")
|
|
|
|
try:
|
|
|
|
val = float(val.replace(",", ".")) # allow ,
|
|
|
|
self.values[field] = val
|
2022-02-14 16:05:43 +01:00
|
|
|
except ValueError:
|
2020-09-26 16:19:37 +02:00
|
|
|
msg.append(
|
|
|
|
"La valeur du champ '%s' doit être un nombre" % field
|
|
|
|
)
|
|
|
|
ok = 0
|
2022-02-14 16:05:43 +01:00
|
|
|
if (
|
|
|
|
ok
|
|
|
|
and (typ[:3] == "int" or typ == "float" or typ == "real")
|
|
|
|
and val != ""
|
|
|
|
and val != None
|
|
|
|
):
|
|
|
|
if "min_value" in descr and self.values[field] < descr["min_value"]:
|
2020-09-26 16:19:37 +02:00
|
|
|
msg.append(
|
|
|
|
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
|
|
|
|
% (val, field, descr["min_value"])
|
|
|
|
)
|
|
|
|
ok = 0
|
2022-02-14 16:05:43 +01:00
|
|
|
if "max_value" in descr and self.values[field] > descr["max_value"]:
|
2020-09-26 16:19:37 +02:00
|
|
|
msg.append(
|
|
|
|
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
|
|
|
|
% (val, field, descr["max_value"])
|
|
|
|
)
|
|
|
|
ok = 0
|
2022-08-29 15:23:44 +02:00
|
|
|
if typ[:3] == "int":
|
|
|
|
if not (scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT):
|
|
|
|
msg.append(
|
|
|
|
f"Le champ '{field}' est a une valeur hors limite"
|
|
|
|
)
|
|
|
|
ok = 0
|
|
|
|
elif typ == "float" or typ == "real":
|
|
|
|
if not (
|
|
|
|
scu.DB_MIN_FLOAT <= self.values[field] <= scu.DB_MAX_FLOAT
|
|
|
|
):
|
|
|
|
msg.append(
|
|
|
|
f"Le champ '{field}' est a une valeur hors limite"
|
|
|
|
)
|
|
|
|
ok = 0
|
2022-03-18 19:55:46 +01:00
|
|
|
if ok and (typ[:3] == "str") and "max_length" in descr:
|
|
|
|
if len(self.values[field]) > descr["max_length"]:
|
|
|
|
msg.append(
|
|
|
|
"Le champ '%s' est trop long (max %d caractères)"
|
|
|
|
% (field, descr["max_length"])
|
|
|
|
)
|
|
|
|
ok = 0
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
# allowed values
|
2021-07-09 17:47:06 +02:00
|
|
|
if "allowed_values" in descr:
|
2020-09-26 16:19:37 +02:00
|
|
|
if descr.get("input_type", None) == "checkbox":
|
|
|
|
# for checkboxes, val is a list
|
|
|
|
for v in val:
|
|
|
|
if not v in descr["allowed_values"]:
|
|
|
|
msg.append(
|
|
|
|
"valeur invalide (%s) pour le champ '%s'" % (val, field)
|
|
|
|
)
|
|
|
|
ok = 0
|
|
|
|
elif descr.get("input_type", None) == "boolcheckbox":
|
|
|
|
pass
|
|
|
|
elif not val in descr["allowed_values"]:
|
|
|
|
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
|
|
|
|
ok = 0
|
2021-07-09 17:47:06 +02:00
|
|
|
if "validator" in descr:
|
2022-09-03 09:29:40 +02:00
|
|
|
try:
|
|
|
|
valid = descr["validator"](val, field)
|
|
|
|
except Exception:
|
|
|
|
valid = False
|
|
|
|
if not valid:
|
2020-09-26 16:19:37 +02:00
|
|
|
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
|
|
|
|
ok = 0
|
2022-01-10 12:00:02 +01:00
|
|
|
elif descr.get("input_type") == "datedmy":
|
|
|
|
if not DMY_REGEXP.match(val):
|
|
|
|
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
|
|
|
|
ok = 0
|
2020-09-26 16:19:37 +02:00
|
|
|
# boolean checkbox
|
|
|
|
if descr.get("input_type", None) == "boolcheckbox":
|
|
|
|
if int(val):
|
2021-08-11 00:36:07 +02:00
|
|
|
self.values[field] = True
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2021-08-11 00:36:07 +02:00
|
|
|
self.values[field] = False
|
2020-09-26 16:19:37 +02:00
|
|
|
if descr.get("convert_numbers", False):
|
|
|
|
if typ[:3] == "int":
|
2023-02-21 02:08:50 +01:00
|
|
|
try:
|
|
|
|
self.values[field] = int(self.values[field])
|
|
|
|
except ValueError:
|
|
|
|
msg.append(
|
|
|
|
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
|
|
|
)
|
|
|
|
ok = False
|
2020-09-26 16:19:37 +02:00
|
|
|
elif typ == "float" or typ == "real":
|
2023-02-21 02:08:50 +01:00
|
|
|
try:
|
|
|
|
self.values[field] = float(self.values[field].replace(",", "."))
|
|
|
|
except ValueError:
|
|
|
|
msg.append(
|
|
|
|
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
|
|
|
)
|
|
|
|
ok = False
|
2020-09-26 16:19:37 +02:00
|
|
|
if ok:
|
|
|
|
self.result = self.values
|
|
|
|
else:
|
|
|
|
self.result = None
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def _GenForm(self, method="", enctype=None, form_url=""):
|
|
|
|
values = self.values
|
|
|
|
add_no_enter_js = False # add JS function to prevent 'enter' -> submit
|
|
|
|
# form template
|
|
|
|
|
|
|
|
# default template for each input element
|
|
|
|
itemtemplate = """<tr%(item_dom_attr)s>
|
|
|
|
<td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td>
|
|
|
|
</tr>
|
|
|
|
"""
|
|
|
|
hiddenitemtemplate = "%(elem)s"
|
|
|
|
separatortemplate = '<tr%(item_dom_attr)s><td colspan="2">%(label)s</td></tr>'
|
|
|
|
# ---- build form
|
|
|
|
buttons_markup = ""
|
|
|
|
if self.submitbutton:
|
|
|
|
buttons_markup += (
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s>'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (
|
|
|
|
self.formid,
|
|
|
|
self.formid,
|
|
|
|
self.submitlabel,
|
|
|
|
" ".join(self.submitbuttonattributes),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if self.cancelbutton:
|
|
|
|
buttons_markup += (
|
2021-12-11 09:58:04 +01:00
|
|
|
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (self.formid, self.formid, self.cancelbutton)
|
|
|
|
)
|
|
|
|
|
|
|
|
R = []
|
|
|
|
suggest_js = []
|
|
|
|
if self.enctype is None:
|
|
|
|
if self.method == "post":
|
|
|
|
enctype = "multipart/form-data"
|
|
|
|
else:
|
|
|
|
enctype = "application/x-www-form-urlencoded"
|
|
|
|
if self.cssclass:
|
|
|
|
klass = ' class="%s"' % self.cssclass
|
|
|
|
else:
|
|
|
|
klass = ""
|
|
|
|
name = self.name
|
|
|
|
R.append(
|
2022-09-03 20:48:56 +02:00
|
|
|
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s %s>'
|
|
|
|
% (
|
|
|
|
self.form_url,
|
|
|
|
self.method,
|
|
|
|
self.formid,
|
|
|
|
enctype,
|
|
|
|
name,
|
|
|
|
klass,
|
|
|
|
self.form_attrs,
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2023-03-19 10:26:03 +01:00
|
|
|
if self.method == "post":
|
|
|
|
R.append(
|
|
|
|
f"""<input type="hidden" name="csrf_token" value="{
|
|
|
|
flask_wtf.csrf.generate_csrf()
|
|
|
|
}">"""
|
|
|
|
)
|
|
|
|
R.append(f"""<input type="hidden" name="{self.formid}_submitted" value="1">""")
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.top_buttons:
|
|
|
|
R.append(buttons_markup + "<p></p>")
|
2022-10-02 23:43:29 +02:00
|
|
|
R.append(self.before_table.format(title=self.title))
|
2020-09-26 16:19:37 +02:00
|
|
|
R.append('<table class="tf">')
|
2022-05-22 03:26:39 +02:00
|
|
|
for field, descr in self.formdescription:
|
2020-09-26 16:19:37 +02:00
|
|
|
if descr.get("readonly", False):
|
|
|
|
R.append(self._ReadOnlyElement(field, descr))
|
|
|
|
continue
|
|
|
|
wid = self.name + "_" + field
|
|
|
|
size = descr.get("size", 12)
|
|
|
|
rows = descr.get("rows", 5)
|
|
|
|
cols = descr.get("cols", 60)
|
|
|
|
title = descr.get("title", field.capitalize())
|
|
|
|
title_bubble = descr.get("title_bubble", None)
|
|
|
|
withcheckbox = descr.get("withcheckbox", False)
|
|
|
|
input_type = descr.get("input_type", "text")
|
|
|
|
item_dom_id = descr.get("dom_id", "")
|
|
|
|
if item_dom_id:
|
2022-05-22 03:26:39 +02:00
|
|
|
item_dom_attr = f' id="{item_dom_id}"'
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
|
|
|
item_dom_attr = ""
|
|
|
|
# choix du template
|
|
|
|
etempl = descr.get("template", None)
|
|
|
|
if etempl is None:
|
|
|
|
if input_type == "hidden":
|
|
|
|
etempl = hiddenitemtemplate
|
|
|
|
elif input_type == "separator":
|
|
|
|
etempl = separatortemplate
|
|
|
|
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
|
|
|
|
continue
|
2022-10-02 23:43:29 +02:00
|
|
|
elif input_type == "table_separator":
|
|
|
|
etempl = ""
|
|
|
|
# Table ouverte ?
|
|
|
|
if len([p for p in R if "<table" in p]) > len(
|
|
|
|
[p for p in R if "</table" in p]
|
|
|
|
):
|
|
|
|
R.append(f"""</table>{self.after_table}""")
|
|
|
|
R.append(
|
|
|
|
f"""{self.before_table.format(title=descr.get("title", ""))}<table class="tf">"""
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
|
|
|
etempl = itemtemplate
|
|
|
|
lab = []
|
|
|
|
lem = []
|
|
|
|
if withcheckbox and input_type != "hidden":
|
|
|
|
if field in values["tf-checked"]:
|
|
|
|
checked = 'checked="checked"'
|
|
|
|
else:
|
|
|
|
checked = ""
|
|
|
|
lab.append(
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s>'
|
2020-09-26 16:19:37 +02:00
|
|
|
% ("tf-checked", field, checked)
|
|
|
|
)
|
|
|
|
if title_bubble:
|
|
|
|
lab.append(
|
|
|
|
'<a class="discretelink" href="" title="%s">%s</a>'
|
|
|
|
% (title_bubble, title)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
lab.append(title)
|
|
|
|
#
|
|
|
|
attribs = " ".join(descr.get("attributes", []))
|
|
|
|
if (
|
|
|
|
withcheckbox and not checked
|
|
|
|
) or not descr.get( # desactive les element non coches:
|
|
|
|
"enabled", True
|
|
|
|
):
|
|
|
|
attribs += ' disabled="true"'
|
|
|
|
#
|
|
|
|
if input_type == "text":
|
|
|
|
lem.append(
|
|
|
|
'<input type="text" name="%s" size="%d" id="%s" %s'
|
|
|
|
% (field, size, wid, attribs)
|
|
|
|
)
|
|
|
|
if descr.get("return_focus_next", False): # and nextitemname:
|
|
|
|
# JS code to focus on next element on 'enter' key
|
|
|
|
# ceci ne marche que pour desactiver enter sous IE (pas Firefox)
|
|
|
|
# lem.append('''onKeyDown="if(event.keyCode==13){
|
|
|
|
# event.cancelBubble = true; event.returnValue = false;}"''')
|
|
|
|
lem.append('onkeypress="return enter_focus_next(this, event);"')
|
|
|
|
add_no_enter_js = True
|
|
|
|
# lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
|
|
|
|
# lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
|
2021-12-11 09:58:04 +01:00
|
|
|
lem.append(('value="%(' + field + ')s" >') % values)
|
2020-09-26 16:19:37 +02:00
|
|
|
elif input_type == "password":
|
|
|
|
lem.append(
|
|
|
|
'<input type="password" name="%s" id="%s" size="%d" %s'
|
|
|
|
% (field, wid, size, attribs)
|
|
|
|
)
|
2021-12-11 09:58:04 +01:00
|
|
|
lem.append(('value="%(' + field + ')s" >') % values)
|
2020-09-26 16:19:37 +02:00
|
|
|
elif input_type == "radio":
|
|
|
|
labels = descr.get("labels", descr["allowed_values"])
|
|
|
|
for i in range(len(labels)):
|
|
|
|
if descr["allowed_values"][i] == values[field]:
|
|
|
|
checked = 'checked="checked"'
|
|
|
|
else:
|
|
|
|
checked = ""
|
|
|
|
lem.append(
|
|
|
|
'<input type="radio" name="%s" value="%s" %s %s>%s</input>'
|
|
|
|
% (
|
|
|
|
field,
|
|
|
|
descr["allowed_values"][i],
|
|
|
|
checked,
|
|
|
|
attribs,
|
|
|
|
labels[i],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
elif input_type == "menu":
|
|
|
|
lem.append('<select name="%s" id="%s" %s>' % (field, wid, attribs))
|
|
|
|
labels = descr.get("labels", descr["allowed_values"])
|
|
|
|
for i in range(len(labels)):
|
|
|
|
if str(descr["allowed_values"][i]) == str(values[field]):
|
|
|
|
selected = "selected"
|
|
|
|
else:
|
|
|
|
selected = ""
|
|
|
|
lem.append(
|
|
|
|
'<option value="%s" %s>%s</option>'
|
|
|
|
% (descr["allowed_values"][i], selected, labels[i])
|
|
|
|
)
|
|
|
|
lem.append("</select>")
|
|
|
|
elif input_type == "checkbox" or input_type == "boolcheckbox":
|
|
|
|
if input_type == "checkbox":
|
|
|
|
labels = descr.get("labels", descr["allowed_values"])
|
|
|
|
else: # boolcheckbox
|
|
|
|
labels = [""]
|
|
|
|
descr["allowed_values"] = ["0", "1"]
|
|
|
|
vertical = descr.get("vertical", False)
|
|
|
|
disabled_items = descr.get("disabled_items", {})
|
|
|
|
if vertical:
|
|
|
|
lem.append("<table>")
|
2022-10-02 20:44:24 +02:00
|
|
|
for i in range(len(labels)): # pylint: disable=consider-using-enumerate
|
2020-09-26 16:19:37 +02:00
|
|
|
if input_type == "checkbox":
|
2021-08-15 09:55:35 +02:00
|
|
|
if (
|
|
|
|
values[field]
|
|
|
|
and descr["allowed_values"][i] in values[field]
|
|
|
|
):
|
2020-09-26 16:19:37 +02:00
|
|
|
checked = 'checked="checked"'
|
|
|
|
else:
|
|
|
|
checked = ""
|
|
|
|
else: # boolcheckbox
|
2021-08-11 00:36:07 +02:00
|
|
|
if values[field] == "True":
|
|
|
|
v = True
|
|
|
|
elif values[field] == "False":
|
|
|
|
v = False
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
v = int(values[field])
|
2022-10-02 20:44:24 +02:00
|
|
|
except (ValueError, KeyError):
|
2021-08-11 00:36:07 +02:00
|
|
|
v = False
|
2020-09-26 16:19:37 +02:00
|
|
|
if v:
|
|
|
|
checked = 'checked="checked"'
|
|
|
|
else:
|
|
|
|
checked = ""
|
|
|
|
if vertical:
|
|
|
|
lem.append("<tr><td>")
|
|
|
|
if disabled_items.get(i, False):
|
|
|
|
disab = 'disabled="1"'
|
|
|
|
ilab = (
|
|
|
|
'<span class="tf-label-disabled">'
|
|
|
|
+ labels[i]
|
|
|
|
+ "</span> <em>(non modifiable)</em>"
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
disab = ""
|
|
|
|
ilab = "<span>" + labels[i] + "</span>"
|
|
|
|
lem.append(
|
|
|
|
'<input type="checkbox" name="%s:list" value="%s" %s %s %s>%s</input>'
|
|
|
|
% (
|
|
|
|
field,
|
|
|
|
descr["allowed_values"][i],
|
|
|
|
attribs,
|
|
|
|
disab,
|
|
|
|
checked,
|
|
|
|
ilab,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if vertical:
|
|
|
|
lem.append("</tr></td>")
|
|
|
|
if vertical:
|
|
|
|
lem.append("</table>")
|
|
|
|
elif input_type == "textarea":
|
|
|
|
lem.append(
|
|
|
|
'<textarea name="%s" id="%s" rows="%d" cols="%d" %s>%s</textarea>'
|
|
|
|
% (field, wid, rows, cols, attribs, values[field])
|
|
|
|
)
|
|
|
|
elif input_type == "hidden":
|
|
|
|
if descr.get("type", "") == "list":
|
|
|
|
for v in values[field]:
|
|
|
|
lem.append(
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="hidden" name="%s:list" value="%s" %s >'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (field, v, attribs)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
lem.append(
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (field, wid, values[field], attribs)
|
|
|
|
)
|
2022-10-02 23:43:29 +02:00
|
|
|
elif (input_type == "separator") or (input_type == "table_separator"):
|
2020-09-26 16:19:37 +02:00
|
|
|
pass
|
|
|
|
elif input_type == "file":
|
|
|
|
lem.append(
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="file" name="%s" size="%s" value="%s" %s>'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (field, size, values[field], attribs)
|
|
|
|
)
|
2022-01-10 12:00:02 +01:00
|
|
|
elif (
|
|
|
|
input_type == "date" or input_type == "datedmy"
|
|
|
|
): # JavaScript widget for date input
|
2020-09-26 16:19:37 +02:00
|
|
|
lem.append(
|
2021-12-11 09:58:04 +01:00
|
|
|
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
2020-09-26 16:19:37 +02:00
|
|
|
% (field, values[field])
|
|
|
|
)
|
|
|
|
elif input_type == "text_suggest":
|
|
|
|
lem.append(
|
|
|
|
'<input type="text" name="%s" id="%s" size="%d" %s'
|
|
|
|
% (field, field, size, attribs)
|
|
|
|
)
|
2021-08-11 00:36:07 +02:00
|
|
|
lem.append(('value="%(' + field + ')s" />') % values)
|
2020-09-26 16:19:37 +02:00
|
|
|
suggest_js.append(
|
2021-08-09 14:29:03 +02:00
|
|
|
f"""var {field}_opts = {dict2js(descr.get("text_suggest_options", {}))};
|
|
|
|
var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|
|
|
"""
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2022-01-25 10:45:13 +01:00
|
|
|
elif input_type == "color":
|
|
|
|
lem.append(
|
|
|
|
'<input type="color" name="%s" id="%s" %s' % (field, field, attribs)
|
|
|
|
)
|
|
|
|
lem.append(('value="%(' + field + ')s" >') % values)
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2022-10-02 23:43:29 +02:00
|
|
|
raise ValueError(f"unkown input_type for form ({input_type})!")
|
2020-09-26 16:19:37 +02:00
|
|
|
explanation = descr.get("explanation", "")
|
|
|
|
if explanation:
|
2022-10-02 23:43:29 +02:00
|
|
|
lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
|
2020-09-26 16:19:37 +02:00
|
|
|
comment = descr.get("comment", "")
|
|
|
|
if comment:
|
2022-10-02 23:43:29 +02:00
|
|
|
if (input_type != "checkbox") and (input_type != "boolcheckbox"):
|
|
|
|
lem.append("<br>")
|
|
|
|
lem.append(f"""<span class="tf-comment">{comment}</span>""")
|
2020-09-26 16:19:37 +02:00
|
|
|
R.append(
|
|
|
|
etempl
|
|
|
|
% {
|
|
|
|
"label": "\n".join(lab),
|
|
|
|
"elem": "\n".join(lem),
|
|
|
|
"item_dom_attr": item_dom_attr,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
R.append("</table>")
|
2022-10-02 23:43:29 +02:00
|
|
|
R.append(self.after_table)
|
2020-09-26 16:19:37 +02:00
|
|
|
R.append(self.html_foot_markup)
|
|
|
|
|
|
|
|
if self.bottom_buttons:
|
2022-10-02 23:43:29 +02:00
|
|
|
R.append("<br>" + buttons_markup)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
if add_no_enter_js:
|
|
|
|
R.append(
|
|
|
|
"""<script type="text/javascript">
|
|
|
|
function enter_focus_next (elem, event) {
|
|
|
|
var cod = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
|
|
|
|
var enter = false;
|
|
|
|
if (event.keyCode == 13)
|
|
|
|
enter = true;
|
|
|
|
if (event.which == 13)
|
|
|
|
enter = true;
|
|
|
|
if (event.charCode == 13)
|
|
|
|
enter = true;
|
|
|
|
if (enter) {
|
|
|
|
var focused = false;
|
|
|
|
var i;
|
|
|
|
for (i = 0; i < elem.form.elements.length; i++)
|
|
|
|
if (elem == elem.form.elements[i])
|
|
|
|
break;
|
|
|
|
i = i + 1;
|
|
|
|
while (i < elem.form.elements.length) {
|
|
|
|
if ((elem.form.elements[i].type == "text")
|
|
|
|
&& (!(elem.form.elements[i].disabled))
|
|
|
|
&& ($(elem.form.elements[i]).is(':visible')))
|
|
|
|
{
|
|
|
|
elem.form.elements[i].focus();
|
|
|
|
focused = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i = i + 1;
|
|
|
|
}
|
|
|
|
if (!focused) {
|
|
|
|
elem.blur();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return true;
|
|
|
|
}</script>
|
|
|
|
"""
|
|
|
|
) # enter_focus_next, ne focus que les champs text
|
|
|
|
if suggest_js:
|
|
|
|
# nota: formid is currently ignored
|
|
|
|
# => only one form with text_suggest field on a page.
|
|
|
|
R.append(
|
|
|
|
"""<script type="text/javascript">
|
|
|
|
function init_tf_form(formid) {
|
|
|
|
%s
|
|
|
|
}
|
|
|
|
</script>"""
|
|
|
|
% "\n".join(suggest_js)
|
|
|
|
)
|
|
|
|
# Javascript common to all forms:
|
|
|
|
R.append(
|
|
|
|
"""<script type="text/javascript">
|
|
|
|
// controle par la checkbox
|
|
|
|
function tf_enable_elem(checkbox) {
|
|
|
|
var oid = checkbox.value;
|
|
|
|
if (oid) {
|
|
|
|
var elem = document.getElementById(oid);
|
|
|
|
if (elem) {
|
|
|
|
if (checkbox.checked) {
|
|
|
|
elem.disabled = false;
|
|
|
|
} else {
|
|
|
|
elem.disabled = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Selections etendues avec shift (use jquery.field)
|
|
|
|
$('input[name="tf-checked:list"]').createCheckboxRange();
|
|
|
|
</script>"""
|
|
|
|
)
|
|
|
|
R.append("</form>")
|
|
|
|
return R
|
|
|
|
|
|
|
|
def _ReadOnlyElement(self, field, descr):
|
|
|
|
"Generate HTML for an element, read-only"
|
|
|
|
R = []
|
|
|
|
title = descr.get("title", field.capitalize())
|
|
|
|
input_type = descr.get("input_type", "text")
|
|
|
|
klass = descr.get("cssclass", "")
|
|
|
|
klass = " " + klass
|
|
|
|
if input_type == "hidden":
|
|
|
|
return ""
|
|
|
|
|
|
|
|
R.append('<tr class="tf-ro-tr%s">' % klass)
|
|
|
|
|
|
|
|
if input_type == "separator": # separator
|
|
|
|
R.append('<td colspan="2">%s' % title)
|
2022-10-02 23:43:29 +02:00
|
|
|
elif input_type != "table_separator":
|
2020-09-26 16:19:37 +02:00
|
|
|
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
|
|
|
|
R.append("%s</td>" % title)
|
|
|
|
R.append('<td class="tf-ro-field%s">' % klass)
|
|
|
|
|
2022-08-26 14:05:25 +02:00
|
|
|
if input_type in ("text", "text_suggest", "color", "datedmy"):
|
2020-09-26 16:19:37 +02:00
|
|
|
R.append(("%(" + field + ")s") % self.values)
|
|
|
|
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
|
|
|
|
if input_type == "boolcheckbox":
|
|
|
|
labels = descr.get(
|
2023-02-28 22:55:15 +01:00
|
|
|
"labels", descr.get("allowed_values", ["non", "oui"])
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-08-11 11:53:20 +02:00
|
|
|
_val = self.values[field]
|
|
|
|
if isinstance(_val, bool):
|
|
|
|
bool_val = 1 if _val else 0
|
|
|
|
elif _val == "False":
|
|
|
|
bool_val = 0
|
|
|
|
elif _val:
|
|
|
|
bool_val = 1
|
|
|
|
else:
|
|
|
|
bool_val = 0
|
|
|
|
R.append(labels[bool_val])
|
|
|
|
if bool_val:
|
2021-12-11 09:58:04 +01:00
|
|
|
R.append('<input type="hidden" name="%s" value="1">' % field)
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
|
|
|
labels = descr.get("labels", descr["allowed_values"])
|
|
|
|
for i in range(len(labels)):
|
|
|
|
if str(descr["allowed_values"][i]) == str(self.values[field]):
|
|
|
|
R.append('<span class="tf-ro-value">%s</span>' % labels[i])
|
|
|
|
elif input_type == "textarea":
|
2021-09-15 22:31:16 +02:00
|
|
|
R.append(
|
|
|
|
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
|
|
|
|
)
|
2022-10-02 23:43:29 +02:00
|
|
|
elif (
|
|
|
|
input_type == "separator"
|
|
|
|
or input_type == "hidden"
|
|
|
|
or input_type == "table_separator"
|
|
|
|
):
|
2020-09-26 16:19:37 +02:00
|
|
|
pass
|
|
|
|
elif input_type == "file":
|
|
|
|
R.append("'%s'" % self.values[field])
|
|
|
|
else:
|
|
|
|
raise ValueError("unkown input_type for form (%s)!" % input_type)
|
|
|
|
|
|
|
|
explanation = descr.get("explanation", "")
|
|
|
|
if explanation:
|
|
|
|
R.append('<span class="tf-explanation">%s</span>' % explanation)
|
|
|
|
|
|
|
|
R.append("</td></tr>")
|
|
|
|
|
|
|
|
return "\n".join(R)
|
|
|
|
|
|
|
|
def _ReadOnlyVersion(self, formdescription):
|
|
|
|
"Generate HTML for read-only view of the form"
|
|
|
|
R = ['<table class="tf-ro">']
|
|
|
|
for (field, descr) in formdescription:
|
|
|
|
R.append(self._ReadOnlyElement(field, descr))
|
|
|
|
R.append("</table>")
|
|
|
|
return R
|
|
|
|
|
|
|
|
|
|
|
|
def dict2js(d):
|
|
|
|
"""convert Python dict to JS code"""
|
|
|
|
r = []
|
|
|
|
for k in d:
|
|
|
|
v = d[k]
|
2021-07-11 18:27:02 +02:00
|
|
|
if isinstance(v, bool):
|
2020-09-26 16:19:37 +02:00
|
|
|
if v:
|
|
|
|
v = "true"
|
|
|
|
else:
|
|
|
|
v = "false"
|
2021-07-11 18:18:44 +02:00
|
|
|
elif isinstance(v, str): # ne marchera pas en python2
|
2020-09-26 16:19:37 +02:00
|
|
|
v = '"' + v + '"'
|
|
|
|
|
|
|
|
r.append("%s: %s" % (k, v))
|
|
|
|
return "{" + ",\n".join(r) + "}"
|
|
|
|
|
|
|
|
|
|
|
|
def tf_error_message(msg):
|
|
|
|
"""html for form error message"""
|
|
|
|
if not msg:
|
|
|
|
return ""
|
2021-07-11 18:18:44 +02:00
|
|
|
if isinstance(msg, str):
|
2020-09-26 16:19:37 +02:00
|
|
|
msg = [msg]
|
|
|
|
return (
|
2021-08-07 08:42:57 +02:00
|
|
|
'<ul class="tf-msg"><li class="tf-msg error-message">%s</li></ul>'
|
|
|
|
% '</li><li class="tf-msg tf-msg error-message">'.join(msg)
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|