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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

""" Gestion des utilisateurs (table SQL pour Zope User Folder)
"""
import string
import re
import time
import md5
import base64
import jaxml

from sco_zope import *  # pylint: disable=unused-wildcard-import

# ---------------
import sco_utils as scu
import notesdb as ndb
from notes_log import log
from scolog import logdb
from scolars import format_prenom, format_nom
import sco_import_users
import sco_excel
from TrivialFormulator import TrivialFormulator, TF, tf_error_message
from gen_tables import GenTable
import scolars
import sco_cache
import sco_users
from sco_permissions import (
    ScoEditAllEvals,
    ScoEditAllNotes,
    ScoImplement,
    ScoSuperAdmin,
    ScoUsersAdmin,
    ScoUsersView,
    ScoView,
)
from sco_exceptions import (
    AccessDenied,
    ScoException,
    ScoValueError,
    ScoInvalidDateError,
    ScoLockedFormError,
    ScoGenError,
)

# ---------------
# cache global: chaque instance,  repérée par son URL, a un cache
# qui est recréé à la demande
# On cache ici la liste des utilisateurs, pour une duree limitee
# (une minute).

CACHE_userlist = {}

# ---------------


class ZScoUsers(
    ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
):

    "ZScousers object"

    meta_type = "ZScoUsers"
    security = ClassSecurityInfo()

    # This is the list of the methods associated to 'tabs' in the ZMI
    # Be aware that The first in the list is the one shown by default, so if
    # the 'View' tab is the first, you will never see your tabs by cliquing
    # on the object.
    manage_options = (
        ({"label": "Contents", "action": "manage_main"},)
        + PropertyManager.manage_options  # add the 'Properties' tab
        + ({"label": "View", "action": "index_html"},)
        + Item.manage_options  # add the 'Undo' & 'Owner' tab
        + RoleManager.manage_options  # add the 'Security' tab
    )

    # no permissions, only called from python
    def __init__(self, id, title):
        "initialise a new instance"
        self.id = id
        self.title = title

    # Connexion to SQL database of users:

    # Ugly but necessary during transition out of Zope:
    _db_cnx_string = "dbname=SCOUSERS port=5432"
    security.declareProtected("Change DTML Documents", "GetUsersDBConnexion")
    GetUsersDBConnexion = ndb.GetUsersDBConnexion

    # --------------------------------------------------------------------
    #
    #   Users (top level)
    #
    # --------------------------------------------------------------------
    # used to view content of the object
    security.declareProtected(ScoUsersView, "index_html")

    def index_html(self, REQUEST, all=0, with_olds=0, format="html"):
        "gestion utilisateurs..."
        all = int(all)
        with_olds = int(with_olds)
        # Controle d'acces
        authuser = REQUEST.AUTHENTICATED_USER
        user_name = str(authuser)
        # log('user: %s roles: %s'%(user_name,authuser.getRolesInContext(self)))
        user = self._user_list(args={"user_name": user_name})
        if not user:
            zope_roles = authuser.getRolesInContext(self)
            if ("Manager" in zope_roles) or ("manage" in zope_roles):
                dept = ""  # special case for zope admin
            else:
                raise AccessDenied("Vous n'avez pas la permission de voir cette page")
        else:
            dept = user[0]["dept"]

        H = [self.sco_header(REQUEST, page_title="Gestion des utilisateurs")]
        H.append("<h2>Gestion des utilisateurs</h2>")

        if authuser.has_permission(ScoUsersAdmin, self):
            H.append(
                '<p><a href="create_user_form" class="stdlink">Ajouter un utilisateur</a>'
            )
            H.append(
                '&nbsp;&nbsp; <a href="import_users_form" class="stdlink">Importer des utilisateurs</a></p>'
            )
        if all:
            checked = "checked"
        else:
            checked = ""
        if with_olds:
            olds_checked = "checked"
        else:
            olds_checked = ""
        H.append(
            """<p><form name="f" action="%s">
        <input type="checkbox" name="all" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
        <input type="checkbox" name="with_olds" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
        </form></p>"""
            % (REQUEST.URL0, checked, olds_checked)
        )

        L = self.list_users(
            dept,
            all=all,
            with_olds=with_olds,
            format=format,
            REQUEST=REQUEST,
            with_links=authuser.has_permission(ScoUsersAdmin, self),
        )
        if format != "html":
            return L
        H.append(L)

        F = self.sco_footer(REQUEST)
        return "\n".join(H) + F

    _userEditor = ndb.EditableTable(
        "sco_users",
        "user_id",
        (
            "user_id",
            "user_name",
            "passwd",
            "roles",
            "date_modif_passwd",
            "nom",
            "prenom",
            "email",
            "dept",
            "passwd_temp",
            "status",
            "date_expiration",
        ),
        output_formators={
            "date_modif_passwd": ndb.DateISOtoDMY,
            "date_expiration": ndb.DateISOtoDMY,
        },
        input_formators={
            "date_modif_passwd": ndb.DateDMYtoISO,
            "date_expiration": ndb.DateDMYtoISO,
        },
        sortkey="nom",
        filter_nulls=False,
    )

    def _user_list(self, **kw):
        # list info sur utilisateur(s)
        cnx = self.GetUsersDBConnexion()
        users = self._userEditor.list(cnx, **kw)
        for u in users:
            if u["status"] == "old":
                u["status_txt"] = "(ancien)"
            else:
                u["status_txt"] = ""

        return users

    def _user_edit(self, user_name, vals):
        # edit user
        cnx = self.GetUsersDBConnexion()
        vals["user_name"] = user_name
        self._userEditor.edit(cnx, vals)
        self.get_userlist_cache().inval_cache()  # >
        self.acl_users.cache_removeUser(user_name)  # exUserFolder's caches
        self.acl_users.xcache_removeUser(user_name)
        # Ensure that if status is "old", login is disabled
        # note that operation is reversible without having to re-enter a password
        # We change the roles (to avoid dealing with passwd hash, controled by exUserFolder)
        u = self._user_list(args={"user_name": user_name})[0]
        if u["status"] == "old" and u["roles"] and u["roles"][0] != "-":
            roles = ["-" + r for r in u["roles"].split(",")]
            cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
            self.acl_users.scodoc_editUser(cursor, user_name, roles=roles)
            self.get_userlist_cache().inval_cache()
        elif not u["status"] and u["roles"] and u["roles"][0] == "-":
            roles = [r[1:] for r in u["roles"].split(",") if (r and r[0] == "-")]
            cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
            self.acl_users.scodoc_editUser(cursor, user_name, roles=roles)
            self.get_userlist_cache().inval_cache()

    def _user_delete(self, user_name):
        # delete user
        cnx = self.GetUsersDBConnexion()
        user_id = self._user_list(args={"user_name": user_name})[0]["user_id"]
        self._userEditor.delete(cnx, user_id)
        self.get_userlist_cache().inval_cache()  # >

    def _all_roles(self):
        "ensemble de tous les roles attribués ou attribuables"
        roles = set(self.DeptUsersRoles())
        cnx = self.GetUsersDBConnexion()
        L = self._userEditor.list(cnx, {})
        for l in L:
            roles.update([x.strip() for x in l["roles"].split(",")])
        return [r for r in roles if r and r[0] != "-"]

    security.declareProtected(ScoUsersAdmin, "user_info")

    def user_info(self, user_name=None, user=None):
        """Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base).
        Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict.
        """
        if user_name:
            infos = self._user_list(args={"user_name": user_name})
        else:
            infos = [user.copy()]
            user_name = user["user_name"]

        if not infos:
            # special case: user is not in our database
            return {
                "user_name": user_name,
                "nom": user_name,
                "prenom": "",
                "email": "",
                "dept": "",
                "nomprenom": user_name,
                "prenomnom": user_name,
                "prenom_fmt": "",
                "nom_fmt": user_name,
                "nomcomplet": user_name,
                "nomplogin": user_name,
                "nomnoacc": scu.suppress_accents(user_name),
                "passwd_temp": 0,
                "status": "",
                "date_expiration": None,
            }
        else:
            info = infos[0]
            # always conceal password !
            del info["passwd"]  # always conceal password !
            #
            if info["prenom"]:
                p = format_prenom(info["prenom"])
            else:
                p = ""
            if info["nom"]:
                n = format_nom(
                    info["nom"], uppercase=False
                )  # strcapitalize(strlower(info['nom']))
            else:
                n = user_name

            prenom_abbrv = scu.abbrev_prenom(p)
            # nomprenom est le nom capitalisé suivi de l'initiale du prénom
            info["nomprenom"] = (n + " " + prenom_abbrv).strip()
            # prenomnom est l'initiale du prénom suivie du nom
            info["prenomnom"] = (prenom_abbrv + " " + n).strip()
            # nom_fmt et prenom_fmt: minuscule capitalisé
            info["nom_fmt"] = n
            info["prenom_fmt"] = scolars.format_prenom(p)
            # nomcomplet est le prenom et le nom complets
            info["nomcomplet"] = info["prenom_fmt"] + " " + info["nom_fmt"]
            # nomplogin est le nom en majuscules suivi du prénom et du login
            # e.g. Dupont Pierre (dupont)
            info["nomplogin"] = "%s %s (%s)" % (scu.strupper(n), p, info["user_name"])
            # nomnoacc est le nom en minuscules sans accents
            info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"]))

            return info

    def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False):
        """true if authuser can see or change passwd of user_name.
        If allow_admindepts, allow Admin from all depts (so they can view users from other depts
        and add roles to them).
        authuser is a Zope user object. user_name is a string.
        """
        # Is authuser a zope admin ?
        zope_roles = authuser.getRolesInContext(self)
        if ("Manager" in zope_roles) or ("manage" in zope_roles):
            return True
        # Anyone can change its own passwd (or see its informations)
        if str(authuser) == user_name:
            return True
        # has permission ?
        if not authuser.has_permission(ScoUsersAdmin, self):
            return False
        # Ok, now check that authuser can manage users from this departement
        # Get user info
        user = self._user_list(args={"user_name": user_name})
        if not user:
            return False  # we don't have infos on this user !
        # Get authuser info
        auth_name = str(authuser)
        authuser_info = self._user_list(args={"user_name": auth_name})
        if not authuser_info:
            return False  # not admin, and not in out database
        auth_dept = authuser_info[0]["dept"]
        if not auth_dept:
            return True  # if no dept, can access users from all depts !
        if auth_dept == user[0]["dept"] or allow_admindepts:
            return True
        else:
            return False

    def do_change_password(self, user_name, password):
        user = self._user_list(args={"user_name": user_name})
        assert len(user) == 1, "database inconsistency: len(user)=%d" % len(user)
        # should not occur, already tested in _can_handle_passwd
        cnx = self.GetUsersDBConnexion()  # en mode autocommit
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            "update sco_users set date_modif_passwd=now(), passwd_temp=0 where user_name=%(user_name)s",
            {"user_name": user_name},
        )

        # Laisse le exUserFolder modifier les donnees:
        self.acl_users.scodoc_editUser(
            cursor, user_name, password=password, roles=[user[0]["roles"]]
        )

        log("change_password: change ok for %s" % user_name)
        self.get_userlist_cache().inval_cache()  # >

    security.declareProtected(ScoView, "change_password")

    def change_password(self, user_name, password, password2, REQUEST):
        "change a password"
        # ScoUsersAdmin: modif tous les passwd de SON DEPARTEMENT
        # sauf si pas de dept (admin global)
        H = []
        F = self.sco_footer(REQUEST)
        # Check access permission
        if not self._can_handle_passwd(REQUEST.AUTHENTICATED_USER, user_name):
            # access denied
            log(
                "change_password: access denied (authuser=%s, user_name=%s, ip=%s)"
                % (REQUEST.AUTHENTICATED_USER, user_name, REQUEST.REMOTE_ADDR)
            )
            raise AccessDenied(
                "vous n'avez pas la permission de changer ce mot de passe"
            )
        # check password
        if password != password2:
            H.append(
                """<p>Les deux mots de passes saisis sont différents !</p>
            <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>"""
                % user_name
            )
        else:
            if not sco_users.is_valid_password(password):
                H.append(
                    """<p><b>ce mot de passe n\'est pas assez compliqué !</b><br/>(oui, il faut un mot de passe vraiment compliqué !)</p>
                <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>
                """
                    % user_name
                )
            else:
                # ok, strong password
                # MD5 hash (now computed by exUserFolder)
                # digest = md5.new()
                # digest.update(password)
                # digest = digest.digest()
                # md5pwd = string.strip(base64.encodestring(digest))
                #
                self.do_change_password(user_name, password)
                #
                # ici page simplifiee car on peut ne plus avoir
                # le droit d'acceder aux feuilles de style
                H.append(
                    "<h2>Changement effectué !</h2><p>Ne notez pas ce mot de passe, mais mémorisez le !</p><p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !</p><p><b>Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.</b></p>"
                )
                return (
                    """<?xml version="1.0" encoding="%s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Mot de passe changé</title>
<meta http-equiv="Content-Type" content="text/html; charset=%s" />
<body><h1>Mot de passe changé !</h1>
"""
                    % (scu.SCO_ENCODING, scu.SCO_ENCODING)
                    + "\n".join(H)
                    + '<a href="%s"  class="stdlink">Continuer</a></body></html>'
                    % self.ScoURL()
                )
        return self.sco_header(REQUEST) + "\n".join(H) + F

    security.declareProtected(ScoView, "form_change_password")

    def form_change_password(self, REQUEST, user_name=None):
        """Formulaire changement mot de passe
        Un utilisateur peut toujours changer son mot de passe"""
        authuser = REQUEST.AUTHENTICATED_USER
        if not user_name:
            user_name = str(authuser)
        H = [self.sco_header(REQUEST, user_check=False)]
        F = self.sco_footer(REQUEST)
        # check access
        if not self._can_handle_passwd(authuser, user_name):
            return (
                "\n".join(H)
                + "<p>Vous n'avez pas la permission de changer ce mot de passe</p>"
                + F
            )
        #
        H.append(
            """<h2>Changement du mot de passe de <font color="red">%(user_name)s</font></h2>
        <p>
        <form action="change_password" method="post" action="%(url)s"><table>
        <tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
        <tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
        </table>
        <input type="hidden" value="%(user_name)s" name="user_name">
        <input type="submit" value="Changer">
        </p>
        <p>Vous pouvez aussi: <a class="stdlink" href="reset_password_form?user_name=%(user_name)s">renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur</a>
"""
            % {"user_name": user_name, "url": REQUEST.URL0}
        )
        return "\n".join(H) + F

    security.declareProtected(ScoView, "userinfo")

    def userinfo(self, user_name=None, REQUEST=None):
        "display page of info about connected user"
        authuser = REQUEST.AUTHENTICATED_USER
        if not user_name:
            user_name = str(authuser)
        # peut on divulguer ces infos ?
        if not self._can_handle_passwd(
            REQUEST.AUTHENTICATED_USER, user_name, allow_admindepts=True
        ):
            raise AccessDenied("Vous n'avez pas la permission de voir cette page")
        H = [self.sco_header(REQUEST, page_title="Utilisateur %s" % user_name)]
        F = self.sco_footer(REQUEST)
        H.append("<h2>Utilisateur: %s" % user_name)
        info = self._user_list(args={"user_name": user_name})
        if info:
            H.append("%(status_txt)s" % info[0])
        H.append("</h2>")
        if not info:
            H.append(
                "<p>L' utilisateur '%s' n'est pas défini dans ce module.</p>"
                % user_name
            )
            if authuser.has_permission(ScoEditAllNotes, self):
                H.append("<p>(il peut modifier toutes les notes)</p>")
            if authuser.has_permission(ScoEditAllEvals, self):
                H.append("<p>(il peut modifier toutes les évaluations)</p>")
            if authuser.has_permission(ScoImplement, self):
                H.append("<p>(il peut creer des formations)</p>")
        else:
            H.append(
                """<p>
            <b>Login :</b> %(user_name)s<br/>
            <b>Nom :</b> %(nom)s<br/>
            <b>Prénom :</b> %(prenom)s<br/>
            <b>Mail :</b> %(email)s<br/>
            <b>Roles :</b> %(roles)s<br/>
            <b>Dept :</b> %(dept)s<br/>
            <b>Dernière modif mot de passe:</b> %(date_modif_passwd)s<br/>
            <b>Date d'expiration:</b> %(date_expiration)s
            <p><ul>
             <li><a class="stdlink" href="form_change_password?user_name=%(user_name)s">changer le mot de passe</a></li>"""
                % info[0]
            )
            if authuser.has_permission(ScoUsersAdmin, self):
                H.append(
                    """
             <li><a  class="stdlink" href="create_user_form?user_name=%(user_name)s&amp;edit=1">modifier/déactiver ce compte</a></li>
             <li><a  class="stdlink" href="delete_user_form?user_name=%(user_name)s">supprimer cet utilisateur</a> <em>(à n'utiliser qu'en cas d'erreur !)</em></li>
             """
                    % info[0]
                )

            H.append("</ul>")

            if str(authuser) == user_name:
                H.append(
                    '<p><b>Se déconnecter: <a class="stdlink" href="acl_users/logout">logout</a></b></p>'
                )
            # Liste des permissions
            H.append(
                '<div class="permissions"><p>Permission de cet utilisateur:</p><ul>'
            )
            permissions = self.ac_inherited_permissions(1)
            scoperms = [p for p in permissions if p[0][:3] == "Sco"]
            try:
                thisuser = self.acl_users.getUser(user_name)
            except:
                # expired from cache ? retry...
                thisuser = self.acl_users.getUser(user_name)
            if not thisuser:
                # Cas de figure incompris ? (bug IUT Amiens janvier 2014: login avec accent ?)
                H.append(
                    "<li><em>impossible de retrouver les permissions de l'utilisateur (contacter l'administrateur)</em></li>"
                )
            else:
                for p in scoperms:
                    permname, _ = p[:2]
                    if thisuser.has_permission(permname, self):
                        b = "oui"
                    else:
                        b = "non"
                    H.append("<li>%s : %s</li>" % (permname, b))
            H.append("</ul></div>")
        if authuser.has_permission(ScoUsersAdmin, self):
            H.append(
                '<p><a class="stdlink" href="%s/Users">Liste de tous les utilisateurs</a></p>'
                % self.ScoURL()
            )
        return "\n".join(H) + F

    security.declareProtected(ScoUsersAdmin, "create_user_form")

    def create_user_form(self, REQUEST, user_name=None, edit=0):
        "form. creation ou edit utilisateur"
        # Get authuser info
        authuser = REQUEST.AUTHENTICATED_USER
        auth_name = str(authuser)
        authuser_info = self._user_list(args={"user_name": auth_name})
        initvalues = {}

        # Access control
        zope_roles = authuser.getRolesInContext(self)
        if (
            not authuser_info
            and not ("Manager" in zope_roles)
            and not ("manage" in zope_roles)
        ):
            # not admin, and not in database
            raise AccessDenied("invalid user (%s)" % auth_name)
        if authuser_info:
            auth_dept = authuser_info[0]["dept"]
        else:
            auth_dept = ""
        #
        edit = int(edit)
        H = [self.sco_header(REQUEST, bodyOnLoad="init_tf_form('')")]
        F = self.sco_footer(REQUEST)
        if edit:
            if not user_name:
                raise ValueError("missing argument: user_name")
            initvalues = self._user_list(args={"user_name": user_name})[0]
            H.append("<h2>Modification de l'utilisateur %s</h2>" % user_name)
        else:
            H.append("<h2>Création d'un utilisateur</h2>")

        if authuser.has_permission(ScoSuperAdmin, self):
            H.append("""<p class="warning">Vous êtes super administrateur !</p>""")

        # Noms de roles pouvant etre attribues aux utilisateurs via ce dialogue
        # si pas SuperAdmin, restreint aux rôles EnsX, SecrX, DeptX
        #
        if authuser.has_permission(ScoSuperAdmin, self):
            log("create_user_form called by %s (super admin)" % (auth_name,))
            editable_roles = set(self._all_roles())
        else:
            editable_roles = set(self.DeptUsersRoles())
        # log('create_user_form: editable_roles=%s' % editable_roles)
        #
        if not edit:
            submitlabel = "Créer utilisateur"
            orig_roles = set()
        else:
            submitlabel = "Modifier utilisateur"
            initvalues["roles"] = initvalues["roles"].split(",") or []
            orig_roles = set(initvalues["roles"])
            if initvalues["status"] == "old":
                editable_roles = set()  # can't change roles of a disabled user
        # add existing user roles
        displayed_roles = list(editable_roles.union(orig_roles))
        displayed_roles.sort()
        disabled_roles = {}  # pour desactiver les roles que l'on ne peut pas editer
        for i in range(len(displayed_roles)):
            if displayed_roles[i] not in editable_roles:
                disabled_roles[i] = True

        # log('create_user_form: displayed_roles=%s' % displayed_roles)

        descr = [
            ("edit", {"input_type": "hidden", "default": edit}),
            ("nom", {"title": "Nom", "size": 20, "allow_null": False}),
            ("prenom", {"title": "Prénom", "size": 20, "allow_null": False}),
        ]
        if auth_name != user_name:  # no one can't change its own status
            descr.append(
                (
                    "status",
                    {
                        "title": "Statut",
                        "input_type": "radio",
                        "labels": ("actif", "ancien"),
                        "allowed_values": ("", "old"),
                    },
                )
            )
        if not edit:
            descr += [
                (
                    "user_name",
                    {
                        "title": "Pseudo (login)",
                        "size": 20,
                        "allow_null": False,
                        "explanation": "nom utilisé pour la connexion. Doit être unique parmi tous les utilisateurs.",
                    },
                ),
                (
                    "passwd",
                    {
                        "title": "Mot de passe",
                        "input_type": "password",
                        "size": 14,
                        "allow_null": False,
                    },
                ),
                (
                    "passwd2",
                    {
                        "title": "Confirmer mot de passe",
                        "input_type": "password",
                        "size": 14,
                        "allow_null": False,
                    },
                ),
            ]
        else:
            descr += [
                (
                    "user_name",
                    {"input_type": "hidden", "default": initvalues["user_name"]},
                ),
                ("user_id", {"input_type": "hidden", "default": initvalues["user_id"]}),
            ]
        descr += [
            (
                "email",
                {
                    "title": "e-mail",
                    "input_type": "text",
                    "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur",
                    "size": 20,
                    "allow_null": True,
                },
            )
        ]

        if not auth_dept:
            # si auth n'a pas de departement (admin global)
            # propose de choisir le dept du nouvel utilisateur
            # sinon, il sera créé dans le même département que auth
            descr.append(
                (
                    "dept",
                    {
                        "title": "Département",
                        "input_type": "text",
                        "size": 12,
                        "allow_null": True,
                        "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""",
                    },
                )
            )
            can_choose_dept = True
        else:
            can_choose_dept = False
            if edit:
                descr.append(
                    (
                        "d",
                        {
                            "input_type": "separator",
                            "title": "L'utilisateur appartient au département %s"
                            % auth_dept,
                        },
                    )
                )
            else:
                descr.append(
                    (
                        "d",
                        {
                            "input_type": "separator",
                            "title": "L'utilisateur  sera crée dans le département %s"
                            % auth_dept,
                        },
                    )
                )

        descr += [
            (
                "date_expiration",
                {
                    "title": "Date d'expiration",  # j/m/a
                    "input_type": "date",
                    "explanation": "j/m/a, laisser vide si pas de limite",
                    "size": 9,
                    "allow_null": True,
                },
            ),
            (
                "roles",
                {
                    "title": "Rôles",
                    "input_type": "checkbox",
                    "vertical": True,
                    "allowed_values": displayed_roles,
                    "disabled_items": disabled_roles,
                },
            ),
            (
                "force",
                {
                    "title": "Ignorer les avertissements",
                    "input_type": "checkbox",
                    "explanation": "passer outre les avertissements (homonymes, etc)",
                    "labels": ("",),
                    "allowed_values": ("1",),
                },
            ),
        ]

        if "tf-submitted" in REQUEST.form and not "roles" in REQUEST.form:
            REQUEST.form["roles"] = []
        if "tf-submitted" in REQUEST.form:
            # Ajoute roles existants mais non modifiables (disabled dans le form)
            # orig_roles - editable_roles
            REQUEST.form["roles"] = list(
                set(REQUEST.form["roles"]).union(orig_roles - editable_roles)
            )

        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            descr,
            initvalues=initvalues,
            submitlabel=submitlabel,
            cancelbutton="Annuler",
        )
        if tf[0] == 0:
            return "\n".join(H) + "\n" + tf[1] + F
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(self.UsersURL())
        else:
            vals = tf[2]
            roles = set(vals["roles"]).intersection(editable_roles)
            if REQUEST.form.has_key("edit"):
                edit = int(REQUEST.form["edit"])
            else:
                edit = 0
            try:
                force = int(vals["force"][0])
            except:
                force = 0

            if edit:
                user_name = initvalues["user_name"]
            else:
                user_name = vals["user_name"]
            # ce login existe ?
            err = None
            users = self._user_list(args={"user_name": user_name})
            if edit and not users:  # safety net, le user_name ne devrait pas changer
                err = "identifiant %s inexistant" % user_name
            if not edit and users:
                err = "identifiant %s déjà utilisé" % user_name
            if err:
                H.append(tf_error_message("""Erreur: %s""" % err))
                return "\n".join(H) + "\n" + tf[1] + F

            if not force:
                ok, msg = self._check_modif_user(
                    edit,
                    user_name=user_name,
                    nom=vals["nom"],
                    prenom=vals["prenom"],
                    email=vals["email"],
                    roles=vals["roles"],
                )
                if not ok:
                    H.append(
                        tf_error_message(
                            """Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
                            % msg
                        )
                    )

                    return "\n".join(H) + "\n" + tf[1] + F

            if edit:  # modif utilisateur (mais pas passwd)
                if (not can_choose_dept) and vals.has_key("dept"):
                    del vals["dept"]
                if vals.has_key("passwd"):
                    del vals["passwd"]
                if vals.has_key("date_modif_passwd"):
                    del vals["date_modif_passwd"]
                if vals.has_key("user_name"):
                    del vals["user_name"]
                if (auth_name == user_name) and vals.has_key("status"):
                    del vals["status"]  # no one can't change its own status

                # traitement des roles: ne doit pas affecter les roles
                # que l'on en controle pas:
                for role in orig_roles:
                    if role and not role in editable_roles:
                        roles.add(role)

                vals["roles"] = ",".join(roles)

                # ok, edit
                log("sco_users: editing %s by %s" % (user_name, auth_name))
                # log('sco_users: previous_values=%s' % initvalues)
                # log('sco_users: new_values=%s' % vals)
                self._user_edit(user_name, vals)
                return REQUEST.RESPONSE.redirect(
                    "userinfo?user_name=%s&head_message=Utilisateur %s modifié"
                    % (user_name, user_name)
                )
            else:  # creation utilisateur
                vals["roles"] = ",".join(vals["roles"])
                # check identifiant
                if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]):
                    msg = tf_error_message(
                        "identifiant invalide (pas d'accents ni de caractères spéciaux)"
                    )
                    return "\n".join(H) + msg + "\n" + tf[1] + F
                # check passwords
                if vals["passwd"] != vals["passwd2"]:
                    msg = tf_error_message(
                        """Les deux mots de passes ne correspondent pas !"""
                    )
                    return "\n".join(H) + msg + "\n" + tf[1] + F
                if not sco_users.is_valid_password(vals["passwd"]):
                    msg = tf_error_message(
                        """Mot de passe trop simple, recommencez !"""
                    )
                    return "\n".join(H) + msg + "\n" + tf[1] + F
                if not can_choose_dept:
                    vals["dept"] = auth_dept
                # ok, go
                log("sco_users: new_user %s by %s" % (vals["user_name"], auth_name))
                self.create_user(vals, REQUEST=REQUEST)

    def _check_modif_user(
        self, edit, user_name="", nom="", prenom="", email="", roles=[]
    ):
        """Vérifie que et utilisateur peut etre crée (edit=0) ou modifié (edit=1)
        Cherche homonymes.
        returns (ok, msg)
          - ok : si vrai, peut continuer avec ces parametres
              (si ok est faux, l'utilisateur peut quand même forcer la creation)
          - msg: message warning a presenter l'utilisateur
        """
        if not user_name or not nom or not prenom:
            return False, "champ requis vide"
        if not email:
            return False, "vous devriez indiquer le mail de l'utilisateur créé !"
        # ce login existe ?
        users = self._user_list(args={"user_name": user_name})
        if edit and not users:  # safety net, le user_name ne devrait pas changer
            return False, "identifiant %s inexistant" % user_name
        if not edit and users:
            return False, "identifiant %s déjà utilisé" % user_name

        # Des noms/prénoms semblables existent ?
        cnx = self.GetUsersDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            "select * from sco_users where lower(nom) ~ %(nom)s and lower(prenom) ~ %(prenom)s;",
            {"nom": nom.lower().strip(), "prenom": prenom.lower().strip()},
        )
        res = cursor.dictfetchall()
        if edit:
            minmatch = 1
        else:
            minmatch = 0
        if len(res) > minmatch:
            return (
                False,
                "des utilisateurs proches existent: "
                + ", ".join(
                    [
                        "%s %s (pseudo=%s)" % (x["prenom"], x["nom"], x["user_name"])
                        for x in res
                    ]
                ),
            )
        # Roles ?
        if not roles and not (edit and users[0]["status"] == "old"):
            # nb: si utilisateur desactivé (old), pas de role attribué
            return False, "aucun rôle sélectionné, êtes vous sûr ?"
        # ok
        return True, ""

    security.declareProtected(ScoUsersAdmin, "import_users_form")

    def import_users_form(self, REQUEST, user_name=None, edit=0):
        """Import utilisateurs depuis feuille Excel"""
        head = self.sco_header(REQUEST, page_title="Import utilisateurs")
        H = [
            head,
            """<h2>Téléchargement d'une nouvelle liste d'utilisateurs</h2>
             <p style="color: red">A utiliser pour importer de <b>nouveaux</b> utilisateurs (enseignants ou secrétaires)
             </p>
             <p>
             L'opération se déroule en deux étapes. Dans un premier temps,
             vous téléchargez une feuille Excel type. Vous devez remplir
             cette feuille, une ligne décrivant chaque utilisateur. Ensuite,
             vous indiquez le nom de votre fichier dans la case "Fichier Excel"
             ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
             votre liste.
             </p>
             """,
        ]
        help = """<p class="help">
        Lors de la creation des utilisateurs, les opérations suivantes sont effectuées:
        </p>
        <ol class="help">
        <li>vérification des données;</li>
        <li>génération d'un mot de passe alétoire pour chaque utilisateur;</li>
        <li>création de chaque utilisateur;</li>
        <li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
        </ol>"""
        H.append(
            """<ol><li><a class="stdlink" href="import_users_generate_excel_sample">
        Obtenir la feuille excel à remplir</a></li><li>"""
        )
        F = self.sco_footer(REQUEST)
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            (
                (
                    "xlsfile",
                    {"title": "Fichier Excel:", "input_type": "file", "size": 40},
                ),
                ("formsemestre_id", {"input_type": "hidden"}),
            ),
            submitlabel="Télécharger",
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + "</li></ol>" + help + F
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(self.UsersURL())
        else:
            # IMPORT
            diag = sco_import_users.import_excel_file(
                tf[2]["xlsfile"], REQUEST=REQUEST, context=self
            )
            H = [head]
            H.append("<p>Import excel: %s</p>" % diag)
            H.append("<p>OK, import terminé !</p>")
            H.append(
                '<p><a class="stdlink" href="%s">Continuer</a></p>' % self.UsersURL()
            )
            return "\n".join(H) + help + F

    security.declareProtected(ScoUsersAdmin, "import_users_generate_excel_sample")

    def import_users_generate_excel_sample(self, REQUEST):
        "une feuille excel pour importation utilisateurs"
        data = sco_import_users.generate_excel_sample()
        return sco_excel.sendExcelFile(REQUEST, data, "ImportUtilisateurs.xls")

    security.declareProtected(ScoUsersAdmin, "create_user")

    def create_user(self, args, REQUEST=None):
        "creation utilisateur zope"
        cnx = self.GetUsersDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        passwd = args["passwd"]
        args["passwd"] = "undefined"
        if "passwd2" in args:
            del args["passwd2"]
        log("create_user: args=%s" % args)  # log apres supr. du mot de passe !
        _ = self._userEditor.create(cnx, args)
        self.get_userlist_cache().inval_cache()  # >

        # call exUserFolder to set passwd
        roles = args["roles"].split(",")
        self.acl_users.scodoc_editUser(
            cursor, args["user_name"], password=passwd, roles=roles
        )

        if REQUEST:
            return REQUEST.RESPONSE.redirect(self.UsersURL())

    security.declareProtected(ScoUsersAdmin, "delete_user_form")

    def delete_user_form(self, REQUEST, user_name, dialog_confirmed=False):
        "delete user"
        authuser = REQUEST.AUTHENTICATED_USER
        if not self._can_handle_passwd(authuser, user_name):
            return (
                self.sco_header(REQUEST, user_check=False)
                + "<p>Vous n'avez pas la permission de supprimer cet utilisateur</p>"
                + self.sco_footer(REQUEST)
            )

        r = self._user_list(args={"user_name": user_name})
        if len(r) != 1:
            return ScoValueError("utilisateur %s inexistant" % user_name)
        if not dialog_confirmed:
            return self.confirmDialog(
                """<h2>Confirmer la suppression de l\'utilisateur %s ?</h2>
                <p class="warning">En général, il est déconseillé de supprimer un utilisateur, son
                identité étant référencé dans les modules de formation. N'utilisez
                cette fonction qu'en cas d'erreur (création de doublons, etc).
                </p>
                """
                % user_name,
                dest_url="",
                REQUEST=REQUEST,
                cancel_url=self.UsersURL(),
                parameters={"user_name": user_name},
            )
        self._user_delete(user_name)
        return REQUEST.RESPONSE.redirect(
            self.UsersURL() + r"?head_message=Utilisateur%20supprimé"
        )

    security.declareProtected(ScoView, "list_users")

    def list_users(
        self,
        dept,
        all=False,  # tous les departements
        with_olds=False,  # inclue les anciens utilisateurs (status "old")
        format="html",
        with_links=True,
        REQUEST=None,
    ):
        "List users"
        authuser = REQUEST.AUTHENTICATED_USER
        if dept and not all:
            r = self.get_userlist(dept=dept, with_olds=with_olds)
            comm = "dept. %s" % dept
        else:
            r = self.get_userlist(with_olds=with_olds)
            comm = "tous"
        if with_olds:
            comm += ", avec anciens"
        comm = "(" + comm + ")"
        # -- Add some information and links:
        for u in r:
            # Can current user modify this user ?
            can_modify = self._can_handle_passwd(
                authuser, u["user_name"], allow_admindepts=True
            )

            # Add links
            if with_links and can_modify:
                target = "userinfo?user_name=%(user_name)s" % u
                u["_user_name_target"] = target
                u["_nom_target"] = target
                u["_prenom_target"] = target

            # Hide passwd modification date (depending on rights wrt user)
            if not can_modify:
                u["date_modif_passwd"] = "(non visible)"

            # Add spaces between roles to ease line wrap
            if u["roles"]:
                u["roles"] = ", ".join(u["roles"].split(","))

            # Convert dates to ISO if XML output
            if format == "xml" and u["date_modif_passwd"] != "NA":
                u["date_modif_passwd"] = ndb.DateDMYtoISO(u["date_modif_passwd"]) or ""

            # Convert date_expiration and date_modif_passwd to ISO to ease sorting
            if u["date_expiration"]:
                u["date_expiration_iso"] = ndb.DateDMYtoISO(u["date_expiration"])
            else:
                u["date_expiration_iso"] = ""
            if u["date_modif_passwd"]:
                u["date_modif_passwd_iso"] = ndb.DateDMYtoISO(u["date_expiration"])
            else:
                u["date_modif_passwd_iso"] = ""

        title = "Utilisateurs définis dans ScoDoc"
        tab = GenTable(
            rows=r,
            columns_ids=(
                "user_name",
                "nom_fmt",
                "prenom_fmt",
                "email",
                "dept",
                "roles",
                "date_expiration_iso",
                "date_modif_passwd_iso",
                "passwd_temp",
                "status_txt",
            ),
            titles={
                "user_name": "Login",
                "nom_fmt": "Nom",
                "prenom_fmt": "Prénom",
                "email": "Mail",
                "dept": "Dept.",
                "roles": "Rôles",
                "date_expiration_iso": "Expiration",
                "date_modif_passwd_iso": "Modif. mot de passe",
                "passwd_temp": "Temp.",
                "status_txt": "Etat",
            },
            caption=title,
            page_title="title",
            html_title="""<h2>%d utilisateurs %s</h2>
            <p class="help">Cliquer sur un nom pour changer son mot de passe</p>"""
            % (len(r), comm),
            html_class="table_leftalign list_users",
            html_with_td_classes=True,
            html_sortable=True,
            base_url="%s?all=%s" % (REQUEST.URL0, all),
            pdf_link=False,  # table is too wide to fit in a paper page => disable pdf
            preferences=self.get_preferences(),
        )

        return tab.make_page(
            self, format=format, with_html_headers=False, REQUEST=REQUEST
        )

    def get_userlist_cache(self):
        url = self.ScoURL()
        if CACHE_userlist.has_key(url):
            return CACHE_userlist[url]
        else:
            log("get_userlist_cache: new simpleCache")
            CACHE_userlist[url] = sco_cache.expiringCache(max_validity=60)
            return CACHE_userlist[url]

    security.declareProtected(ScoView, "get_userlist")

    def get_userlist(self, dept=None, with_olds=False):
        """Returns list of users.
        If dept, select users from this dept,
        else return all users.
        """
        # on ne cache que la liste sans les "olds"
        if with_olds:
            r = None
        else:
            cache = self.get_userlist_cache()
            r = cache.get(dept)

        if r != None:
            return r
        else:
            args = {}
            if not with_olds:
                args["status"] = None
            if dept != None:
                args["dept"] = dept

            r = self._user_list(args=args)

            l = [self.user_info(user=user) for user in r]

            if not with_olds:
                cache.set(dept, l)
            return l

    security.declareProtected(ScoView, "get_userlist_xml")

    def get_userlist_xml(self, dept=None, start="", limit=25, REQUEST=None):
        """Returns XML list of users with name (nomplogin) starting with start.
        Used for forms auto-completion."""
        userlist = self.get_userlist(dept=dept)
        start = scu.suppression_diacritics(unicode(start, "utf-8"))
        start = scu.strlower(str(start))

        userlist = [user for user in userlist if user["nomnoacc"].startswith(start)]
        if REQUEST:
            REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
        doc = jaxml.XML_document(encoding=scu.SCO_ENCODING)
        doc.results()
        for user in userlist[:limit]:
            doc._push()
            doc.rs(user["nomplogin"], id=user["user_id"], info="")
            doc._pop()
        return repr(doc)

    security.declareProtected(ScoView, "get_user_name_from_nomplogin")

    def get_user_name_from_nomplogin(self, nomplogin):
        """Returns user_name (login) from nomplogin"""
        m = re.match(r".*\((.*)\)", nomplogin.strip())
        if m:
            return m.group(1)
        else:
            return None

    security.declareProtected(ScoView, "reset_password_form")

    def reset_password_form(self, user_name=None, dialog_confirmed=False, REQUEST=None):
        """Form to reset a password"""
        if not dialog_confirmed:
            return self.confirmDialog(
                """<h2>Ré-initialiser le mot de passe de %s ?</h2>
<p>Le mot de passe de %s va être choisi au hasard et lui être envoyé par mail.
Il devra ensuite se connecter et le changer.
</p>
                """
                % (user_name, user_name),
                parameters={"user_name": user_name},
                REQUEST=REQUEST,
            )
        self.reset_password(user_name=user_name, REQUEST=REQUEST)
        return REQUEST.RESPONSE.redirect(
            self.UsersURL()
            + r"?head_message=mot%20de%20passe%20de%20"
            + user_name
            + "%20reinitialise"
        )

    security.declareProtected(ScoView, "reset_password")

    def reset_password(self, user_name=None, REQUEST=None):
        """Reset a password:
        - set user's passwd_temp to 1
        - set roles to 'ScoReset'
        - generate a random password and mail it
        """
        authuser = REQUEST.AUTHENTICATED_USER
        auth_name = str(authuser)
        if not user_name:
            user_name = auth_name
        # Access control
        if not self._can_handle_passwd(authuser, user_name):
            raise AccessDenied(
                "vous n'avez pas la permission de changer ce mot de passe"
            )
        log("reset_password: %s" % user_name)
        # Check that user has valid mail
        info = self.user_info(user_name=user_name)
        if not scu.is_valid_mail(info["email"]):
            raise Exception("pas de mail valide associé à l'utilisateur")
        # Generate random password
        password = sco_import_users.generate_password()
        self.do_change_password(user_name, password)
        # Flag it as temporary:
        cnx = self.GetUsersDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        ui = {"user_name": user_name}
        cursor.execute(
            "update sco_users set passwd_temp=1 where user_name='%(user_name)s'" % ui
        )
        # Send email
        info["passwd"] = password
        sco_import_users.mail_password(info, context=self, reset=True)


# --------------------------------------------------------------------
#
# Zope Product Administration
#
# --------------------------------------------------------------------
def manage_addZScoUsers(
    self, id="id_ZScousers", title="The Title for ZScoUsers Object", REQUEST=None
):
    "Add a ZScoUsers instance to a folder."
    self._setObject(id, ZScoUsers(id, title))
    if REQUEST is not None:
        return self.manage_main(self, REQUEST)
