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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 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
#
##############################################################################

""" Accès donnees etudiants
"""

# Ancien module "scolars"
import os
import time
from operator import itemgetter

from flask import url_for, g

from app import db, email
from app import log
from app.models import Admission, Identite
from app.models.etudiants import input_civilite, make_etud_args, pivot_year
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app.scodoc import safehtml
from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb


def format_etud_ident(etud):
    """Format identite de l'étudiant (modifié en place)
    nom, prénom et formes associees
    """
    etud["nom"] = format_nom(etud["nom"])
    if "nom_usuel" in etud:
        etud["nom_usuel"] = format_nom(etud["nom_usuel"])
    else:
        etud["nom_usuel"] = ""
    etud["prenom"] = format_prenom(etud["prenom"])
    if "prenom_etat_civil" in etud:
        etud["prenom_etat_civil"] = format_prenom(etud["prenom_etat_civil"])
    else:
        etud["prenom_etat_civil"] = ""
    etud["civilite_str"] = format_civilite(etud["civilite"])
    etud["civilite_etat_civil_str"] = format_civilite(
        etud.get("civilite_etat_civil", "X")
    )
    # Nom à afficher:
    if etud["nom_usuel"]:
        etud["nom_disp"] = etud["nom_usuel"]
        if etud["nom"]:
            etud["nom_disp"] += " (" + etud["nom"] + ")"
    else:
        etud["nom_disp"] = etud["nom"]

    etud["nomprenom"] = format_nomprenom(etud)  # M. Pierre DUPONT
    etud["etat_civil"] = format_etat_civil(etud)
    if etud["civilite"] == "M":
        etud["ne"] = ""
    elif etud["civilite"] == "F":
        etud["ne"] = "e"
    else:  # 'X'
        etud["ne"] = "(e)"
    # Mail à utiliser pour les envois vers l'étudiant:
    # choix qui pourrait être controé par une preference
    # ici priorité au mail institutionnel:
    etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")


def force_uppercase(s):
    return s.upper() if s else s


def format_nomprenom(etud, reverse=False):
    """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
    Si reverse, "Dupont Pierre", sans civilité.

    DEPRECATED: utiliser Identite.nomprenom
    """
    nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
    prenom = format_prenom(etud["prenom"])
    civilite = format_civilite(etud["civilite"])
    if reverse:
        fs = [nom, prenom]
    else:
        fs = [civilite, prenom, nom]
    return " ".join([x for x in fs if x])


def format_prenom(s):
    """Formatte prenom etudiant pour affichage
    DEPRECATED: utiliser Identite.prenom_str
    """
    if not s:
        return ""
    frags = s.split()
    r = []
    for frag in frags:
        fs = frag.split("-")
        r.append("-".join([x.lower().capitalize() for x in fs]))
    return " ".join(r)


def format_nom(s, uppercase=True):
    if not s:
        return ""
    if uppercase:
        return s.upper()
    else:
        return format_prenom(s)


def format_civilite(civilite):
    """returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
    personne ne souhaitant pas d'affichage).
    Raises ScoValueError if conversion fails.
    """
    try:
        return {
            "M": "M.",
            "F": "Mme",
            "X": "",
        }[civilite]
    except KeyError:
        raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)


def format_etat_civil(etud: dict):
    if etud["prenom_etat_civil"]:
        civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
        return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
    else:
        return etud["nomprenom"]


def format_lycee(nomlycee):
    nomlycee = nomlycee.strip()
    s = nomlycee.lower()
    if s[:5] == "lycee" or s[:5] == "lycée":
        return nomlycee[5:]
    else:
        return nomlycee


def format_telephone(n):
    if n is None:
        return ""
    if len(n) < 7:
        return n
    else:
        n = n.replace(" ", "").replace(".", "")
        i = 0
        r = ""
        j = len(n) - 1
        while j >= 0:
            r = n[j] + r
            if i % 2 == 1 and j != 0:
                r = " " + r
            i += 1
            j -= 1
        if len(r) == 13 and r[0] != "0":
            r = "0" + r
        return r


def format_pays(s):
    "laisse le pays seulement si != FRANCE"
    if s.upper() != "FRANCE":
        return s
    else:
        return ""


