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

##############################################################################
#
# Gestion scolarite IUT
#
# 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
#
##############################################################################

"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
"""
import collections
from operator import attrgetter

import flask
from flask import url_for, g, request
from flask_login import current_user

from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
    FormSemestre,
    Identite,
    ModuleImpl,
    Partition,
    ScolarFormSemestreValidation,
    UniteEns,
)
from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
from app.tables import list_etuds


def moduleimpl_inscriptions_edit(
    moduleimpl_id, etudids: list[int] | None = None, submitted=False
):
    """Formulaire inscription des etudiants a ce module
    * Gestion des inscriptions
         Nom          TD     TA    TP  (triable)
     [x] M. XXX YYY   -      -     -


     ajouter TD A, TD B, TP 1, TP 2 ...
     supprimer TD A, TD B, TP 1, TP 2 ...

     * Si pas les droits: idem en readonly
    """
    etudids = etudids or []
    modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
    module = modimpl.module
    formsemestre = modimpl.formsemestre
    # -- check lock
    if not formsemestre.etat:
        raise ScoValueError("opération impossible: semestre verrouille")
    header = html_sco_header.sco_header(
        page_title="Inscription au module",
        init_qtip=True,
        javascripts=["js/etud_info.js"],
    )
    footer = html_sco_header.sco_footer()
    H = [
        header,
        f"""<h2>Inscriptions au module <a class="stdlink" href="{
            url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
                moduleimpl_id=moduleimpl_id)
        }">{module.titre or "(module sans titre)"}</a> ({module.code})</a></h2>
    <p class="help">Cette page permet d'éditer les étudiants inscrits à ce module
    (ils doivent évidemment être inscrits au semestre).
    Les étudiants cochés sont (ou seront) inscrits. Vous pouvez inscrire ou
    désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
    </p>
    <p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas
    sur le bouton "Appliquer les modifications".
    </p>
    """,
    ]
    # Liste des inscrits à ce semestre
    inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
        formsemestre.id
    )
    for ins in inscrits:
        etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
        if not etuds_info:
            log(
                f"""moduleimpl_inscriptions_edit: inconsistency for etudid={ins['etudid']} !"""
            )
            raise ScoValueError(
                f"""Étudiant {ins['etudid']} inscrit mais inconnu dans la base !"""
            )
        ins["etud"] = etuds_info[0]
    inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
    in_m = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
    in_module = {x["etudid"] for x in in_m}
    #
    partitions = sco_groups.get_partitions_list(formsemestre.id)
    #
    if not submitted:
        H.append(
            """<script type="text/javascript">
    function group_select(groupName, partitionIdx, check) {
    var nb_inputs_to_skip = 2; // nb d'input avant les checkbox !!!
    var elems = document.getElementById("mi_form").getElementsByTagName("input");

    if (partitionIdx==-1) {
      for (var i =nb_inputs_to_skip; i < elems.length; i++) {
         elems[i].checked=check;
      }
    } else {
     for (var i =nb_inputs_to_skip; i < elems.length; i++) {
       var cells = elems[i].parentNode.parentNode.getElementsByTagName("td")[partitionIdx].childNodes;
       if (cells.length && cells[0].nodeValue == groupName) {
          elems[i].checked=check;
       }
     }
    }
    }

    </script>
    <style>
    table.mi_table td, table.mi_table th {
        text-align: left;
    }
    </style>
    """
        )
        H.append(
            f"""<form method="post" id="mi_form" action="{request.base_url}">
        <input type="hidden" name="moduleimpl_id" value="{modimpl.id}"/>
        <input type="submit" name="submitted" value="Appliquer les modifications"/>
        <div>
            { _make_menu(partitions, "Ajouter", "true") }
            { _make_menu(partitions, "Enlever", "false")}
        </div>
        <table class="gt_table mi_table">
        <thead>
        <tr>
            <th class="etud">Nom</th>
        """
        )
        for partition in partitions:
            if partition["partition_name"]:
                H.append(f"<th>{partition['partition_name']}</th>")
        H.append("</tr></thead><tbody>")

        for ins in inscrits:
            etud = ins["etud"]
            if etud["etudid"] in in_module:
                checked = 'checked="checked"'
            else:
                checked = ""
            H.append(
                f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>"""
            )
            H.append(
                f"""<a class="discretelink etudinfo" href="{
                    url_for(
                        "scolar.fiche_etud",
                        scodoc_dept=g.scodoc_dept,
                        etudid=etud["etudid"],
                    )
                    }" id="{etud['etudid']}">{etud['nomprenom']}</a>"""
            )
            H.append("""</input></td>""")

            groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id)
            for partition in partitions:
                if partition["partition_name"]:
                    gr_name = ""
                    for group in groups:
                        if group["partition_id"] == partition["partition_id"]:
                            gr_name = group["group_name"]
                            break
                    # gr_name == '' si etud non inscrit dans un groupe de cette partition
                    H.append(f"<td>{gr_name}</td>")
        H.append("""</tbody></table></form>""")
    else:  # SUBMISSION
        # inscrit a ce module tous les etuds selectionnes
        sco_moduleimpl.do_moduleimpl_inscrit_etuds(
            moduleimpl_id, formsemestre.id, etudids, reset=True
        )
        return flask.redirect(
            url_for(
                "notes.moduleimpl_status",
                scodoc_dept=g.scodoc_dept,
                moduleimpl_id=moduleimpl_id,
            )
        )
    #
    H.append(footer)
    return "\n".join(H)


