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

##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2024 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
#
##############################################################################

"""
Module users: interface gestion utilisateurs
ré-écriture pour Flask ScoDoc7 / ZScoUsers.py

Vues s'appuyant sur auth et sco_users

Emmanuel Viennet, 2021
"""
import datetime
import re
from enum import auto, IntEnum
from xml.etree import ElementTree

import flask
from flask import g, url_for, request, flash
from flask import redirect, render_template
from flask_login import current_user
from flask_wtf import FlaskForm

from wtforms import HiddenField, PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, ValidationError, EqualTo

from app import db
from app import email
from app.auth.forms import DeactivateUserForm
from app.auth.models import Permission
from app.auth.models import User
from app.auth.models import Role
from app.auth.models import UserRole
from app.auth.models import is_valid_password
from app.models import Departement
from app.models.config import ScoDocSiteConfig

from app.decorators import (
    scodoc,
    scodoc7func,
    permission_required,
)

from app.scodoc import sco_import_users, sco_roles_default
from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml
from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied, ScoValueError
from app.scodoc.sco_import_users import generate_password
from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.views import users_bp as bp


_ = lambda x: x  # sans babel
_l = _


class ChangePasswordForm(FlaskForm):
    """formulaire changement mot de passe et mail"""

    user_name = HiddenField()
    old_password = PasswordField(_l("Mot de passe actuel"))
    new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur"))
    bis_password = PasswordField(
        _l("Répéter"),
        validators=[
            EqualTo(
                "new_password",
                message="Les deux saisies sont différentes, recommencez",
            ),
        ],
    )
    email = StringField(
        _l("Email"),
        validators=[
            DataRequired(),
            Email(message="adresse email invalide, recommencez"),
        ],
    )
    submit = SubmitField()
    cancel = SubmitField("Annuler")

    def validate_email(self, e_mail):
        "vérifie que le mail est unique (inline wtf validator)"
        user = User.query.filter_by(email=e_mail.data.strip()).first()
        if user is not None and self.user_name.data != user.user_name:
            raise ValidationError(
                _("Cette adresse e-mail est déjà attribuée à un autre compte")
            )

    def validate_new_password(self, new_password):
        "vérifie que le mot de passe est acceptable"
        if new_password.data != "" and not is_valid_password(new_password.data):
            raise ValidationError("Mot de passe trop simple, recommencez")

    def validate_old_password(self, old_password):
        "vérifie password actuel"
        if not current_user.check_password(old_password.data):
            raise ValidationError("Mot de passe actuel incorrect, ré-essayez")


class Mode(IntEnum):
    "Mode d'envoi de mail lors de création d'utilisateur"
    WELCOME_AND_CHANGE_PASSWORD = auto()  # mail indiquant de changer mdp
    WELCOME_ONLY = auto()  # mail d'accueil simple
    SILENT = auto()  # pas de mail


@bp.route("/")
@bp.route("/index_html", alias=True)
@scodoc
@permission_required(Permission.UsersView)
def index_html():
    "Page accueil utilisateur: tableau avec liste des comptes"
    all_depts = scu.to_bool(request.args.get("all_depts", False))
    detail_roles = scu.to_bool(request.args.get("detail_roles", False))
    fmt = request.args.get("fmt", "html")
    having_role_name = request.args.get("having_role_name", "")
    with_inactives = scu.to_bool(request.args.get("with_inactives", False))
    return sco_users.index_html(
        all_depts=all_depts,
        detail_roles=detail_roles,
        having_role_name=having_role_name,
        with_inactives=with_inactives,
        fmt=fmt,
    )


def _get_administrable_depts() -> list[str]:
    """Liste des acronymes des départements dans lesquels l'utilisateur
    courant peut administrer des utilisateurs.
    Si SuperAdmin, tous les départements
    Sinon, les départements dans lesquels l'utilisateur a la permission UsersAdmin
    """
    #
    if current_user.is_administrator():
        log(f"create_user_form called by {current_user.user_name} (super admin)")
        administrable_dept_acronyms = sorted(
            [d.acronym for d in Departement.query.all()]
        )
    else:
        administrable_dept_acronyms = current_user.get_depts_with_permission(
            Permission.UsersAdmin
        )
        if None in administrable_dept_acronyms:
            administrable_dept_acronyms = sorted(
                [d.acronym for d in Departement.query.all()]
            )

    return administrable_dept_acronyms