def etud_sort_key(etud: dict) -> tuple:
    """Clé de tri pour les étudiants représentés par des dict (anciens codes).
    Equivalent moderne: identite.sort_key
    """
    return (
        scu.sanitize_string(
            etud.get("nom_usuel") or etud["nom"] or "", remove_spaces=False
        ).lower(),
        scu.sanitize_string(etud["prenom"] or "", remove_spaces=False).lower(),
    )


_identiteEditor = ndb.EditableTable(
    "identite",
    "etudid",
    (
        "etudid",
        "nom",
        "nom_usuel",
        "prenom",
        "prenom_etat_civil",
        "cas_id",
        "cas_allow_login",
        "cas_allow_scodoc_login",
        "civilite",  # 'M", "F", or "X"
        "civilite_etat_civil",
        "date_naissance",
        "lieu_naissance",
        "dept_naissance",
        "nationalite",
        "statut",
        "boursier",
        "foto",
        "photo_filename",
        "code_ine",
        "code_nip",
    ),
    filter_dept=True,
    sortkey="nom",
    input_formators={
        "nom": force_uppercase,
        "prenom": force_uppercase,
        "prenom_etat_civil": force_uppercase,
        "civilite": input_civilite,
        "civilite_etat_civil": input_civilite,
        "date_naissance": ndb.DateDMYtoISO,
        "boursier": bool,
    },
    output_formators={"date_naissance": ndb.DateISOtoDMY},
    convert_null_outputs_to_empty=True,
    # allow_set_id=True,  # car on specifie le code Apogee a la creation #sco8
)

identite_delete = _identiteEditor.delete


def identite_list(cnx, *a, **kw):
    """List, adding on the fly 'annee_naissance' and 'civilite_str' (M., Mme, "")."""
    objs = _identiteEditor.list(cnx, *a, **kw)
    for o in objs:
        if o["date_naissance"]:
            o["annee_naissance"] = int(o["date_naissance"].split("/")[2])
        else:
            o["annee_naissance"] = o["date_naissance"]
        o["civilite_str"] = format_civilite(o["civilite"])
        o["civilite_etat_civil_str"] = format_civilite(o["civilite_etat_civil"])
    return objs


def identite_edit_nocheck(cnx, args):
    """Modifie les champs mentionnes dans args, sans verification ni notification."""
    etud = db.session.get(Identite, args["etudid"])
    etud.from_dict(args)
    db.session.commit()


def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
    """Check if nom and prenom are valid.
    Also check for duplicates (homonyms), excluding etudid :
    in general, homonyms are allowed, but it may be useful to generate a warning.
    Returns:
    True | False, NbHomonyms
    """
    if not nom or (not prenom and not scu.CONFIG.ALLOW_NULL_PRENOM):
        return False, 0
    nom = nom.lower().strip()
    if prenom:
        prenom = prenom.lower().strip()
    # Don't allow some special cars (eg used in sql regexps)
    if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom):
        return False, 0
    # Now count homonyms (dans tous les départements):
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    req = """SELECT id 
    FROM identite 
    WHERE lower(nom) ~ %(nom)s 
    and lower(prenom) ~ %(prenom)s
    """
    if etudid:
        req += "  and id <> %(etudid)s"
    cursor.execute(req, {"nom": nom, "prenom": prenom, "etudid": etudid})
    res = cursor.dictfetchall()
    return True, len(res)