def _make_menu(partitions: list[dict], title="", check="true") -> str:
    """Menu with list of all groups"""
    items = [{"title": "Tous", "attr": f"onclick=\"group_select('', -1, {check})\""}]
    p_idx = 0
    for partition in partitions:
        if partition["partition_name"] is not None:
            p_idx += 1
            for group in sco_groups.get_partition_groups(partition):
                items.append(
                    {
                        "title": "%s %s"
                        % (partition["partition_name"], group["group_name"]),
                        "attr": "onclick=\"group_select('%s', %s, %s)\""
                        % (group["group_name"], p_idx, check),
                    }
                )
    return (
        '<div class="inscr_addremove_menu">'
        + htmlutils.make_menu(title, items, alone=True)
        + "</div>"
    )


def moduleimpl_inscriptions_stats(formsemestre_id):
    """Affiche quelques informations sur les inscriptions
    aux modules de ce semestre.

    Inscrits au semestre: <nb>

    Modules communs (tous inscrits): <liste des modules (codes)

    Autres modules: (regroupés par UE)
    UE 1
    <code du module>: <nb inscrits> (<description en termes de groupes>)
    ...

    En APC, n'affiche pas la colonne UE, car le rattachement n'a pas
    d'importance pédagogique.

    descriptions:
      groupes de TD A, B et C
      tous sauf groupe de TP Z (?)
      tous sauf <liste d'au plus 7 noms>

    """
    authuser = current_user
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    is_apc = formsemestre.formation.is_apc()
    inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
        args={"formsemestre_id": formsemestre_id}
    )
    set_all = set([x["etudid"] for x in inscrits])
    partitions, _ = sco_groups.get_formsemestre_groups(formsemestre_id)

    can_change = authuser.has_permission(Permission.EtudInscrit) and formsemestre.etat

    # Décrit les inscriptions aux modules:
    commons = []  # modules communs a tous les etuds du semestre
    options = []  # modules ou seuls quelques etudiants sont inscrits
    mod_description = {}  # modimplid : str
    mod_nb_inscrits = {}  # modimplid : int
    if is_apc:
        modimpls = sorted(formsemestre.modimpls, key=lambda m: m.module.sort_key_apc())
    else:
        modimpls = formsemestre.modimpls_sorted
    for modimpl in modimpls:
        tous_inscrits, nb_inscrits, descr = descr_inscrs_module(
            modimpl.id,
            set_all,
            partitions,
        )
        if tous_inscrits:
            commons.append(modimpl)
        else:
            mod_description[modimpl.id] = descr
            mod_nb_inscrits[modimpl.id] = nb_inscrits
            options.append(modimpl)

    # Page HTML:
    H = [
        html_sco_header.html_sem_header(
            "Inscriptions aux modules et UE du semestre",
            javascripts=["js/etud_info.js", "js/moduleimpl_inscriptions_stats.js"],
            init_qtip=True,
        )
    ]

    H.append(f"<h3>Inscrits au semestre: {len(inscrits)} étudiants</h3>")

    if options:
        H.append("<h3>Modules auxquels tous les étudiants ne sont pas inscrits:</h3>")
        H.append(
            f"""<table class="formsemestre_status formsemestre_inscr">
            <tr>
            {'<th>UE</th>' if not is_apc else ""}
            <th>Code</th>
            <th>Module</th>
            <th>Inscrits</th>
            <th></th>
            </tr>
            """
        )
        for modimpl in options:
            if can_change:
                c_link = f"""<a class="discretelink" href="{url_for(
                    'notes.moduleimpl_inscriptions_edit',
                    scodoc_dept=g.scodoc_dept,
                    moduleimpl_id=modimpl.id)
                    }">{mod_description[modimpl.id] or "<i>(inscrire des étudiants)</i>"}</a>
                """
            else:
                c_link = mod_description[modimpl.id]
            H.append("""<tr class="formsemestre_status">""")
            if not is_apc:
                H.append(
                    f"""
                    <td>{
                        modimpl.module.ue.acronyme or ""
                    }</td>
                    """
                )
            H.append(
                f"""
                <td class="formsemestre_status_code">{
                    modimpl.module.code or "(module sans code)"
                }</td>
                <td class="formsemestre_status_module">{modimpl.module.titre or ""}</td>
                <td class="formsemestre_status_inscrits">{
                    mod_nb_inscrits[modimpl.id]}</td><td>{c_link}</td>
                </tr>
                """
            )
        H.append("</table>")
    else:
        H.append(
            """<span style="font-size:110%; font-style:italic; color: red;"
            >Tous les étudiants sont inscrits à tous les modules.</span>"""
        )

    if commons:
        H.append(
            f"""<h3>Modules communs (auxquels tous les étudiants sont inscrits):</h3>

            <table class="formsemestre_status formsemestre_inscr">
            <tr>
            {'<th>UE</th>' if not is_apc else ""}
            <th>Code</th>
            <th>Module</th>"""
        )
        if is_apc:
            H.append("<th>Parcours</th>")
        H.append("""</tr>""")
        for modimpl in commons:
            if can_change:
                c_link = f"""<a class="discretelink" href="{
                    url_for("notes.moduleimpl_inscriptions_edit",
                    scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
                    }">{modimpl.module.titre}</a>"""
            else:
                c_link = modimpl.module.titre
            H.append("""<tr class="formsemestre_status_green">""")
            if not is_apc:
                H.append(
                    f"""
                <td>{modimpl.module.ue.acronyme or ""}</td>
                """
                )
            H.append(
                f"""
                <td class="formsemestre_status_code">{
                    modimpl.module.code or "(module sans code)"
                }</td><td>{c_link}</td>"""
            )
            if is_apc:
                H.append(
                    f"""<td><em>{', '.join(p.code for p in modimpl.module.parcours)}</em></td>"""
                )
            H.append("</tr>")
        H.append("</table>")

    # Etudiants "dispensés" d'une UE (capitalisée)
    ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id)
    if ues_cap_info:
        H.append(
            '<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
        )
        ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
        ues.sort(key=lambda u: u.numero)
        for ue in ues:
            H.append(
                f"""<li class="tit"><span class="tit">{ue.acronyme}: {ue.titre or ''}</span>"""
            )
            H.append("<ul>")
            for info in ues_cap_info[ue.id]:
                etud = Identite.get_etud(info["etudid"])
                H.append(
                    f"""<li class="etud"><a class="discretelink etudinfo"
                    id="{etud.id}"
                    href="{
                        url_for(
                            "scolar.fiche_etud",
                            scodoc_dept=g.scodoc_dept,
                            etudid=etud.id,
                        )
                    }">{etud.nomprenom}</a>"""
                )
                if info["ue_status"]["event_date"]:
                    H.append(
                        f"""(cap. le {info["ue_status"]["event_date"].strftime(scu.DATE_FMT)})"""
                    )
                if is_apc:
                    is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
                else:
                    # CLASSIQUE
                    is_inscrit_ue = info["is_ins"]
                    if is_inscrit_ue:
                        dm = ", ".join(
                            [
                                m["code"] or m["abbrev"] or "pas_de_code"
                                for m in info["is_ins"]
                            ]
                        )
                        H.append(
                            f"""actuellement inscrit dans <a title="{dm}" class="discretelink"
                            >{len(info["is_ins"])} modules</a>"""
                        )
                if is_inscrit_ue:
                    if info["ue_status"]["is_capitalized"]:
                        H.append(
                            """<div><em style="font-size: 70%">UE actuelle moins bonne que
                            l'UE capitalisée</em>
                            </div>"""
                        )
                    else:
                        H.append(
                            """<div><em style="font-size: 70%">UE actuelle meilleure que
                            l'UE capitalisée</em>
                            </div>"""
                        )
                    if can_change:
                        H.append(
                            f"""<div><a class="stdlink" href="{
                                url_for("notes.etud_desinscrit_ue",
                                scodoc_dept=g.scodoc_dept, etudid=etud.id,
                                formsemestre_id=formsemestre_id, ue_id=ue.id)
                            }">désinscrire {"des modules" if not is_apc else ""} de cette UE</a></div>
                            """
                        )
                else:
                    H.append("(non réinscrit dans cette UE)")
                    if can_change:
                        H.append(
                            f"""<div><a class="stdlink" href="{
                                url_for("notes.etud_inscrit_ue",
                                    scodoc_dept=g.scodoc_dept, etudid=etud.id,
                                    formsemestre_id=formsemestre_id, ue_id=ue.id)
                            }">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
                            """
                        )
                H.append("</li>")
            H.append("</ul></li>")
        H.append("</ul>")
    # BUT: propose dispense de toutes UEs
    if is_apc:
        H.append(_list_but_ue_inscriptions(res, read_only=not can_change))

    H.append(
        """<hr/><p class="help">Cette page décrit les inscriptions actuelles.
    Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
    cliquant sur la ligne du module.</p>
    <p  class="help">Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
    l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.</p>
    """
    )

    H.append(html_sco_header.sco_footer())
    return "\n".join(H)


