# -*- mode: python -*-
# -*- coding: utf-8 -*-


"""Simple form generator/validator

   E. Viennet 2005 - 2008

   v 1.3 (python3)
"""
import html
import re
import app.scodoc.sco_utils as scu

# 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})$"
)


def TrivialFormulator(
    form_url,
    values,
    formdescription=(),
    initvalues={},
    method="post",
    enctype=None,
    submitlabel="OK",
    name=None,
    formid="tf",
    form_attrs="",
    cssclass="",
    cancelbutton=None,
    submitbutton=True,
    submitbuttonattributes=[],
    top_buttons=False,  # place buttons at top of form
    bottom_buttons=True,  # buttons after form
    html_foot_markup="",
    readonly=False,
    is_submitted=False,
    title="",
    after_table="",
    before_table="{title}",
):
    """
    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,
                 a dictionnary with the requeted values.
    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.
          convert_numbers: convert int and float values (from string)
          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
          template: HTML template for element
          HTML elements:
             input_type : 'text', 'textarea', 'password',
                          'radio', 'menu', 'checkbox',
                          'hidden', 'separator', 'table_separator',
                          'file', 'date', 'datedmy' (avec validation),
                          'boolcheckbox', 'text_suggest',
                          'color'
                         (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,
        initvalues,
        method,
        enctype,
        submitlabel,
        name,
        formid,
        form_attrs=form_attrs,
        cssclass=cssclass,
        cancelbutton=cancelbutton,
        submitbutton=submitbutton,
        submitbuttonattributes=submitbuttonattributes,
        top_buttons=top_buttons,
        bottom_buttons=bottom_buttons,
        html_foot_markup=html_foot_markup,
        readonly=readonly,
        is_submitted=is_submitted,
        title=title,
        after_table=after_table,
        before_table=before_table,
    )
    form = t.getform()
    if t.canceled():
        res = -1
    elif t.submitted() and t.result:
        res = 1
    else:
        res = 0
    return res, form, t.result


class TF(object):
    def __init__(
        self,
        form_url,
        values,
        formdescription=[],
        initvalues={},
        method="POST",
        enctype=None,
        submitlabel="OK",
        name=None,
        formid="tf",
        form_attrs="",
        cssclass="",
        cancelbutton=None,
        submitbutton=True,
        submitbuttonattributes=[],
        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,
        title="",
        after_table="",
        before_table="{title}",
    ):
        self.form_url = form_url
        self.values = values.copy()
        self.formdescription = list(formdescription)
        self.initvalues = initvalues
        self.method = method
        self.enctype = enctype
        self.submitlabel = submitlabel
        if name:
            self.name = name
        else:
            self.name = formid  # 'tf'
        self.formid = formid
        self.form_attrs = form_attrs
        self.cssclass = cssclass
        self.cancelbutton = cancelbutton
        self.submitbutton = submitbutton
        self.submitbuttonattributes = submitbuttonattributes
        self.top_buttons = top_buttons
        self.bottom_buttons = bottom_buttons
        self.html_foot_markup = html_foot_markup
        self.title = title
        self.after_table = after_table
        self.before_table = before_table
        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
        return self.values.get("%s_submitted" % self.formid, False)

    def canceled(self):
        "true if form has been canceled"
        return self.values.get("%s_cancel" % self.formid, False)

    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():
                if field not in self.values:
                    self.values[field] = 0
                else:
                    self.values[field] = 1
            if field not in self.values:
                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] = ""

            # convert numbers, except ids
            if field.endswith("id") and self.values[field]:
                # enforce integer ids:
                try:
                    self.values[field] = int(self.values[field])
                except ValueError:
                    pass
            elif isinstance(self.values[field], (int, float)):
                self.values[field] = str(self.values[field])
        #
        if "tf-checked" not in self.values:
            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
            elif val == "" or val == None:
                continue  # allowed empty field, skip
            # type
            typ = descr.get("type", "string")
            if val != "" and val is not None:
                # check only non-null values
                if typ[:3] == "int":
                    try:
                        val = int(val)
                        self.values[field] = val
                    except ValueError:
                        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
                    except ValueError:
                        msg.append(
                            "La valeur du champ '%s' doit être un nombre" % field
                        )
                        ok = 0
                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"]:
                        msg.append(
                            "La valeur (%d) du champ '%s' est trop petite (min=%s)"
                            % (val, field, descr["min_value"])
                        )
                        ok = 0
                    if "max_value" in descr and self.values[field] > descr["max_value"]:
                        msg.append(
                            "La valeur (%s) du champ '%s' est trop grande (max=%s)"
                            % (val, field, descr["max_value"])
                        )
                        ok = 0
                    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
                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

            # allowed values
            if "allowed_values" in descr:
                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
            if "validator" in descr:
                try:
                    valid = descr["validator"](val, field)
                except Exception:
                    valid = False
                if not valid:
                    msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
                    ok = 0
            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
            # boolean checkbox
            if descr.get("input_type", None) == "boolcheckbox":
                if int(val):
                    self.values[field] = True
                else:
                    self.values[field] = False
            if descr.get("convert_numbers", False):
                if typ[:3] == "int":
                    try:
                        self.values[field] = int(self.values[field])
                    except ValueError:
                        msg.append(
                            f"valeur invalide ({self.values[field]}) pour le champs {field}"
                        )
                        ok = False
                elif typ == "float" or typ == "real":
                    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
        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 += (
                '<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s>'
                % (
                    self.formid,
                    self.formid,
                    self.submitlabel,
                    " ".join(self.submitbuttonattributes),
                )
            )
        if self.cancelbutton:
            buttons_markup += (
                ' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
                % (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(
            '<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,
            )
        )
        R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
        if self.top_buttons:
            R.append(buttons_markup + "<p></p>")
        R.append(self.before_table.format(title=self.title))
        R.append('<table class="tf">')
        for field, descr in self.formdescription:
            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:
                item_dom_attr = f' id="{item_dom_id}"'
            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
                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">"""
                    )
                else:
                    etempl = itemtemplate
            lab = []
            lem = []
            if withcheckbox and input_type != "hidden":
                if field in values["tf-checked"]:
                    checked = 'checked="checked"'
                else:
                    checked = ""
                lab.append(
                    '<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s>'
                    % ("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))
                lem.append(('value="%(' + field + ')s" >') % values)
            elif input_type == "password":
                lem.append(
                    '<input type="password" name="%s" id="%s" size="%d" %s'
                    % (field, wid, size, attribs)
                )
                lem.append(('value="%(' + field + ')s" >') % values)
            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>")
                for i in range(len(labels)):  # pylint: disable=consider-using-enumerate
                    if input_type == "checkbox":
                        if (
                            values[field]
                            and descr["allowed_values"][i] in values[field]
                        ):
                            checked = 'checked="checked"'
                        else:
                            checked = ""
                    else:  # boolcheckbox
                        if values[field] == "True":
                            v = True
                        elif values[field] == "False":
                            v = False
                        else:
                            try:
                                v = int(values[field])
                            except (ValueError, KeyError):
                                v = False
                        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(
                            '<input type="hidden" name="%s:list" value="%s" %s >'
                            % (field, v, attribs)
                        )
                else:
                    lem.append(
                        '<input type="hidden" name="%s" id="%s" value="%s" %s >'
                        % (field, wid, values[field], attribs)
                    )
            elif (input_type == "separator") or (input_type == "table_separator"):
                pass
            elif input_type == "file":
                lem.append(
                    '<input type="file" name="%s" size="%s" value="%s" %s>'
                    % (field, size, values[field], attribs)
                )
            elif (
                input_type == "date" or input_type == "datedmy"
            ):  # JavaScript widget for date input
                lem.append(
                    '<input type="text" name="%s" size="10" value="%s" class="datepicker">'
                    % (field, values[field])
                )
            elif input_type == "text_suggest":
                lem.append(
                    '<input type="text" name="%s" id="%s" size="%d" %s'
                    % (field, field, size, attribs)
                )
                lem.append(('value="%(' + field + ')s" />') % values)
                suggest_js.append(
                    f"""var {field}_opts = {dict2js(descr.get("text_suggest_options", {}))};
var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
"""
                )
            elif input_type == "color":
                lem.append(
                    '<input type="color" name="%s" id="%s" %s' % (field, field, attribs)
                )
                lem.append(('value="%(' + field + ')s" >') % values)
            else:
                raise ValueError(f"unkown input_type for form ({input_type})!")
            explanation = descr.get("explanation", "")
            if explanation:
                lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
            comment = descr.get("comment", "")
            if comment:
                if (input_type != "checkbox") and (input_type != "boolcheckbox"):
                    lem.append("<br>")
                lem.append(f"""<span class="tf-comment">{comment}</span>""")
            R.append(
                etempl
                % {
                    "label": "\n".join(lab),
                    "elem": "\n".join(lem),
                    "item_dom_attr": item_dom_attr,
                }
            )
        R.append("</table>")
        R.append(self.after_table)
        R.append(self.html_foot_markup)

        if self.bottom_buttons:
            R.append("<br>" + buttons_markup)

        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)
        elif input_type != "table_separator":
            R.append('<td class="tf-ro-fieldlabel%s">' % klass)
            R.append("%s</td>" % title)
            R.append('<td class="tf-ro-field%s">' % klass)

        if input_type in ("text", "text_suggest", "color", "datedmy"):
            R.append(("%(" + field + ")s") % self.values)
        elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
            if input_type == "boolcheckbox":
                labels = descr.get(
                    "labels", descr.get("allowed_values", ["non", "oui"])
                )
                _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:
                    R.append('<input type="hidden" name="%s" value="1">' % field)
            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":
            R.append(
                '<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
            )
        elif (
            input_type == "separator"
            or input_type == "hidden"
            or input_type == "table_separator"
        ):
            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]
        if isinstance(v, bool):
            if v:
                v = "true"
            else:
                v = "false"
        elif isinstance(v, str):  # ne marchera pas en python2
            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 ""
    if isinstance(msg, str):
        msg = [msg]
    return (
        '<ul class="tf-msg"><li class="tf-msg error-message">%s</li></ul>'
        % '</li><li class="tf-msg tf-msg error-message">'.join(msg)
    )