def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
    """Vérifie que le code n'est pas dupliqué"""
    etudid = args.get("etudid", None)
    if args.get(code_name, None):
        etuds = identite_list(cnx, {code_name: str(args[code_name])})
        duplicate = False
        if edit:
            duplicate = (len(etuds) > 1) or (
                (len(etuds) == 1) and etuds[0]["id"] != args["etudid"]
            )
        else:
            duplicate = len(etuds) > 0
        if duplicate:
            listh = []  # liste des doubles
            for e in etuds:
                listh.append(
                    f"""Autre étudiant: <a href="{
                        url_for(
                            "scolar.ficheEtud",
                            scodoc_dept=g.scodoc_dept,
                            etudid=e["etudid"]
                        )}">{e['nom']} {e['prenom']}</a>"""
                )
            if etudid:
                OK = "retour à la fiche étudiant"
                dest_endpoint = "scolar.ficheEtud"
                parameters = {"etudid": etudid}
            else:
                if "tf_submitted" in args:
                    del args["tf_submitted"]
                    OK = "Continuer"
                    dest_endpoint = "scolar.etudident_create_form"
                    parameters = args
                else:
                    OK = "Annuler"
                    dest_endpoint = "notes.index_html"
                    parameters = {}
            if not disable_notify:
                err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
                <p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir 
                ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
                </p>
                <ul><li>
                { '</li><li>'.join(listh) }
                </li></ul>
                <p>
                <a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
                ">{OK}</a>
                </p>
                """
            else:
                err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>"""
            log(f"*** error: code {code_name} duplique: {args[code_name]}")
            raise ScoGenError(err_page)


def _check_civilite(args):
    civilite = args.get("civilite", "X") or "X"
    args["civilite"] = input_civilite(civilite)  # TODO: A faire valider


def identite_edit(cnx, args, disable_notify=False):
    """Modifie l'identite d'un étudiant.
    Si pref notification et difference, envoie message notification, sauf si disable_notify
    """
    _check_duplicate_code(
        cnx, args, "code_nip", disable_notify=disable_notify, edit=True
    )
    _check_duplicate_code(
        cnx, args, "code_ine", disable_notify=disable_notify, edit=True
    )
    notify_to = None
    if not disable_notify:
        try:
            notify_to = sco_preferences.get_preference("notify_etud_changes_to")
        except:
            pass

    if notify_to:
        # etat AVANT edition pour envoyer diffs
        before = identite_list(cnx, {"etudid": args["etudid"]})[0]

    identite_edit_nocheck(cnx, args)

    # Notification du changement par e-mail:
    if notify_to:
        etud = get_etud_info(etudid=args["etudid"], filled=True)[0]
        after = identite_list(cnx, {"etudid": args["etudid"]})[0]
        notify_etud_change(
            notify_to,
            etud,
            before,
            after,
            "Modification identite %(nomprenom)s" % etud,
        )


def identite_create(cnx, args):
    "check unique etudid, then create"
    _check_duplicate_code(cnx, args, "code_nip", edit=False)
    _check_duplicate_code(cnx, args, "code_ine", edit=False)
    _check_civilite(args)

    if "etudid" in args:
        etudid = args["etudid"]
        r = identite_list(cnx, {"etudid": etudid})
        if r:
            raise ScoValueError(
                "Code identifiant (etudid) déjà utilisé ! (%s)" % etudid
            )
    return _identiteEditor.create(cnx, args)


def notify_etud_change(email_addr, etud, before, after, subject):
    """Send email notifying changes to etud
    before and after are two dicts, with values before and after the change.
    """
    txt = [
        "Code NIP:" + etud["code_nip"],
        "Civilité: " + etud["civilite_str"],
        "Nom: " + etud["nom"],
        "Prénom: " + etud["prenom"],
        "Etudid: " + str(etud["etudid"]),
        "\n",
        "Changements effectués:",
    ]
    n = 0
    for key in after.keys():
        if before[key] != after[key]:
            txt.append('%s: %s (auparavant: "%s")' % (key, after[key], before[key]))
            n += 1
    if not n:
        return  # pas de changements
    txt = "\n".join(txt)
    # build mail
    log("notify_etud_change: sending notification to %s" % email_addr)
    log("notify_etud_change: subject: %s" % subject)
    log(txt)
    email.send_email(subject, email.get_from_addr(), [email_addr], txt)
    return txt


# --------
# Note: la table adresse n'est pas dans dans la table "identite"
#       car on prevoit plusieurs adresses par etudiant (ie domicile, entreprise)

_adresseEditor = ndb.EditableTable(
    "adresse",
    "adresse_id",
    (
        "adresse_id",
        "etudid",
        "email",
        "emailperso",
        "domicile",
        "codepostaldomicile",
        "villedomicile",
        "paysdomicile",
        "telephone",
        "telephonemobile",
        "fax",
        "typeadresse",
        "description",
    ),
    convert_null_outputs_to_empty=True,
)