def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) -> str:
    """HTML pour dispenser/reinscrire chaque étudiant à chaque UE du BUT"""
    H = [
        """
    <div class="list_but_ue_inscriptions">
    <h3>Inscriptions/déinscription aux UEs du BUT</h3>
    <form class="list_but_ue_inscriptions">
    """
    ]
    table_inscr = _table_but_ue_inscriptions(res)
    ue_ids = (
        set.union(*(set(x.keys()) for x in table_inscr.values()))
        if table_inscr
        else set()
    )
    ues = sorted(
        (db.session.get(UniteEns, ue_id) for ue_id in ue_ids),
        key=lambda u: (u.numero or 0, u.acronyme),
    )
    H.append(
        """<table id="but_ue_inscriptions" class="stripe compact">
        <thead>
        <tr><th>Nom</th><th>Parcours</th>
        """
    )
    for ue in ues:
        H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""")
    H.append(
        """</tr>
        </thead>
        <tbody>
    """
    )
    partition_parcours: Partition = Partition.query.filter_by(
        formsemestre=res.formsemestre, partition_name=scu.PARTITION_PARCOURS
    ).first()
    etuds = list_etuds.etuds_sorted_from_ids(table_inscr.keys())
    for etud in etuds:
        ues_etud = table_inscr[etud.id]
        H.append(
            f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
            href="{url_for(
                        "scolar.fiche_etud",
                        scodoc_dept=g.scodoc_dept,
                        etudid=etud.id,
                    )}"
        >{etud.nomprenom}</a></td>"""
        )
        # Parcours:
        if partition_parcours:
            group = partition_parcours.get_etud_group(etud.id)
            parcours_name = group.group_name if group else ""
        else:
            parcours_name = ""
        H.append(f"""<td class="parcours">{parcours_name}</td>""")
        # UEs:
        for ue in ues:
            td_class = ""
            est_inscr = ues_etud.get(ue.id)  # None si pas concerné
            if est_inscr is None:
                content = ""
            else:
                # Validations d'UE déjà enregistrées dans d'autres semestres
                validations_ue = (
                    ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
                    .filter(
                        ScolarFormSemestreValidation.formsemestre_id
                        != res.formsemestre.id,
                        ScolarFormSemestreValidation.code.in_(
                            codes_cursus.CODES_UE_VALIDES
                        ),
                    )
                    .join(UniteEns)
                    .filter_by(ue_code=ue.ue_code)
                    .all()
                )
                validations_ue.sort(
                    key=lambda v: codes_cursus.BUT_CODES_ORDER.get(v.code, 0)
                )
                validation = validations_ue[-1] if validations_ue else None
                expl_validation = (
                    f"""Validée ({validation.code}) le {
                            validation.event_date.strftime(scu.DATE_FMT)}"""
                    if validation
                    else ""
                )
                td_class = ' class="ue_validee"' if validation else ""
                content = f"""<input type="checkbox"
                    {'checked' if est_inscr else ''}
                    {'disabled' if read_only else ''}
                    title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}"
                    onchange="change_ue_inscr(this);"
                    data-url_inscr="{
                        url_for("notes.etud_inscrit_ue",
                            scodoc_dept=g.scodoc_dept, etudid=etud.id,
                            formsemestre_id=res.formsemestre.id, ue_id=ue.id)
                    }"
                    data-url_desinscr="{
                        url_for("notes.etud_desinscrit_ue",
                            scodoc_dept=g.scodoc_dept, etudid=etud.id,
                            formsemestre_id=res.formsemestre.id, ue_id=ue.id)
                    }"
                    />
                """

            H.append(f"""<td{td_class}>{content}</td>""")
    H.append(
        """
    </tbody>
    </table>
    </form>
    <div class="help">
    <p>L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
    mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
    </p>
    <p>Il peut s'agir d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
    présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
    cas particuliers.
    </p>
    <p>La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
    et n'affecte pas les notes saisies.
    </p>
    </div>
    </div>
    """
    )
    return "\n".join(H)