def _get_editable_roles(
    administrable_dept_acronyms: list = None, all_roles=True
) -> set[tuple[Role, str]]:
    """Rôles modifiables: ensemble de tuples (role, dept_acronym)
    (où dept_acronym est None si tous dept.)

    Si all_roles, tous les rôles définis et modifiables par l'utilisateurs.
    Sinon, seulement les rôles "standards" de ScoDoc.
    """
    if all_roles:
        # tous sauf SuperAdmin
        roles = [
            r
            for r in Role.query.all()
            if r.permissions != Permission.ALL_PERMISSIONS[0]
        ]
    else:
        # Les rôles standards créés à l'initialisation de ScoDoc:
        roles = [
            Role.get_named_role(r) for r in sco_roles_default.ROLES_ATTRIBUABLES_DEPT
        ]

    # Génère toutes les combinaisons roles/départements
    editable_roles_set = {
        (r, dept) for r in roles for dept in administrable_dept_acronyms
    }
    if current_user.is_administrator():
        editable_roles_set |= {
            (Role.get_named_role(r), None)
            for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC
        }
        # Un super-admin peut nommer d'autres super-admin:
        editable_roles_set |= {(Role.get_named_role("SuperAdmin"), None)}
    return editable_roles_set


@bp.route("/create_user_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.UsersAdmin)
@scodoc7func
def create_user_form(user_name=None, edit=0, all_roles=True):
    "form. création ou édition utilisateur"
    if user_name is not None:  # scodoc7func converti en int !
        user_name = str(user_name)
    Role.ensure_standard_roles()  # assure la présence des rôles en base
    auth_dept = current_user.dept
    initvalues = {}
    edit = int(edit)
    all_roles = int(all_roles)
    H = []
    the_user: User = None
    if edit:
        if not user_name:
            raise ValueError("missing argument: user_name")
        the_user: User = User.query.filter_by(user_name=user_name).first()
        if not the_user:
            raise ScoValueError("utilisateur inexistant")
        initvalues = the_user.to_dict()
        H.append(f"<h2>Modification de l'utilisateur {user_name}</h2>")
        submitlabel = "Modifier utilisateur"
        if "roles_string" in initvalues:
            initvalues["roles"] = [
                r.strip() for r in initvalues["roles_string"].split(",")
            ]
        else:
            initvalues["roles"] = []
        if "date_expiration" in initvalues:
            initvalues["date_expiration"] = (
                the_user.date_expiration.strftime(scu.DATE_FMT)
                if the_user.date_expiration
                else ""
            )
        initvalues["status"] = "" if the_user.active else "old"
        orig_roles = {  # set des roles existants avant édition
            UserRole.role_dept_from_string(role_dept)
            for role_dept in initvalues["roles"]
            if role_dept
        }
        if not initvalues["active"]:
            editable_roles_set = set()  # can't change roles of a disabled user
    else:
        H.append("<h2>Création d'un utilisateur</h2>")
        submitlabel = "Créer utilisateur"
        orig_roles = set()

    is_super_admin = current_user.is_administrator()
    if is_super_admin:
        H.append("""<p class="warning">Vous êtes super administrateur !</p>""")

    administrable_dept_acronyms = _get_administrable_depts()
    if edit:
        if the_user.dept is None:  # seul le super admin peut le toucher
            edit_only_roles = not current_user.is_administrator()
        else:
            edit_only_roles = the_user.dept not in administrable_dept_acronyms
    else:
        edit_only_roles = False  # création nouvel utilisateur
    editable_roles_set = _get_editable_roles(
        administrable_dept_acronyms, all_roles=all_roles
    )
    editable_roles_strings = {
        r.name + "_" + (dept or "") for (r, dept) in editable_roles_set
    }
    orig_roles_strings = {r.name + "_" + (dept or "") for (r, dept) in orig_roles}
    # add existing user roles
    displayed_roles = list(editable_roles_set.union(orig_roles))
    displayed_roles.sort(
        key=lambda x: (
            x[1] or "",
            (x[0].name or "") if x[0].name != "SuperAdmin" else "A",
        )
    )
    displayed_roles_strings = [
        r.name + "_" + (dept or "") for (r, dept) in displayed_roles
    ]
    displayed_roles_labels = [
        f"{dept or '<em>tout dépt.</em>'}: {r.name}" for (r, dept) in displayed_roles
    ]
    disabled_roles = {}  # pour désactiver les roles que l'on ne peut pas éditer
    for i, role_string in enumerate(displayed_roles_strings):
        if role_string not in editable_roles_strings:
            disabled_roles[i] = True
    # Formulaire:
    descr = [
        ("edit", {"input_type": "hidden", "default": edit}),
        (
            "nom",
            {
                "title": "Nom",
                "size": 20,
                "allow_null": False,
                "readonly": edit_only_roles,
                "strip": True,
            },
        ),
        (
            "prenom",
            {
                "title": "Prénom",
                "size": 20,
                "allow_null": False,
                "readonly": edit_only_roles,
                "strip": True,
            },
        ),
    ]
    if current_user.user_name != user_name and not edit_only_roles:
        # no one can change its own status
        descr.append(
            (
                "status",
                {
                    "title": "Statut",
                    "input_type": "radio",
                    "labels": ("actif", "ancien"),
                    "allowed_values": ("", "old"),
                },
            )
        )
        descr.append(
            (
                "passwd_must_be_changed",
                {
                    "title": "Force à changer le mot de passe",
                    "input_type": "boolcheckbox",
                    "explanation": """ à la première connexion.""",
                },
            )
        )
    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.
                        Lettres ou chiffres uniquement.""",
                },
            ),
            ("formsemestre_id", {"input_type": "hidden"}),
            (
                "password",
                {
                    "title": "Mot de passe",
                    "input_type": "password",
                    "size": 14,
                    "allow_null": True,
                    "explanation": "optionnel, l'utilisateur pourra le saisir avec son mail",
                },
            ),
            (
                "password2",
                {
                    "title": "Confirmer mot de passe",
                    "input_type": "password",
                    "size": 14,
                    "allow_null": True,
                },
            ),
        ]
    else:  # edition: on ne peut pas changer user_name
        descr += [
            (
                "user_name",
                {"input_type": "hidden", "default": initvalues["user_name"]},
            )
        ]
    cas_enabled = ScoDocSiteConfig.is_cas_enabled()
    if edit:
        cas_allow_login_default = the_user.cas_allow_login
    else:
        cas_allow_login_default = int(
            bool(ScoDocSiteConfig.get("cas_allow_for_new_users"))
        )
    require_email_institutionnel = (
        ScoDocSiteConfig.is_user_require_email_institutionnel_enabled()
    )
    descr += [
        (
            "email",
            {
                "title": "e-mail",
                "input_type": "text",
                "explanation": (
                    "requis, doit fonctionner" if not edit_only_roles else ""
                ),
                "size": 36,
                "allow_null": False,
                "readonly": edit_only_roles,
                "strip": True,
            },
        ),
        (
            "cas_id",
            {
                "title": "Identifiant CAS",
                "input_type": "text",
                "no_convert": True,  # empeche conversion en int de l'id
                "explanation": "id du compte utilisateur sur le CAS de l'établissement "
                + (
                    "<b>pa défaut identique à l'identifiant ScoDoc</b> "
                    if ScoDocSiteConfig.get("cas_uid_use_scodoc")
                    else (
                        "(<b>sera déduit de son e-mail institutionnel</b>) "
                        if ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
                        else ""
                    )
                )
                + (
                    "(service CAS activé)"
                    if cas_enabled
                    else "(service CAS non activé)"
                ),
                "size": 36,
                "allow_null": True,
                "readonly": not cas_enabled
                or not current_user.has_permission(Permission.UsersChangeCASId),
            },
        ),
        (
            "cas_allow_login",
            {
                "title": "Autorise connexion via CAS",
                "input_type": "boolcheckbox",
                "explanation": """ si CAS est activé.
                    Seul le super-administrateur peut changer ce réglage.""",
                "enabled": current_user.is_administrator(),
                "default": cas_allow_login_default,
            },
        ),
        (
            "cas_allow_scodoc_login",
            {
                "title": "Autorise connexion via ScoDoc",
                "input_type": "boolcheckbox",
                "explanation": """ même si CAS est activé et cas_id renseigné.
                    Seul le super-administrateur peut changer ce réglage""",
                "enabled": current_user.is_administrator(),
            },
        ),
        (
            "email_institutionnel",
            {
                "title": "e-mail institutionnel",
                "input_type": "text",
                "explanation": (
                    ("requis" if require_email_institutionnel else "facultatif")
                    if not edit_only_roles
                    else ""
                ),
                "size": 36,
                "strip": True,
                "allow_null": not require_email_institutionnel,
                "readonly": edit_only_roles,
            },
        ),
        (
            "edt_id",
            {
                "title": "Identifiant sur l'emploi du temps",
                "input_type": "text",
                "explanation": """id du compte utilisateur sur l'emploi du temps
                    ou l'annuaire de l'établissement (par défaut, l'e-mail institutionnel )""",
                "size": 36,
                "allow_null": True,
            },
        ),
    ]
    if not edit:  # options création utilisateur
        descr += [
            (
                "welcome",
                {
                    "title": "Message d'accueil",
                    "input_type": "boolcheckbox",
                    "explanation": "Envoie un mail d'accueil à l'utilisateur.",
                    "default": 1,
                },
            ),
            (
                "reset_password",
                {
                    "title": "",
                    "input_type": "boolcheckbox",
                    "explanation": """indiquer par mail de changer le mot
                        de passe initial (sauf si CAS forcé)""",
                    "default": 1,
                },
            ),
        ]
    # Si on a le droit d'administrer les utilisateurs de plusieurs départements,
    # propose le choix du dept du nouvel utilisateur
    selectable_dept_acronyms = set(administrable_dept_acronyms)
    if edit:
        if the_user.dept is not None:  # ajoute dept actuel de l'utilisateur
            selectable_dept_acronyms |= {the_user.dept}
        default_dept = the_user.dept
    else:
        default_dept = (
            g.scodoc_dept
            if g.scodoc_dept in selectable_dept_acronyms
            else (auth_dept or "")
        )
    if len(selectable_dept_acronyms) > 0:
        selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
        descr.append(
            (
                "dept",
                {
                    "title": "Département",
                    "input_type": "menu",
                    "explanation": """département de rattachement de l'utilisateur""",
                    "labels": selectable_dept_acronyms,
                    "allowed_values": selectable_dept_acronyms,
                    "default": default_dept,
                },
            )
        )
        can_choose_dept = True
    else:  # pas de choix de département
        can_choose_dept = False
        if edit:
            descr.append(
                (
                    "d",
                    {
                        "input_type": "separator",
                        "title": f"""L'utilisateur appartient au département {
                            the_user.dept or "(tous/aucun)"}""",
                    },
                )
            )
        else:
            descr.append(
                (
                    "d",
                    {
                        "input_type": "separator",
                        "title": f"""L'utilisateur  sera créé dans le département {
                            auth_dept or 'aucun'}""",
                    },
                )
            )

    descr += [
        (
            "date_expiration",
            {
                "title": "Date d'expiration",  # j/m/a
                "input_type": "datedmy",
                "explanation": (
                    "j/m/a, laisser vide si pas de limite"
                    if not edit_only_roles
                    else ""
                ),
                "size": 9,
                "allow_null": True,
                "readonly": edit_only_roles,
            },
        ),
        (
            "roles",
            {
                "title": "Rôles",
                "input_type": "checkbox",
                "vertical": True,
                "labels": displayed_roles_labels,
                "allowed_values": displayed_roles_strings,
                "disabled_items": disabled_roles,
            },
        ),
    ]
    if not edit_only_roles:
        descr += [
            (
                "force",
                {
                    "title": "Ignorer les avertissements",
                    "input_type": "checkbox",
                    "explanation": "passer outre les avertissements (homonymes, etc)",
                    "labels": ("",),
                    "allowed_values": ("1",),
                },
            ),
        ]
    vals = scu.get_request_args()
    if "tf_submitted" in vals and "roles" not in vals:
        vals["roles"] = []
    if "tf_submitted" in vals:
        # Ajoute roles existants mais non modifiables (disabled dans le form)
        vals["roles"] = list(
            set(vals["roles"]).union(orig_roles_strings - editable_roles_strings)
        )

    tf = TrivialFormulator(
        request.base_url,
        vals,
        descr,
        initvalues=initvalues,
        submitlabel=submitlabel,
        cancelbutton="Annuler",
    )
    if tf[0] == 0:
        return render_template(
            "base.j2",
            content="\n".join(H) + "\n" + tf[1],
            javascripts=["js/user_form.js"],
        )
    if tf[0] == -1:
        return flask.redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))

    vals = tf[2]
    roles = set(vals["roles"]).intersection(editable_roles_strings)
    if not current_user.is_administrator():
        # empeche modification des paramètres CAS
        if "cas_allow_login" in vals:
            vals["cas_allow_login"] = cas_allow_login_default
        if "cas_allow_scodoc_login" in vals:
            if the_user is None:
                vals.pop("cas_allow_scodoc_login", None)
            else:
                vals["cas_allow_scodoc_login"] = the_user.cas_allow_scodoc_login

    if not current_user.has_permission(Permission.UsersChangeCASId):
        vals.pop("cas_id", None)
    if "edit" in vals:
        edit = int(vals["edit"])
    else:
        edit = 0
    try:
        force = int(vals.get("force", "0")[0])
    except (IndexError, ValueError, TypeError):
        force = 0

    if edit:
        user_name = initvalues["user_name"]
    else:
        user_name = vals["user_name"]
    # ce login existe ?
    err_msg = None
    nb_existing_user = User.query.filter_by(user_name=user_name).count() > 0
    if edit and (
        nb_existing_user == 0
    ):  # safety net, le user_name ne devrait pas changer
        err_msg = f"identifiant {user_name} inexistant"
    if not edit and nb_existing_user > 0:
        err_msg = f"identifiant {user_name} déjà utilisé"
    if err_msg:
        H.append(tf_error_message(f"""Erreur: {err_msg}"""))
        return render_template(
            "base.j2",
            content="\n".join(H) + "\n" + tf[1],
            javascripts=["js/user_form.js"],
        )

    if not edit_only_roles:
        ok_modif, msg = sco_users.check_modif_user(
            edit,
            enforce_optionals=not force,
            user_name=user_name,
            nom=vals["nom"],
            prenom=vals["prenom"],
            email=vals["email"],
            dept=vals.get("dept", auth_dept),
            roles=vals["roles"],
            cas_id=vals.get("cas_id"),  # pas présent si pas super-admin
        )
        if not ok_modif:
            H.append(tf_error_message(msg))
            return render_template(
                "base.j2",
                content="\n".join(H) + "\n" + tf[1],
                javascripts=["js/user_form.js"],
            )
        if "date_expiration" in vals:
            try:
                if vals["date_expiration"]:
                    vals["date_expiration"] = datetime.datetime.strptime(
                        vals["date_expiration"], scu.DATE_FMT
                    )
                    if vals["date_expiration"] < datetime.datetime.now():
                        H.append(tf_error_message("date expiration passée"))
                        return render_template(
                            "base.j2",
                            content="\n".join(H) + "\n" + tf[1],
                            javascripts=["js/user_form.js"],
                        )
                else:
                    vals["date_expiration"] = None
            except ValueError:
                H.append(tf_error_message("date expiration invalide"))
                return render_template(
                    "base.j2",
                    content="\n".join(H) + "\n" + tf[1],
                    javascripts=["js/user_form.js"],
                )

    if edit:  # modif utilisateur (mais pas password ni user_name !)
        if (not can_choose_dept) and "dept" in vals:
            del vals["dept"]
        if "password" in vals:
            del vals["passwordd"]
        if "date_modif_passwd" in vals:
            del vals["date_modif_passwd"]
        if "user_name" in vals:
            del vals["user_name"]
        if (current_user.user_name == user_name) and "status" in vals:
            del vals["status"]  # no one can't change its own status
        if "status" in vals:
            vals["active"] = vals["status"] == ""
        # Département:
        if ("dept" in vals) and (vals["dept"] not in selectable_dept_acronyms):
            del vals["dept"]  # ne change pas de dept
        # Traitement des roles: ne doit pas affecter les rôles
        # que l'on en contrôle pas:
        for role in orig_roles_strings:  # { "Ens_RT", "Secr_CJ", ... }
            if role and not role in editable_roles_strings:
                roles.add(role)

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

        # ok, edit
        if not edit_only_roles:
            log(f"sco_users: editing {user_name} by {current_user.user_name}")
            log(f"sco_users: previous_values={initvalues}")
            log(f"sco_users: new_values={vals}")
        else:
            vals = {"roles_string": vals["roles_string"]}
        the_user.from_dict(vals)
        db.session.add(the_user)
        db.session.commit()
        flash(f"Utilisateur {user_name} modifié")
        return flask.redirect(
            url_for(
                "users.user_info_page",
                scodoc_dept=g.scodoc_dept,
                user_name=user_name,
            )
        )

    else:  # création utilisateur
        vals["roles_string"] = ",".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 render_template(
                "base.j2",
                content="\n".join(H) + msg + "\n" + tf[1],
                javascripts=["js/user_form.js"],
            )
        # Traitement initial (mode) : 3 cas
        # cf énumération Mode
        # A: envoi de welcome + procedure de reset
        # B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
        # C: Aucun envoi (mot de passe saisi dans le formulaire)
        if vals["welcome"]:  # "Envoie un mail d'accueil" coché
            if vals["reset_password"] and (
                (not ScoDocSiteConfig.get("cas_force"))
                or vals.get("cas_allow_scodoc_login", False)
            ):
                # nb: si login scodoc non autorisé car CAS seul, n'envoie pas le mot de passe.
                mode = Mode.WELCOME_AND_CHANGE_PASSWORD
            else:
                mode = Mode.WELCOME_ONLY
        else:
            mode = Mode.SILENT

        # check passwords
        if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
            vals["password"] = generate_password()
        else:
            if vals["password"]:
                if vals["password"] != vals["password2"]:
                    msg = tf_error_message(
                        """Les deux mots de passes ne correspondent pas !"""
                    )
                    return render_template(
                        "base.j2",
                        content="\n".join(H) + msg + "\n" + tf[1],
                        javascripts=["js/user_form.js"],
                    )
                if not is_valid_password(vals["password"]):
                    msg = tf_error_message(
                        """Mot de passe trop simple, recommencez !"""
                    )
                    return render_template(
                        "base.j2",
                        content="\n".join(H) + msg + "\n" + tf[1],
                        javascripts=["js/user_form.js"],
                    )
        # Département:
        if not can_choose_dept:
            vals["dept"] = auth_dept
        else:
            if auth_dept:  # pas super-admin
                if vals["dept"] not in selectable_dept_acronyms:
                    raise ScoValueError("département invalide")
        # ok, go
        log(f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}""")
        the_user = User(user_name=user_name)
        the_user.from_dict(vals, new_user=True)
        db.session.add(the_user)
        db.session.commit()
        # envoi éventuel d'un message
        if mode in (Mode.WELCOME_AND_CHANGE_PASSWORD, Mode.WELCOME_ONLY):
            token = (
                the_user.get_reset_password_token()
                if mode == Mode.WELCOME_AND_CHANGE_PASSWORD
                else None
            )

            cas_force = ScoDocSiteConfig.get("cas_force")
            # Le from doit utiliser la préférence du département de l'utilisateur
            email.send_email(
                "[ScoDoc] Création de votre compte",
                sender=email.get_from_addr(),
                recipients=[the_user.email],
                text_body=render_template(
                    "email/welcome.txt",
                    user=the_user,
                    token=token,
                    cas_force=cas_force,
                ),
                html_body=render_template(
                    "email/welcome.j2",
                    user=the_user,
                    token=token,
                    cas_force=cas_force,
                ),
            )
            flash(f"Mail accueil envoyé à {the_user.email}")

        flash("Nouvel utilisateur créé")
        return flask.redirect(
            url_for(
                "users.user_info_page",
                scodoc_dept=g.scodoc_dept,
                user_name=user_name,
            )
        )


@bp.route("/import_users_generate_excel_sample")
@scodoc
@permission_required(Permission.UsersAdmin)
@scodoc7func
def import_users_generate_excel_sample():
    "une feuille excel pour importation utilisateurs"
    data = sco_import_users.generate_excel_sample()
    return scu.send_file(data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)


@bp.route("/import_users_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.UsersAdmin)
@scodoc7func
def import_users_form():
    """Import utilisateurs depuis feuille Excel"""
    H = [
        """<h2>Téléchargement d'une 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_msg = """<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(
        f"""<ul>
        <li><b>Étape 1: </b><a class="stdlink" href="{
            url_for("users.import_users_generate_excel_sample", scodoc_dept=g.scodoc_dept)
        }">Obtenir la feuille excel vide à remplir</a>
        </li>
        <li><b> Étape 2:</b>
        """
    )
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        (
            (
                "xlsfile",
                {"title": "Fichier Excel:", "input_type": "file", "size": 40},
            ),
            (
                "force",
                {
                    "title": "Ignorer les avertissements",
                    "input_type": "checkbox",
                    "explanation": "passer outre les avertissements (homonymes, etc)",
                    "labels": ("",),
                    "allowed_values": ("1",),
                },
            ),
            ("formsemestre_id", {"input_type": "hidden"}),
        ),
        submitlabel="Télécharger",
    )
    if tf[0] == 0:
        return render_template(
            "sco_page_dept.j2",
            title="Import utilisateurs",
            content="\n".join(H) + tf[1] + "</li></ul>" + help_msg,
        )
    elif tf[0] == -1:
        return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept))

    # IMPORT
    ok, diags, nb_created = sco_import_users.import_excel_file(
        tf[2]["xlsfile"], tf[2]["force"]
    )
    H = [
        """<h2>Téléchargement d'une liste d'utilisateurs</h2>
         <ul>"""
    ]
    for diag in diags:
        H.append(f"<li>{diag}</li>")
    H.append("</ul>")
    if ok:
        dest_url = url_for("users.index_html", scodoc_dept=g.scodoc_dept, all_depts=1)
        H.append(
            f"""<p>Ok, Import terminé ({nb_created} utilisateurs créés)!</p>
                <p><a class="stdlink" href="{dest_url}">Continuer</a></p>
        """
        )
    else:
        dest_url = url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
        H.append(
            f"""<p>Erreur, importation annulée !</p>
                <p><a class="stdlink" href="{dest_url}">Continuer</a></p>
        """
        )
    return render_template(
        "sco_page_dept.j2", title="Import utilisateurs", content="\n".join(H)
    )