adresse_create = _adresseEditor.create
adresse_delete = _adresseEditor.delete
adresse_list = _adresseEditor.list


def adresse_edit(cnx, args, disable_notify=False):
    """Modifie l'adresse d'un étudiant.
    Si pref notification et difference, envoie message notification, sauf si disable_notify
    """
    notify_to = None
    if not disable_notify:
        try:
            notify_to = sco_preferences.get_preference("notify_etud_changes_to")
        except:
            pass
    if notify_to:
        # etat AVANT edition pour envoyer diffs
        before = adresse_list(cnx, {"etudid": args["etudid"]})[0]

    _adresseEditor.edit(cnx, args)

    # Notification du changement par e-mail:
    if notify_to:
        etud = get_etud_info(etudid=args["etudid"], filled=True)[0]
        after = adresse_list(cnx, {"etudid": args["etudid"]})[0]
        notify_etud_change(
            notify_to,
            etud,
            before,
            after,
            "Modification adresse %(nomprenom)s" % etud,
        )


def getEmail(cnx, etudid):
    "get email institutionnel etudiant (si plusieurs adresses, prend le premier non null"
    adrs = adresse_list(cnx, {"etudid": etudid})
    for adr in adrs:
        if adr["email"]:
            return adr["email"]
    return ""


# ---------
_admissionEditor = ndb.EditableTable(
    "admissions",
    "adm_id",
    (
        "adm_id",
        "etudid",
        "annee",
        "bac",
        "specialite",
        "annee_bac",
        "math",
        "physique",
        "anglais",
        "francais",
        "rang",
        "qualite",
        "rapporteur",
        "decision",
        "score",
        "classement",
        "apb_groupe",
        "apb_classement_gr",
        "commentaire",
        "nomlycee",
        "villelycee",
        "codepostallycee",
        "codelycee",
        "type_admission",
        "boursier_prec",
    ),
    input_formators={
        "annee": pivot_year,
        "bac": force_uppercase,
        "specialite": force_uppercase,
        "annee_bac": pivot_year,
        "classement": ndb.int_null_is_null,
        "apb_classement_gr": ndb.int_null_is_null,
        "boursier_prec": bool,
    },
    output_formators={"type_admission": lambda x: x or scu.TYPE_ADMISSION_DEFAULT},
    convert_null_outputs_to_empty=True,
)

admission_create = _admissionEditor.create
admission_delete = _admissionEditor.delete
admission_list = _admissionEditor.list
admission_edit = _admissionEditor.edit


# Edition simultanee de identite et admission
class EtudIdentEditor(object):
    def create(self, cnx, args):
        etudid = identite_create(cnx, args)
        args["etudid"] = etudid
        admission_create(cnx, args)
        return etudid

    def list(self, *args, **kw):
        R = identite_list(*args, **kw)
        Ra = admission_list(*args, **kw)
        # print len(R), len(Ra)
        # merge: add admission fields to identite
        A = {}
        for r in Ra:
            A[r["etudid"]] = r
        res = []
        for i in R:
            res.append(i)
            if i["etudid"] in A:
                # merge
                res[-1].update(A[i["etudid"]])
            else:  # pas d'etudiant trouve
                # print "*** pas d'info admission pour %s" % str(i)
                void_adm = {
                    k: None
                    for k in _admissionEditor.dbfields
                    if k != "etudid" and k != "adm_id"
                }
                res[-1].update(void_adm)
        # tri par nom
        res.sort(key=itemgetter("nom", "prenom"))
        return res

    def edit(self, cnx, args, disable_notify=False):
        identite_edit(cnx, args, disable_notify=disable_notify)
        if "adm_id" in args:  # safety net
            admission_edit(cnx, args)


_etudidentEditor = EtudIdentEditor()
etudident_list = _etudidentEditor.list
etudident_edit = _etudidentEditor.edit