def _table_but_ue_inscriptions(res: NotesTableCompat) -> dict[int, dict]:
    """ "table" avec les inscriptions aux UEs de chaque étudiant
    {
        etudid : { ue_id : True | False }
    }
    """
    return {
        etudid: {
            ue_id: (etudid, ue_id) not in res.dispense_ues
            for ue_id in res.etud_ues_ids(etudid)
        }
        for etudid, inscr in res.formsemestre.etuds_inscriptions.items()
        if inscr.etat == scu.INSCRIT
    }


def descr_inscrs_module(moduleimpl_id, set_all, partitions):
    """returns tous_inscrits, nb_inscrits, descr"""
    ins = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
    set_m = set([x["etudid"] for x in ins])  # ens. des inscrits au module
    non_inscrits = set_all - set_m
    if len(non_inscrits) == 0:
        return True, len(ins), ""  # tous inscrits
    if len(non_inscrits) <= 7:  # seuil arbitraire
        return False, len(ins), "tous sauf " + _fmt_etud_set(non_inscrits)
    # Cherche les groupes:
    gr = []  #  [ ( partition_name , [ group_names ] ) ]
    for partition in partitions:
        grp = []  # groupe de cette partition
        for group in sco_groups.get_partition_groups(partition):
            members = sco_groups.get_group_members(group["group_id"])
            set_g = set([m["etudid"] for m in members])
            if set_g.issubset(set_m):
                grp.append(group["group_name"])
                set_m = set_m - set_g
        gr.append((partition["partition_name"], grp))
    #
    d = []
    for partition_name, grp in gr:
        if grp:
            d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
    r = []
    if d:
        r.append(", ".join(d))
    if set_m:
        r.append(_fmt_etud_set(set_m))
    #
    return False, len(ins), " et ".join(r)