@bp.route("/user_info_page")
@scodoc
@permission_required(Permission.UsersView)
@scodoc7func
def user_info_page(user_name=None):
    """Display page of info about given user.
    If user_name not specified, user current_user
    """
    if user_name is not None:  # scodoc7func converti en int !
        user_name = str(user_name)
    # peut-on divulguer ces infos ?
    if not can_handle_passwd(current_user, allow_admindepts=True):
        raise AccessDenied("Vous n'avez pas la permission de voir cette page")

    dept = g.scodoc_dept
    if not user_name:
        user = current_user
    else:
        user = User.query.filter_by(user_name=user_name).first()
    if not user:
        raise ScoValueError("invalid user_name")

    session_info = None
    if user.id == current_user.id:
        session_info = flask.session.get("scodoc_cas_login_date")

    return render_template(
        "auth/user_info_page.j2",
        dept=dept,
        Permission=Permission,
        ScoDocSiteConfig=ScoDocSiteConfig,
        session_info=session_info,
        title=f"Utilisateur {user.user_name}",
        user=user,
    )


@bp.route("/get_user_list_xml")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def get_user_list_xml(dept=None, start="", limit=25):
    """Returns XML list of users with name (nomplogin) starting with start.
    Used for forms auto-completion.
    """
    # suggère seulement seulement les utilisateurs actifs:
    userlist = sco_users.get_user_list(dept=dept)
    start = scu.suppress_accents(str(start)).lower()
    # TODO : à refaire avec une requete SQL #py3
    # (et en json)
    userlist = [
        user
        for user in userlist
        if scu.suppress_accents((user.nom or "").strip().lower()).startswith(start)
    ]
    doc = ElementTree.Element("results")
    for user in userlist[:limit]:
        x_rs = ElementTree.Element("rs", id=str(user.id), info="")
        x_rs.text = user.get_nomplogin()
        doc.append(x_rs)

    data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
    return scu.send_file(data, mime=scu.XML_MIMETYPE)