def log_unknown_etud():
    """Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
    etud_args = make_etud_args(raise_exc=False)
    log(f"unknown student: args={etud_args}")


def get_etud_info(etudid=False, code_nip=False, filled=False) -> list[dict]:
    """infos sur un etudiant (API). If not found, returns empty list.
    On peut spécifier etudid ou code_nip
    ou bien cherche dans les arguments de la requête courante:
     etudid, code_nip, code_ine (dans cet ordre).
    """
    if etudid is None:
        return []
    cnx = ndb.GetDBConnexion()
    args = make_etud_args(etudid=etudid, code_nip=code_nip)
    etud = etudident_list(cnx, args=args)

    if filled:
        fill_etuds_info(etud)
    return etud


def create_etud(cnx, args: dict = None):
    """Création d'un étudiant. Génère aussi évenement et "news".

    Args:
        args: dict avec les attributs de l'étudiant

    Returns:
        etud, l'étudiant créé.
    """
    from app.models import ScolarNews

    # creation d'un etudiant
    args_dict = Identite.convert_dict_fields(args)
    args_dict["dept_id"] = g.scodoc_dept_id
    etud = Identite.create_etud(**args_dict)
    db.session.add(etud)
    db.session.commit()
    admission = etud.admission.first()
    admission.from_dict(args)
    db.session.add(admission)
    db.session.commit()
    etudid = etud.id

    # event
    scolar_events_create(
        cnx,
        args={
            "etudid": etudid,
            "event_date": time.strftime("%d/%m/%Y"),
            "formsemestre_id": None,
            "event_type": "CREATION",
        },
    )
    # log
    logdb(
        cnx,
        method="etudident_edit_form",
        etudid=etudid,
        msg="creation initiale",
    )
    etud = etudident_list(cnx, {"etudid": etudid})[0]
    fill_etuds_info([etud])
    etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
    ScolarNews.add(
        typ=ScolarNews.NEWS_INSCR,
        text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
        url=etud["url"],
        max_frequency=0,
    )
    return etud


# ---------- "EVENTS"
_scolar_eventsEditor = ndb.EditableTable(
    "scolar_events",
    "event_id",
    (
        "event_id",
        "etudid",
        "event_date",
        "formsemestre_id",
        "ue_id",
        "event_type",
        "comp_formsemestre_id",
    ),
    sortkey="event_date",
    convert_null_outputs_to_empty=True,
    output_formators={"event_date": ndb.DateISOtoDMY},
    input_formators={"event_date": ndb.DateDMYtoISO},
)

# scolar_events_create = _scolar_eventsEditor.create
scolar_events_delete = _scolar_eventsEditor.delete
scolar_events_list = _scolar_eventsEditor.list
scolar_events_edit = _scolar_eventsEditor.edit


def scolar_events_create(cnx, args):
    # several "events" may share the same values
    _scolar_eventsEditor.create(cnx, args)


# --------
_etud_annotationsEditor = ndb.EditableTable(
    "etud_annotations",
    "id",
    (
        "id",
        "date",
        "etudid",
        "author",
        "comment",
        "author",
    ),
    sortkey="date desc",
    convert_null_outputs_to_empty=True,
    output_formators={"comment": safehtml.html_to_safe_html, "date": ndb.DateISOtoDMY},
)

etud_annotations_create = _etud_annotationsEditor.create
etud_annotations_delete = _etud_annotationsEditor.delete
etud_annotations_list = _etud_annotationsEditor.list
etud_annotations_edit = _etud_annotationsEditor.edit


def add_annotations_to_etud_list(etuds):
    """Add key 'annotations' describing annotations of etuds
    (used to list all annotations of a group)
    """
    cnx = ndb.GetDBConnexion()
    for etud in etuds:
        l = []
        for a in etud_annotations_list(cnx, args={"etudid": etud["etudid"]}):
            l.append("%(comment)s (%(date)s)" % a)
        etud["annotations_str"] = ", ".join(l)


# -------- Noms des Lycées à partir du code
def read_etablissements():
    filename = os.path.join(scu.SCO_TOOLS_DIR, scu.CONFIG.ETABL_FILENAME)
    log("reading %s" % filename)
    with open(filename) as f:
        L = [x[:-1].split(";") for x in f]
    E = {}
    for l in L[1:]:
        E[l[0]] = {
            "name": l[1],
            "address": l[2],
            "codepostal": l[3],
            "commune": l[4],
            "position": l[5] + "," + l[6],
        }
    return E


ETABLISSEMENTS = None


def get_etablissements():
    global ETABLISSEMENTS
    if ETABLISSEMENTS is None:
        ETABLISSEMENTS = read_etablissements()
    return ETABLISSEMENTS


def get_lycee_infos(codelycee):
    E = get_etablissements()
    return E.get(codelycee, None)


def format_lycee_from_code(codelycee):
    "Description lycee à partir du code"
    E = get_etablissements()
    if codelycee in E:
        e = E[codelycee]
        nomlycee = e["name"]
        return "%s (%s)" % (nomlycee, e["commune"])
    else:
        return "%s (établissement inconnu)" % codelycee


def etud_add_lycee_infos(etud):
    """Si codelycee est renseigné, ajout les champs au dict"""
    if etud["codelycee"]:
        il = get_lycee_infos(etud["codelycee"])
        if il:
            if not etud["codepostallycee"]:
                etud["codepostallycee"] = il["codepostal"]
            if not etud["nomlycee"]:
                etud["nomlycee"] = il["name"]
            if not etud["villelycee"]:
                etud["villelycee"] = il["commune"]
            if not etud.get("positionlycee", None):
                if il["position"] != "0.0,0.0":
                    etud["positionlycee"] = il["position"]
    return etud


""" Conversion fichier original:
f = open('etablissements.csv')
o = open('etablissements2.csv', 'w')
o.write( f.readline() )
for l in f:
    fs = l.split(';')
    nom = ' '.join( [ x.capitalize() for x in fs[1].split() ] )
    adr = ' '.join( [ x.capitalize() for x in fs[2].split() ] )
    ville=' '.join( [ x.capitalize() for x in fs[4].split() ] )
    o.write( '%s;%s;%s;%s;%s\n' % (fs[0], nom, adr, fs[3], ville))