def _fmt_etud_set(etudids, max_list_size=7) -> str:
    # max_list_size est le nombre max de noms d'etudiants listés
    # au delà, on indique juste le nombre, sans les noms.
    if len(etudids) > max_list_size:
        return f"{len(etudids)} étudiants"
    etuds = []
    for etudid in etudids:
        etud = db.session.get(Identite, etudid)
        if etud:
            etuds.append(etud)

    return ", ".join(
        [
            f"""<a class="discretelink" href="{
                url_for(
                    "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
                )
            }">{etud.nomprenom}</a>"""
            for etud in sorted(etuds, key=attrgetter("sort_key"))
        ]
    )


def get_etuds_with_capitalized_ue(formsemestre_id: int) -> list[dict]:
    """For each UE, computes list of students capitalizing the UE.
    returns { ue_id : [ { infos } ] }
    """
    ues_cap_info = collections.defaultdict(list)
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

    inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
        args={"formsemestre_id": formsemestre_id}
    )
    ues = nt.get_ues_stat_dict()
    for ue in ues:
        for etud in inscrits:
            ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
            if ue_status and ue_status["was_capitalized"]:
                ues_cap_info[ue["ue_id"]].append(
                    {
                        "etudid": etud["etudid"],
                        "ue_status": ue_status,
                        "is_ins": etud_modules_ue_inscr(
                            etud["etudid"], formsemestre_id, ue["ue_id"]
                        ),
                    }
                )
    return ues_cap_info