@bp.route("/form_change_password", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def form_change_password(user_name=None):
    """Formulaire de changement mot de passe de l'utilisateur user_name.
    Un utilisateur peut toujours changer son propre mot de passe.
    """
    if user_name is not None:  # scodoc7func converti en int !
        user_name = str(user_name)
    if not user_name:
        user = current_user
    else:
        user = User.query.filter_by(user_name=user_name).first()

    # check access
    if not can_handle_passwd(user):
        return render_template(
            "base.j2",
            content="<p>Vous n'avez pas la permission de changer ce mot de passe</p>",
            title="Accès refusé",
        )

    form = ChangePasswordForm(user_name=user.user_name, email=user.email)
    destination = url_for(
        "users.user_info_page",
        scodoc_dept=g.scodoc_dept,
        user_name=user_name,
    )
    if request.method == "POST" and form.cancel.data:  # cancel button clicked
        return redirect(destination)
    if form.validate_on_submit():
        messages = []
        if form.new_password.data != "":  # change password
            user.set_password(form.new_password.data)
            messages.append("Mot de passe modifié")
        if form.email.data.strip() != user.email:  # change email
            user.email = form.email.data.strip()
            messages.append("Adresse email modifiée")
        db.session.commit()
        flash("\n".join(messages))
        return redirect(destination)

    return render_template(
        "auth/change_password.j2",
        form=form,
        title="Modification compte ScoDoc",
        auth_username=current_user.user_name,
    )


@bp.route("/toggle_active_user/<user_name>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.UsersAdmin)
def toggle_active_user(user_name: str = None):
    """Change active status of a user account"""
    if user_name is not None:  # scodoc7func converti en int !
        user_name = str(user_name)

    u = User.query.filter_by(user_name=user_name).first()
    if not u:
        raise ScoValueError("invalid user_name")
    # permission check:
    if not (
        current_user.is_administrator()
        or current_user.has_permission(Permission.UsersAdmin, u.dept)
    ):
        raise ScoPermissionDenied()
    form = DeactivateUserForm()
    if request.method == "POST" and form.cancel.data:
        # if cancel button is clicked, the form.cancel.data will be True
        return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
    if form.validate_on_submit():
        u.active = not u.active
        db.session.add(u)
        db.session.commit()
        flash(
            f"Compte utilisateur {u.user_name} {'activé' if u.active else 'désactivé'}"
        )
        return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
    return render_template("auth/toogle_active_user.j2", form=form, u=u)