o.close()
"""


def list_scolog(etudid):
    "liste des operations effectuees sur cet etudiant"
    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        "SELECT * FROM scolog WHERE etudid=%(etudid)s ORDER BY DATE DESC",
        {"etudid": etudid},
    )
    return cursor.dictfetchall()


def fill_etuds_info(etuds: list[dict], add_admission=True):
    """etuds est une liste d'etudiants (mappings)
    Pour chaque etudiant, ajoute ou formatte les champs
    -> informations pour fiche etudiant ou listes diverses

    Si add_admission: ajoute au dict le schamps "admission" s'il n'y sont pas déjà.
    """
    cnx = ndb.GetDBConnexion()
    for etud in etuds:
        etudid = etud["etudid"]
        etud["dept"] = g.scodoc_dept
        # Admission
        if add_admission and "nomlycee" not in etud:
            admission = (
                Admission.query.filter_by(etudid=etudid).first().to_dict(no_nulls=True)
            )
            del admission["id"]  # pour garder id == etudid dans etud
            etud.update(admission)
        #
        adrs = adresse_list(cnx, {"etudid": etudid})
        if not adrs:
            # certains "vieux" etudiants n'ont pas d'adresse
            adr = {}.fromkeys(_adresseEditor.dbfields, "")
            adr["etudid"] = etudid
        else:
            adr = adrs[0]
            if len(adrs) > 1:
                log("fill_etuds_info: etudid=%s a %d adresses" % (etudid, len(adrs)))
        adr.pop("id", None)
        etud.update(adr)
        format_etud_ident(etud)

        etud.update(etud_inscriptions_infos(etudid, etud["ne"]))

        # nettoyage champs souvent vides
        if etud.get("nomlycee"):
            etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
            if etud["villelycee"]:
                etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
            etud["ilycee"] += "<br>"
        else:
            if etud.get("codelycee"):
                etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
            else:
                etud["ilycee"] = ""
        rap = ""
        if etud.get("rapporteur") or etud.get("commentaire"):
            rap = "Note du rapporteur"
            if etud.get("rapporteur"):
                rap += " (%s)" % etud["rapporteur"]
            rap += ": "
            if etud.get("commentaire"):
                rap += "<em>%s</em>" % etud["commentaire"]
        etud["rap"] = rap

        if etud.get("telephone"):
            etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
        else:
            etud["telephonestr"] = ""
        if etud.get("telephonemobile"):
            etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
                etud["telephonemobile"]
            )
        else:
            etud["telephonemobilestr"] = ""


def etud_inscriptions_infos(etudid: int, ne="") -> dict:
    """Dict avec les informations sur les semestres passés et courant.
    {
        "sems" : , # trie les semestres par date de debut, le plus recent d'abord
        "ins" : ,
        "cursem" : ,
        "inscription" : ,
        "inscriptionstr" : ,
        "inscription_formsemestre_id" : ,
        "etatincursem" : ,
        "situation" : ,
    }
    """
    from app.scodoc import sco_formsemestre
    from app.scodoc import sco_formsemestre_inscriptions

    etud = {}
    # Semestres dans lesquel il est inscrit
    ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
        {"etudid": etudid}
    )
    etud["ins"] = ins
    sems = []
    cursem = None  # semestre "courant" ou il est inscrit
    for i in ins:
        sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"])
        if sco_formsemestre.sem_est_courant(sem):
            cursem = sem
            curi = i
        sem["ins"] = i
        sems.append(sem)
    # trie les semestres par date de debut, le plus recent d'abord
    # (important, ne pas changer (suivi cohortes))
    sems.sort(key=itemgetter("dateord"), reverse=True)
    etud["sems"] = sems
    etud["cursem"] = cursem
    if cursem:
        etud["inscription"] = cursem["titremois"]
        etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
        etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
        etud["etatincursem"] = curi["etat"]
        etud["situation"] = descr_situation_etud(etudid, ne)
    else:
        if etud["sems"]:
            if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()):
                etud["inscription"] = "futur"
                etud["situation"] = "futur élève"
            else:
                etud["inscription"] = "ancien"
                etud["situation"] = "ancien élève"
        else:
            etud["inscription"] = "non inscrit"
            etud["situation"] = etud["inscription"]
        etud["inscriptionstr"] = etud["inscription"]
        etud["inscription_formsemestre_id"] = None
        etud["etatincursem"] = "?"
    return etud


def descr_situation_etud(etudid: int, ne="") -> str:
    """Chaîne décrivant la situation actuelle de l'étudiant
    XXX Obsolete, utiliser Identite.descr_situation_etud() dans
        les nouveaux codes
    """
    from app.scodoc import sco_formsemestre

    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        """SELECT I.formsemestre_id, I.etat 
        FROM notes_formsemestre_inscription I,  notes_formsemestre S 
        WHERE etudid=%(etudid)s 
        and S.id = I.formsemestre_id 
        and date_debut < now() 
        and date_fin > now() 
        ORDER BY S.date_debut DESC;""",
        {"etudid": etudid},
    )
    r = cursor.dictfetchone()
    if not r:
        situation = "non inscrit" + ne
    else:
        sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"])
        if r["etat"] == scu.INSCRIT:
            situation = "inscrit%s en %s" % (ne, sem["titremois"])
            # Cherche la date d'inscription dans scolar_events:
            events = scolar_events_list(
                cnx,
                args={
                    "etudid": etudid,
                    "formsemestre_id": sem["formsemestre_id"],
                    "event_type": "INSCRIPTION",
                },
            )
            if not events:
                log(
                    "*** situation inconsistante pour %s (inscrit mais pas d'event)"
                    % etudid
                )
                date_ins = "???"  # ???
            else:
                date_ins = events[0]["event_date"]
            situation += " le " + str(date_ins)
        else:
            situation = "démission de %s" % sem["titremois"]
            # Cherche la date de demission dans scolar_events:
            events = scolar_events_list(
                cnx,
                args={
                    "etudid": etudid,
                    "formsemestre_id": sem["formsemestre_id"],
                    "event_type": "DEMISSION",
                },
            )
            if not events:
                log(
                    "*** situation inconsistante pour %s (demission mais pas d'event)"
                    % etudid
                )
                date_dem = "???"  # ???
            else:
                date_dem = events[0]["event_date"]
            situation += " le " + str(date_dem)
    return situation