def etud_modules_ue_inscr(etudid, formsemestre_id, ue_id) -> list[int]:
    """Modules de cette UE dans ce semestre
    auxquels l'étudiant est inscrit.
    Utile pour formations classiques seulement.
    """
    r = ndb.SimpleDictFetch(
        """SELECT mod.id AS module_id, mod.*
    FROM notes_moduleimpl mi, notes_modules mod,
         notes_formsemestre sem, notes_moduleimpl_inscription i
    WHERE sem.id = %(formsemestre_id)s
    AND mi.formsemestre_id = sem.id
    AND mod.id = mi.module_id
    AND mod.ue_id = %(ue_id)s
    AND i.moduleimpl_id = mi.id
    AND i.etudid = %(etudid)s
    ORDER BY mod.numero
    """,
        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
    )
    return r


def do_etud_desinscrit_ue_classic(etudid, formsemestre_id, ue_id):
    """Désinscrit l'etudiant de tous les modules de cette UE dans ce semestre.
    N'utiliser que pour les formations classiques, pas APC.
    """
    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        """DELETE FROM notes_moduleimpl_inscription
    WHERE id IN (
      SELECT i.id FROM
        notes_moduleimpl mi, notes_modules mod,
        notes_formsemestre sem, notes_moduleimpl_inscription i
      WHERE sem.id = %(formsemestre_id)s
      AND mi.formsemestre_id = sem.id
      AND mod.id = mi.module_id
      AND mod.ue_id = %(ue_id)s
      AND i.moduleimpl_id = mi.id
      AND i.etudid = %(etudid)s
    )
    """,
        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
    )
    logdb(
        cnx,
        method="etud_desinscrit_ue",
        etudid=etudid,
        msg=f"desinscription UE {ue_id}",
        commit=False,
    )
    sco_cache.invalidate_formsemestre(
        formsemestre_id=formsemestre_id
    )  # > desinscription etudiant des modules


def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
    """Incrit l'etudiant de tous les modules de cette UE dans ce semestre."""
    # Verifie qu'il est bien inscrit au semestre
    insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
        args={"formsemestre_id": formsemestre_id, "etudid": etudid}
    )
    if not insem:
        raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)

    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        """SELECT mi.id
      FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
      WHERE sem.id = %(formsemestre_id)s
      AND mi.formsemestre_id = sem.id
      AND mod.id = mi.module_id
      AND mod.ue_id = %(ue_id)s
     """,
        {"formsemestre_id": formsemestre_id, "ue_id": ue_id},
    )
    res = cursor.dictfetchall()
    for moduleimpl_id in [x["id"] for x in res]:
        sco_moduleimpl.do_moduleimpl_inscription_create(
            {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
            formsemestre_id=formsemestre_id,
        )