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

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

"""Calculs sur les notes et cache des résultats

    Ancien code ScoDoc 7 en cours de rénovation 
"""

from operator import itemgetter

from flask import g, url_for

from app.but import bulletin_but
from app.models import FormSemestre, Identite
from app.models import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_formulas import NoteVector
from app.scodoc.sco_exceptions import ScoValueError

from app.scodoc.sco_formsemestre import (
    formsemestre_uecoef_list,
    formsemestre_uecoef_create,
)
from app.scodoc.sco_codes_parcours import (
    DEF,
    UE_SPORT,
    UE_is_fondamentale,
    UE_is_professionnelle,
)
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_compute_moy
from app.scodoc import sco_cache
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_preferences
from app.scodoc import sco_etud


def comp_ranks(T):
    """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ]
    (valeur est une note numérique), en tenant compte des ex-aequos
    Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
    """
    rangs = {}  # { etudid : rang } (rang est une chaine)
    nb_ex = 0  # nb d'ex-aequo consécutifs en cours
    for i in range(len(T)):
        # test ex-aequo
        if i < len(T) - 1:
            next = T[i + 1][0]
        else:
            next = None
        moy = T[i][0]
        if nb_ex:
            srang = "%d ex" % (i + 1 - nb_ex)
            if moy == next:
                nb_ex += 1
            else:
                nb_ex = 0
        else:
            if moy == next:
                srang = "%d ex" % (i + 1 - nb_ex)
                nb_ex = 1
            else:
                srang = "%d" % (i + 1)
        rangs[T[i][-1]] = srang  # str(i+1)
    return rangs


def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
    """Get liste des UE du semestre (à partir des moduleimpls)
    (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
    """
    if modimpls is None:
        modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
    uedict = {}
    for modimpl in modimpls:
        mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
        modimpl["module"] = mod
        if not mod["ue_id"] in uedict:
            ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
            uedict[ue["ue_id"]] = ue
    ues = list(uedict.values())
    ues.sort(key=lambda u: u["numero"])
    return ues, modimpls


def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
    """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
    ou None s'il n'y a aucun module.

    (nécessaire pour éviter appels récursifs de nt, qui peuvent boucler)
    """
    infos = ndb.SimpleDictFetch(
        """SELECT mod.coefficient
    FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
    WHERE mod.id = mi.module_id
    and ins.etudid = %(etudid)s
    and ins.moduleimpl_id = mi.id
    and mi.formsemestre_id = %(formsemestre_id)s
    and mod.ue_id = %(ue_id)s
    """,
        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
    )

    if not infos:
        return None
    else:
        s = sum(x["coefficient"] for x in infos)
        return s


class NotesTable:
    """Une NotesTable représente un tableau de notes pour un semestre de formation.
    Les colonnes sont des modules.
    Les lignes des étudiants.
    On peut calculer les moyennes par étudiant (pondérées par les coefs)
    ou les moyennes par module.

    Attributs publics (en lecture):
    - inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
    - identdict: { etudid : ident }
    - sem : le formsemestre
    get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ]
    (où toutes les valeurs sont soit des nombres soit des chaines spéciales comme 'NA', 'NI'),
    incluant les UE de sport

    - bonus[etudid] : valeur du bonus "sport".

    Attributs privés:
    - _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
    - _ues : liste des UE de ce semestre (hors capitalisees)
    - _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } }

    """

    def __init__(self, formsemestre_id):
        # log(f"NotesTable( formsemestre_id={formsemestre_id} )")
        raise NotImplementedError()  # XXX
        if not formsemestre_id:
            raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
        self.formsemestre_id = formsemestre_id
        cnx = ndb.GetDBConnexion()
        self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
        self.moduleimpl_stats = {}  # { moduleimpl_id : {stats} }
        self._uecoef = {}  # { ue_id : coef } cache coef manuels ue cap
        self._evaluations_etats = None  # liste des evaluations avec état
        self.use_ue_coefs = sco_preferences.get_preference(
            "use_ue_coefs", formsemestre_id
        )
        # si vrai, bloque calcul des moy gen. et d'UE.:
        self.block_moyennes = self.sem["block_moyennes"]
        # Infos sur les etudiants
        self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
            args={"formsemestre_id": formsemestre_id}
        )
        # infos identite etudiant
        # xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 !
        self.identdict = {}  # { etudid : ident }
        self.inscrdict = {}  # { etudid : inscription }
        for x in self.inscrlist:
            i = sco_etud.etudident_list(cnx, {"etudid": x["etudid"]})[0]
            self.identdict[x["etudid"]] = i
            self.inscrdict[x["etudid"]] = x
            x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"]  # pour tri

        # Tri les etudids par NOM
        self.inscrlist.sort(key=itemgetter("nomp"))

        # { etudid : rang dans l'ordre alphabetique }
        self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}

        self.bonus = scu.DictDefault(defaultvalue=0)
        # Notes dans les modules  { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
        (
            self._modmoys,
            self._modimpls,
            self._valid_evals_per_mod,
            valid_evals,
            mods_att,
            self.expr_diagnostics,
        ) = sco_compute_moy.formsemestre_compute_modimpls_moyennes(
            self, formsemestre_id
        )
        self._mods_att = mods_att  # liste des modules avec des notes en attente
        self._matmoys = {}  # moyennes par matieres
        self._valid_evals = {}  # { evaluation_id : eval }
        for e in valid_evals:
            self._valid_evals[e["evaluation_id"]] = e  # Liste des modules et UE
        uedict = {}  # public member: { ue_id : ue }
        self.uedict = uedict  # les ues qui ont un modimpl dans ce semestre
        for modimpl in self._modimpls:
            # module has been added by formsemestre_compute_modimpls_moyennes
            mod = modimpl["module"]
            if not mod["ue_id"] in uedict:
                ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
                uedict[ue["ue_id"]] = ue
            else:
                ue = uedict[mod["ue_id"]]
            modimpl["ue"] = ue  # add ue dict to moduleimpl
            self._matmoys[mod["matiere_id"]] = {}
            mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
                0
            ]
            modimpl["mat"] = mat  # add matiere dict to moduleimpl
            # calcul moyennes du module et stocke dans le module
            # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=

        self.formation = sco_formations.formation_list(
            args={"formation_id": self.sem["formation_id"]}
        )[0]
        self.parcours = sco_codes_parcours.get_parcours_from_code(
            self.formation["type_parcours"]
        )

        # En APC, il faut avoir toutes les UE du semestre
        # (elles n'ont pas nécessairement un module rattaché):
        if self.parcours.APC_SAE:
            formsemestre = FormSemestre.query.get(formsemestre_id)
            for ue in formsemestre.query_ues():
                if ue.id not in self.uedict:
                    self.uedict[ue.id] = ue.to_dict()

        # Decisions jury et UE capitalisées
        self.comp_decisions_jury()
        self.comp_ue_capitalisees()

        # Liste des moyennes de tous, en chaines de car., triées
        self._ues = list(uedict.values())
        self._ues.sort(key=lambda u: u["numero"])

        T = []

        self.moy_gen = {}  # etudid : moy gen (avec UE capitalisées)
        self.moy_ue = {}  # ue_id : { etudid : moy ue } (valeur numerique)
        self.etud_moy_infos = {}  # etudid : resultats de comp_etud_moy_gen()
        valid_moy = []  # liste des valeurs valides de moyenne generale (pour min/max)
        for ue in self._ues:
            self.moy_ue[ue["ue_id"]] = {}
        self._etud_moy_ues = {}  # { etudid : { ue_id : {'moy', 'sum_coefs', ... } }

        for etudid in self.get_etudids():
            etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx)
            self.etud_moy_infos[etudid] = etud_moy_gen
            ue_status = etud_moy_gen["moy_ues"]
            self._etud_moy_ues[etudid] = ue_status

            moy_gen = etud_moy_gen["moy"]
            self.moy_gen[etudid] = moy_gen
            if etud_moy_gen["sum_coefs"] > 0:
                valid_moy.append(moy_gen)

            moy_ues = []
            for ue in self._ues:
                moy_ue = ue_status[ue["ue_id"]]["moy"]
                moy_ues.append(moy_ue)
                self.moy_ue[ue["ue_id"]][etudid] = moy_ue

            t = [moy_gen] + moy_ues
            #
            is_cap = {}  # ue_id : is_capitalized
            for ue in self._ues:
                is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]

            for modimpl in self.get_modimpls_dict():
                val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
                if is_cap[modimpl["module"]["ue_id"]]:
                    t.append("-c-")
                else:
                    t.append(val)
            #
            t.append(etudid)
            T.append(t)

        self.T = T
        # tri par moyennes décroissantes,
        # en laissant les demissionnaires a la fin, par ordre alphabetique
        self.T.sort(key=self._row_key)

        if len(valid_moy):
            self.moy_min = min(valid_moy)
            self.moy_max = max(valid_moy)
        else:
            self.moy_min = self.moy_max = "NA"

        # calcul rangs (/ moyenne generale)
        self.etud_moy_gen_ranks = comp_ranks(T)

        self.rangs_groupes = (
            {}
        )  # { group_id : { etudid : rang } }  (lazy, see get_etud_rang_group)
        self.group_etuds = (
            {}
        )  # { group_id : set of etudids } (lazy, see get_etud_rang_group)

        # calcul rangs dans chaque UE
        ue_rangs = (
            {}
        )  # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine)
        for ue in self._ues:
            ue_id = ue["ue_id"]
            val_ids = [
                (self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id]
            ]
            ue_eff = len(
                [x for x in val_ids if isinstance(x[0], float)]
            )  # nombre d'étudiants avec une note dans l'UE
            val_ids.sort(key=self._row_key)
            ue_rangs[ue_id] = (
                comp_ranks(val_ids),
                ue_eff,
            )  # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo
        self.ue_rangs = ue_rangs
        # ---- calcul rangs dans les modules
        self.mod_rangs = {}
        for modimpl in self._modimpls:
            vals = self._modmoys[modimpl["moduleimpl_id"]]
            val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
            val_ids.sort(key=self._row_key)
            self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
        #
        self.compute_moy_moy()
        #
        log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")

    def _row_key(self, x):
        """clé de tri par moyennes décroissantes,
        en laissant les demissionnaires a la fin, par ordre alphabetique.
        (moy_gen, rang_alpha)
        """
        try:
            moy = -float(x[0])
        except (ValueError, TypeError):
            moy = 1000.0
        return (moy, self._rang_alpha[x[-1]])

    def get_etudids(self, sorted=False):
        if sorted:
            # Tri par moy. generale décroissante
            return [x[-1] for x in self.T]
        else:
            # Tri par ordre alphabetique de NOM
            return [x["etudid"] for x in self.inscrlist]

    def get_sexnom(self, etudid):
        "M. DUPONT"
        etud = self.identdict[etudid]
        return etud["civilite_str"] + " " + (etud["nom_usuel"] or etud["nom"]).upper()

    def get_nom_short(self, etudid):
        "formatte nom d'un etud (pour table recap)"
        etud = self.identdict[etudid]
        # Attention aux caracteres multibytes pour decouper les 2 premiers:
        return (
            (etud["nom_usuel"] or etud["nom"]).upper()
            + " "
            + etud["prenom"].capitalize()[:2]
            + "."
        )

    def get_nom_long(self, etudid):
        "formatte nom d'un etud:  M. Pierre DUPONT"
        etud = self.identdict[etudid]
        return sco_etud.format_nomprenom(etud)

    def get_displayed_etud_code(self, etudid):
        'code à afficher sur les listings "anonymes"'
        return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"]

    def get_etud_etat(self, etudid):
        "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
        if etudid in self.inscrdict:
            return self.inscrdict[etudid]["etat"]
        else:
            return ""

    def get_etud_etat_html(self, etudid):
        etat = self.inscrdict[etudid]["etat"]
        if etat == "I":
            return ""
        elif etat == "D":
            return ' <font color="red">(DEMISSIONNAIRE)</font> '
        elif etat == DEF:
            return ' <font color="red">(DEFAILLANT)</font> '
        else:
            return ' <font color="red">(%s)</font> ' % etat

    def get_ues_stat_dict(self, filter_sport=False):  # was get_ues()
        """Liste des UEs, ordonnée par numero.
        Si filter_sport, retire les UE de type SPORT
        """
        if not filter_sport:
            return self._ues
        else:
            return [ue for ue in self._ues if ue["type"] != UE_SPORT]

    def get_modimpls_dict(self, ue_id=None):
        "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
        if ue_id is None:
            r = self._modimpls
        else:
            r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id]
        # trie la liste par ue.numero puis mat.numero puis mod.numero
        r.sort(
            key=lambda x: (x["ue"]["numero"], x["mat"]["numero"], x["module"]["numero"])
        )
        return r

    def get_etud_eval_note(self, etudid, evaluation_id):
        "note d'un etudiant a une evaluation"
        return self._valid_evals[evaluation_id]["notes"][etudid]

    def get_evals_in_mod(self, moduleimpl_id):
        "liste des evaluations valides dans un module"
        return [
            e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id
        ]

    def get_mod_stats(self, moduleimpl_id):
        """moyenne generale, min, max pour un module
        Ne prend en compte que les evaluations où toutes les notes sont entrées
        Cache le resultat.
        """
        if moduleimpl_id in self.moduleimpl_stats:
            return self.moduleimpl_stats[moduleimpl_id]
        nb_notes = 0
        sum_notes = 0.0
        nb_missing = 0
        moys = self._modmoys[moduleimpl_id]
        vals = []
        for etudid in self.get_etudids():
            # saute les demissionnaires et les défaillants:
            if self.inscrdict[etudid]["etat"] != "I":
                continue
            val = moys.get(etudid, None)  # None si non inscrit
            try:
                vals.append(float(val))
            except:
                nb_missing = nb_missing + 1
        sum_notes = sum(vals)
        nb_notes = len(vals)
        if nb_notes > 0:
            moy = sum_notes / nb_notes
            max_note, min_note = max(vals), min(vals)
        else:
            moy, min_note, max_note = "NA", "-", "-"
        s = {
            "moy": moy,
            "max": max_note,
            "min": min_note,
            "nb_notes": nb_notes,
            "nb_missing": nb_missing,
            "nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]),
        }
        self.moduleimpl_stats[moduleimpl_id] = s
        return s

    def compute_moy_moy(self):
        """precalcule les moyennes d'UE et generale (moyennes sur tous
        les etudiants), et les stocke dans self.moy_moy, self.ue['moy']

        Les moyennes d'UE ne tiennent pas compte des capitalisations.
        """
        ues = self.get_ues_stat_dict()
        sum_moy = 0  # la somme des moyennes générales valides
        nb_moy = 0  # le nombre de moyennes générales valides
        for ue in ues:
            ue["_notes"] = []  # liste tmp des valeurs de notes valides dans l'ue
        nb_dem = 0  # nb d'étudiants démissionnaires dans le semestre
        nb_def = 0  # nb d'étudiants défaillants dans le semestre
        T = self.get_table_moyennes_triees()
        for t in T:
            etudid = t[-1]
            # saute les demissionnaires et les défaillants:
            if self.inscrdict[etudid]["etat"] != "I":
                if self.inscrdict[etudid]["etat"] == "D":
                    nb_dem += 1
                if self.inscrdict[etudid]["etat"] == DEF:
                    nb_def += 1
                continue
            try:
                sum_moy += float(t[0])
                nb_moy += 1
            except:
                pass
            i = 0
            for ue in ues:
                i += 1
                try:
                    ue["_notes"].append(float(t[i]))
                except:
                    pass
        self.nb_demissions = nb_dem
        self.nb_defaillants = nb_def
        if nb_moy > 0:
            self.moy_moy = sum_moy / nb_moy
        else:
            self.moy_moy = "-"

        i = 0
        for ue in ues:
            i += 1
            ue["nb_vals"] = len(ue["_notes"])
            if ue["nb_vals"] > 0:
                ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"]
                ue["max"] = max(ue["_notes"])
                ue["min"] = min(ue["_notes"])
            else:
                ue["moy"], ue["max"], ue["min"] = "", "", ""
            del ue["_notes"]

    def get_etud_mod_moy(self, moduleimpl_id, etudid):
        """moyenne d'un etudiant dans un module (ou NI si non inscrit)"""
        return self._modmoys[moduleimpl_id].get(etudid, "NI")

    def get_etud_mat_moy(self, matiere_id, etudid):
        """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
        matmoy = self._matmoys.get(matiere_id, None)
        if not matmoy:
            return "NM"  # non inscrit
            # log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid))
            # raise ValueError('matiere invalide !') # should not occur
        return matmoy.get(etudid, "NA")

    def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None):
        """Calcule moyenne gen. pour un etudiant dans une UE
        Ne prend en compte que les evaluations où toutes les notes sont entrées
        Return a dict(moy, nb_notes, nb_missing, sum_coefs)
        Si pas de notes, moy == 'NA' et sum_coefs==0
        Si non inscrit, moy == 'NI' et sum_coefs==0
        """
        assert ue_id
        modimpls = self.get_modimpls_dict(ue_id)
        nb_notes = 0  # dans cette UE
        sum_notes = 0.0
        sum_coefs = 0.0
        nb_missing = 0  # nb de modules sans note dans cette UE

        notes_bonus_gen = []  # liste des notes de sport et culture
        coefs_bonus_gen = []

        ue_malus = 0.0  # malus à appliquer à cette moyenne d'UE

        notes = NoteVector()
        coefs = NoteVector()
        coefs_mask = NoteVector()  # 0/1, 0 si coef a ete annulé

        matiere_id_last = None
        matiere_sum_notes = matiere_sum_coefs = 0.0

        est_inscrit = False  # inscrit à l'un des modules de cette UE ?

        for modimpl in modimpls:
            # module ne faisant pas partie d'une UE capitalisee
            val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI")
            # si 'NI', etudiant non inscrit a ce module
            if val != "NI":
                est_inscrit = True
            if modimpl["module"]["module_type"] == ModuleType.STANDARD:
                coef = modimpl["module"]["coefficient"]
                if modimpl["ue"]["type"] != UE_SPORT:
                    notes.append(val, name=modimpl["module"]["code"])
                    try:
                        sum_notes += val * coef
                        sum_coefs += coef
                        nb_notes = nb_notes + 1
                        coefs.append(coef)
                        coefs_mask.append(1)
                        matiere_id = modimpl["module"]["matiere_id"]
                        if (
                            matiere_id_last
                            and matiere_id != matiere_id_last
                            and matiere_sum_coefs
                        ):
                            self._matmoys[matiere_id_last][etudid] = (
                                matiere_sum_notes / matiere_sum_coefs
                            )
                            matiere_sum_notes = matiere_sum_coefs = 0.0
                        matiere_sum_notes += val * coef
                        matiere_sum_coefs += coef
                        matiere_id_last = matiere_id
                    except TypeError:  # val == "NI" "NA"
                        assert val == "NI" or val == "NA" or val == "ERR"
                        nb_missing = nb_missing + 1
                        coefs.append(0)
                        coefs_mask.append(0)

                else:  # UE_SPORT:
                    # la note du module de sport agit directement sur la moyenne gen.
                    try:
                        notes_bonus_gen.append(float(val))
                        coefs_bonus_gen.append(coef)
                    except:
                        # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
                        pass
            elif modimpl["module"]["module_type"] == ModuleType.MALUS:
                try:
                    ue_malus += val
                except:
                    pass  # si non inscrit ou manquant, ignore
            elif modimpl["module"]["module_type"] in (
                ModuleType.RESSOURCE,
                ModuleType.SAE,
            ):
                # XXX temporaire pour ne pas bloquer durant le dev
                pass
            else:
                raise ValueError(
                    "invalid module type (%s)" % modimpl["module"]["module_type"]
                )

        if matiere_id_last and matiere_sum_coefs:
            self._matmoys[matiere_id_last][etudid] = (
                matiere_sum_notes / matiere_sum_coefs
            )

        # Calcul moyenne:
        if sum_coefs > 0:
            moy = sum_notes / sum_coefs
            if ue_malus:
                moy -= ue_malus
                moy = max(scu.NOTES_MIN, min(moy, 20.0))
            moy_valid = True
        else:
            moy = "NA"
            moy_valid = False

        # Recalcule la moyenne en utilisant une formule utilisateur
        expr_diag = {}
        formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
        if formula:
            moy = sco_compute_moy.compute_user_formula(
                self.sem,
                etudid,
                moy,
                moy_valid,
                notes,
                coefs,
                coefs_mask,
                formula,
                diag_info=expr_diag,
            )
            if expr_diag:
                expr_diag["ue_id"] = ue_id
                self.expr_diagnostics.append(expr_diag)

        return dict(
            moy=moy,
            nb_notes=nb_notes,
            nb_missing=nb_missing,
            sum_coefs=sum_coefs,
            notes_bonus_gen=notes_bonus_gen,
            coefs_bonus_gen=coefs_bonus_gen,
            expr_diag=expr_diag,
            ue_malus=ue_malus,
            est_inscrit=est_inscrit,
        )

    def comp_etud_moy_gen(self, etudid, cnx):
        """Calcule moyenne gen. pour un etudiant
        Return a dict:
         moy  : moyenne générale
         nb_notes, nb_missing, sum_coefs
         ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
         ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
         ects_pot_pro: (float) nb d'ECTS issus d'UE pro
         moy_ues : { ue_id : ue_status }
        où ue_status = {
             'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
             'moy' :  moyenne, avec capitalisation eventuelle
             'capitalized_ue_id' : id de l'UE capitalisée
             'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
                         (la somme des coefs des modules, ou le coef d'UE capitalisée,
                         ou encore le coef d'UE si l'option use_ue_coefs est active)
             'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
             'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
             'is_capitalized' : True|False,
             'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
             'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,
             'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon,
             'formsemestre_id' : (si capitalisee),
             'event_date' : (si capitalisee)
             }
        Si pas de notes, moy == 'NA' et sum_coefs==0

        Prend toujours en compte les UE capitalisées.
        """
        # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
        block_computation = (
            self.inscrdict[etudid]["etat"] == "D"
            or self.inscrdict[etudid]["etat"] == DEF
            or self.block_moyennes
        )

        moy_ues = {}
        notes_bonus_gen = (
            []
        )  # liste des notes de sport et culture (s'appliquant à la MG)
        coefs_bonus_gen = []
        nb_notes = 0  # nb de notes d'UE (non capitalisees)
        sum_notes = 0.0  # somme des notes d'UE
        # somme des coefs d'UE (eux-même somme des coefs de modules avec notes):
        sum_coefs = 0.0

        nb_missing = 0  # nombre d'UE sans notes
        sem_ects_pot = 0.0
        sem_ects_pot_fond = 0.0
        sem_ects_pot_pro = 0.0

        for ue in self.get_ues_stat_dict():
            # - On calcule la moyenne d'UE courante:
            if not block_computation:
                mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx)
            else:
                mu = dict(
                    moy="NA",
                    nb_notes=0,
                    nb_missing=0,
                    sum_coefs=0,
                    notes_bonus_gen=0,
                    coefs_bonus_gen=0,
                    expr_diag="",
                    est_inscrit=False,
                )
            # infos supplementaires pouvant servir au calcul du bonus sport
            mu["ue"] = ue
            moy_ues[ue["ue_id"]] = mu

            # - Faut-il prendre une UE capitalisée ?
            if mu["moy"] != "NA" and mu["est_inscrit"]:
                max_moy_ue = mu["moy"]
            else:
                # pas de notes dans l'UE courante, ou pas inscrit
                max_moy_ue = 0.0
            if not mu["est_inscrit"]:
                coef_ue = 0.0
            else:
                if self.use_ue_coefs:
                    coef_ue = mu["ue"]["coefficient"]
                else:
                    # coef UE = sum des coefs modules
                    coef_ue = mu["sum_coefs"]

            # is_capitalized si l'UE prise en compte est une UE capitalisée
            mu["is_capitalized"] = False
            # was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure)
            mu["was_capitalized"] = False

            is_external = False
            event_date = None
            if not block_computation:
                for ue_cap in self.ue_capitalisees[etudid]:
                    if ue_cap["ue_code"] == ue["ue_code"]:
                        moy_ue_cap = ue_cap["moy"]
                        mu["was_capitalized"] = True
                        event_date = event_date or ue_cap["event_date"]
                        if (
                            (moy_ue_cap != "NA")
                            and isinstance(moy_ue_cap, float)
                            and isinstance(max_moy_ue, float)
                            and (moy_ue_cap > max_moy_ue)
                        ):
                            # meilleure UE capitalisée
                            event_date = ue_cap["event_date"]
                            max_moy_ue = moy_ue_cap
                            mu["is_capitalized"] = True
                            capitalized_ue_id = ue_cap["ue_id"]
                            formsemestre_id = ue_cap["formsemestre_id"]
                            coef_ue = self.get_etud_ue_cap_coef(
                                etudid, ue, ue_cap, cnx=cnx
                            )
                            is_external = ue_cap["is_external"]

            mu["cur_moy_ue"] = mu["moy"]  # la moyenne dans le sem. courant
            if mu["est_inscrit"]:
                mu["cur_coef_ue"] = mu["sum_coefs"]
            else:
                mu["cur_coef_ue"] = 0.0
            mu["moy"] = max_moy_ue  # la moyenne d'UE a prendre en compte
            mu["is_external"] = is_external  # validation externe (dite "antérieure")
            mu["coef_ue"] = coef_ue  # coef reel ou coef de l'ue si capitalisee

            if mu["is_capitalized"]:
                mu["formsemestre_id"] = formsemestre_id
                mu["capitalized_ue_id"] = capitalized_ue_id
            if mu["was_capitalized"]:
                mu["event_date"] = event_date
            # - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury
            if (
                isinstance(mu["moy"], float)
                and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
            ):
                mu["ects_pot"] = ue["ects"] or 0.0
                if UE_is_fondamentale(ue["type"]):
                    mu["ects_pot_fond"] = mu["ects_pot"]
                else:
                    mu["ects_pot_fond"] = 0.0
                if UE_is_professionnelle(ue["type"]):
                    mu["ects_pot_pro"] = mu["ects_pot"]
                else:
                    mu["ects_pot_pro"] = 0.0
            else:
                mu["ects_pot"] = 0.0
                mu["ects_pot_fond"] = 0.0
                mu["ects_pot_pro"] = 0.0
            sem_ects_pot += mu["ects_pot"]
            sem_ects_pot_fond += mu["ects_pot_fond"]
            sem_ects_pot_pro += mu["ects_pot_pro"]

            # - Calcul moyenne générale dans le semestre:
            if mu["is_capitalized"]:
                try:
                    sum_notes += mu["moy"] * mu["coef_ue"]
                    sum_coefs += mu["coef_ue"]
                except:  # pas de note dans cette UE
                    pass
            else:
                if mu["coefs_bonus_gen"]:
                    notes_bonus_gen.extend(mu["notes_bonus_gen"])
                    coefs_bonus_gen.extend(mu["coefs_bonus_gen"])
                #
                try:
                    sum_notes += mu["moy"] * mu["sum_coefs"]
                    sum_coefs += mu["sum_coefs"]
                    nb_notes = nb_notes + 1
                except TypeError:
                    nb_missing = nb_missing + 1
        # Le resultat:
        infos = dict(
            nb_notes=nb_notes,
            nb_missing=nb_missing,
            sum_coefs=sum_coefs,
            moy_ues=moy_ues,
            ects_pot=sem_ects_pot,
            ects_pot_fond=sem_ects_pot_fond,
            ects_pot_pro=sem_ects_pot_pro,
            sem=self.sem,
        )
        # ---- Calcul moyenne (avec bonus sport&culture)
        if sum_coefs <= 0 or block_computation:
            infos["moy"] = "NA"
        else:
            if self.use_ue_coefs:
                # Calcul optionnel (mai 2020)
                # moyenne pondére par leurs coefficients des moyennes d'UE
                sum_moy_ue = 0
                sum_coefs_ue = 0
                for mu in moy_ues.values():
                    # mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas)
                    if (
                        (mu["ue"]["type"] != UE_SPORT)
                        and scu.isnumber(mu["moy"])
                        and (mu["est_inscrit"] or mu["is_capitalized"])
                    ):
                        coef_ue = mu["ue"]["coefficient"]
                        sum_moy_ue += mu["moy"] * coef_ue
                        sum_coefs_ue += coef_ue
                if sum_coefs_ue != 0:
                    infos["moy"] = sum_moy_ue / sum_coefs_ue
                else:
                    infos["moy"] = "NA"
            else:
                # Calcul standard ScoDoc: moyenne pondérée des notes de modules
                infos["moy"] = sum_notes / sum_coefs

            if notes_bonus_gen and infos["moy"] != "NA":
                # regle de calcul maison (configurable, voir bonus_sport.py)
                if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1:
                    log(
                        "comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)"
                        % (
                            coefs_bonus_gen,
                            notes_bonus_gen,
                            etudid,
                            self.formsemestre_id,
                        )
                    )
                    bonus = 0
                else:
                    if len(coefs_bonus_gen) == 1:
                        coefs_bonus_gen = [1.0]  # irrelevant, may be zero

                    # XXX attention: utilise anciens bonus_sport, évidemment
                    bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
                    if bonus_func:
                        bonus = bonus_func(
                            notes_bonus_gen, coefs_bonus_gen, infos=infos
                        )
                    else:
                        bonus = 0.0
                self.bonus[etudid] = bonus
                infos["moy"] += bonus
                infos["moy"] = min(infos["moy"], 20.0)  # clip bogus bonus

        return infos

    def get_etud_moy_gen(self, etudid):  # -> float | str
        """Moyenne generale de cet etudiant dans ce semestre.
        Prend en compte les UE capitalisées.
        Si pas de notes: 'NA'
        """
        return self.moy_gen[etudid]

    def get_etud_moy_infos(self, etudid):  # XXX OBSOLETE
        """Infos sur moyennes"""
        return self.etud_moy_infos[etudid]

    # was etud_has_all_ue_over_threshold:
    def etud_check_conditions_ues(self, etudid):
        """Vrai si les conditions sur les UE sont remplies.
        Ne considère que les UE ayant des notes (moyenne calculée).
        (les UE sans notes ne sont pas comptées comme sous la barre)
        Prend en compte les éventuelles UE capitalisées.

        Pour les parcours habituels, cela revient à vérifier que
        les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)

        Pour les parcours non standards (LP2014), cela peut être plus compliqué.

        Return: True|False, message explicatif
        """
        ue_status_list = []
        for ue in self._ues:
            ue_status = self.get_etud_ue_status(etudid, ue["ue_id"])
            if ue_status:
                ue_status_list.append(ue_status)
        return self.parcours.check_barre_ues(ue_status_list)

    def get_table_moyennes_triees(self):
        return self.T

    def get_etud_rang(self, etudid) -> str:
        return self.etud_moy_gen_ranks.get(etudid, "999")

    def get_etud_rang_group(self, etudid, group_id):
        """Returns rank of etud in this group and number of etuds in group.
        If etud not in group, returns None.
        """
        if not group_id in self.rangs_groupes:
            # lazy: fill rangs_groupes on demand
            # { groupe : { etudid : rang } }
            if not group_id in self.group_etuds:
                # lazy fill: list of etud in group_id
                etuds = sco_groups.get_group_members(group_id)
                self.group_etuds[group_id] = set([x["etudid"] for x in etuds])
            # 1- build T restricted to group
            Tr = []
            for t in self.get_table_moyennes_triees():
                t_etudid = t[-1]
                if t_etudid in self.group_etuds[group_id]:
                    Tr.append(t)
            #
            self.rangs_groupes[group_id] = comp_ranks(Tr)

        return (
            self.rangs_groupes[group_id].get(etudid, None),
            len(self.rangs_groupes[group_id]),
        )

    def get_table_moyennes_dict(self):
        """{ etudid : (liste des moyennes) } comme get_table_moyennes_triees"""
        D = {}
        for t in self.T:
            D[t[-1]] = t
        return D

    def get_moduleimpls_attente(self):
        "Liste des moduleimpls avec des notes en attente"
        return self._mods_att

    # Decisions existantes du jury
    def comp_decisions_jury(self):
        """Cherche les decisions du jury pour le semestre (pas les UE).
        Calcule l'attribut:
        decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
        decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
        Si la decision n'a pas été prise, la clé etudid n'est pas présente.
        Si l'étudiant est défaillant, met un code DEF sur toutes les UE
        """
        cnx = ndb.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            """SELECT etudid, code, assidu, compense_formsemestre_id, event_date 
            FROM scolar_formsemestre_validation 
            WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
            """,
            {"formsemestre_id": self.formsemestre_id},
        )
        decisions_jury = {}
        for (
            etudid,
            code,
            assidu,
            compense_formsemestre_id,
            event_date,
        ) in cursor.fetchall():
            decisions_jury[etudid] = {
                "code": code,
                "assidu": assidu,
                "compense_formsemestre_id": compense_formsemestre_id,
                "event_date": ndb.DateISOtoDMY(event_date),
            }

        self.decisions_jury = decisions_jury
        # UEs:
        cursor.execute(
            "select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;",
            {"formsemestre_id": self.formsemestre_id},
        )
        decisions_jury_ues = {}
        for (etudid, ue_id, code, event_date) in cursor.fetchall():
            if etudid not in decisions_jury_ues:
                decisions_jury_ues[etudid] = {}
            # Calcul des ECTS associes a cette UE:
            ects = 0.0
            if sco_codes_parcours.code_ue_validant(code):
                ue = self.uedict.get(ue_id, None)
                if ue is None:  # not in list for this sem ??? (probably an error)
                    log(
                        "Warning: %s capitalized an UE %s which is not part of current sem %s"
                        % (etudid, ue_id, self.formsemestre_id)
                    )
                    ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
                    self.uedict[ue_id] = ue  # record this UE
                    if ue_id not in self._uecoef:
                        cl = formsemestre_uecoef_list(
                            cnx,
                            args={
                                "formsemestre_id": self.formsemestre_id,
                                "ue_id": ue_id,
                            },
                        )
                        if not cl:
                            # cas anormal: UE capitalisee, pas dans ce semestre, et sans coef
                            log("Warning: setting UE coef to zero")
                            formsemestre_uecoef_create(
                                cnx,
                                args={
                                    "formsemestre_id": self.formsemestre_id,
                                    "ue_id": ue_id,
                                    "coefficient": 0,
                                },
                            )

                ects = ue["ects"] or 0.0  # 0 if None

            decisions_jury_ues[etudid][ue_id] = {
                "code": code,
                "ects": ects,  # 0. si non UE validée ou si mode de calcul different (?)
                "event_date": ndb.DateISOtoDMY(event_date),
            }

        self.decisions_jury_ues = decisions_jury_ues

    def get_etud_decision_sem(self, etudid):
        """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
        { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
        Si état défaillant, force le code a DEF
        """
        if self.get_etud_etat(etudid) == DEF:
            return {
                "code": DEF,
                "assidu": False,
                "event_date": "",
                "compense_formsemestre_id": None,
            }
        else:
            return self.decisions_jury.get(etudid, None)

    def get_etud_decision_ues(self, etudid):
        """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
        Ne tient pas compte des UE capitalisées.
        { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
        Ne renvoie aucune decision d'UE pour les défaillants
        """
        if self.get_etud_etat(etudid) == DEF:
            return {}
        else:
            return self.decisions_jury_ues.get(etudid, None)

    def sem_has_decisions(self):
        """True si au moins une decision de jury dans ce semestre"""
        if [x for x in self.decisions_jury_ues.values() if x]:
            return True

        return len([x for x in self.decisions_jury_ues.values() if x]) > 0

    def etud_has_decision(self, etudid):
        """True s'il y a une décision de jury pour cet étudiant"""
        return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)

    def all_etuds_have_sem_decisions(self):
        """True si tous les étudiants du semestre ont une décision de jury.
        ne regarde pas les décisions d'UE (todo: à voir ?)
        """
        for etudid in self.get_etudids():
            if self.inscrdict[etudid]["etat"] == "D":
                continue  # skip demissionnaires
            if self.get_etud_decision_sem(etudid) is None:
                return False
        return True

    # Capitalisation des UEs
    def comp_ue_capitalisees(self):
        """Cherche pour chaque etudiant ses UE capitalisées dans ce semestre.
        Calcule l'attribut:
        ue_capitalisees = { etudid :
                             [{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] }
        """
        self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
        cnx = None
        semestre_id = self.sem["semestre_id"]
        for etudid in self.get_etudids():
            capital = formsemestre_get_etud_capitalisation(
                self.formation["id"],
                semestre_id,
                ndb.DateDMYtoISO(self.sem["date_debut"]),
                etudid,
            )
            for ue_cap in capital:
                # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
                # il faut la calculer ici et l'enregistrer
                if ue_cap["moy_ue"] is None:
                    log(
                        "comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)"
                        % (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"])
                    )
                    nt_cap = sco_cache.NotesTableCache.get(
                        ue_cap["formsemestre_id"]
                    )  # > UE capitalisees par un etud
                    ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])
                    if ue_cap_status:
                        moy_ue_cap = ue_cap_status["moy"]
                    else:
                        moy_ue_cap = ""
                    ue_cap["moy_ue"] = moy_ue_cap
                    if (
                        isinstance(moy_ue_cap, float)
                        and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE
                    ):
                        if not cnx:
                            cnx = ndb.GetDBConnexion()
                        sco_parcours_dut.do_formsemestre_validate_ue(
                            cnx,
                            nt_cap,
                            ue_cap["formsemestre_id"],
                            etudid,
                            ue_cap["ue_id"],
                            ue_cap["code"],
                        )
                    else:
                        log(
                            "*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)"
                            % (
                                moy_ue_cap,
                                etudid,
                                ue_cap["ue_id"],
                                ue_cap["formsemestre_id"],
                            )
                        )
                ue_cap["moy"] = ue_cap["moy_ue"]  # backward compat (needs refactoring)
                self.ue_capitalisees[etudid].append(ue_cap)
        if cnx:
            cnx.commit()
        # log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) )

    # def comp_etud_sum_coef_modules_ue( etudid, ue_id):
    #     """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
    #     ou None s'il n'y a aucun module
    #     """
    #     c_list = [ mod['module']['coefficient']
    #                for mod in self._modimpls
    #                if (( mod['module']['ue_id'] == ue_id)
    #                    and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False)
    #     ]
    #     if not c_list:
    #         return None
    #     return sum(c_list)

    def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None):
        """Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
        injectée dans le semestre courant.

        ue : ue du semestre courant

        ue_cap = resultat de formsemestre_get_etud_capitalisation
        { 'ue_id' (dans le semestre source),
          'ue_code', 'moy', 'event_date','formsemestre_id' }
        """
        # log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap))
        # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
        if ue["ue_id"] not in self._uecoef:
            self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list(
                cnx,
                args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]},
            )

        if len(self._uecoef[ue["ue_id"]]):
            # utilisation du coef manuel
            return self._uecoef[ue["ue_id"]][0]["coefficient"]

        # 2- Mode automatique: calcul du coefficient
        # Capitalisation depuis un autre semestre ScoDoc ?
        coef = None
        if ue_cap["formsemestre_id"]:
            # Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016)
            coef = comp_etud_sum_coef_modules_ue(
                ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
            )
        if coef != None:
            return coef
        else:
            # Capitalisation UE externe: quel coef appliquer ?
            # Si l'étudiant est inscrit dans le semestre courant,
            # somme des coefs des modules de l'UE auxquels il est inscrit
            c = comp_etud_sum_coef_modules_ue(self.formsemestre_id, etudid, ue["ue_id"])
            if c is not None:  # inscrit à au moins un module de cette UE
                return c
            # arfff: aucun moyen de déterminer le coefficient de façon sûre
            log(
                "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s"
                % (self.formsemestre_id, etudid, ue, ue_cap)
            )
            raise ScoValueError(
                """<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
                pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
                <p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
                </div>
                """
                % (
                    ue["acronyme"],
                    url_for(
                        "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
                    ),
                    self.get_nom_long(etudid),
                    url_for(
                        "notes.formsemestre_edit_uecoefs",
                        scodoc_dept=g.scodoc_dept,
                        formsemestre_id=self.formsemestre_id,
                        err_ue_id=ue["ue_id"],
                    ),
                )
            )

        return 0.0  # ?

    def get_etud_ue_status(self, etudid, ue_id):
        "Etat de cette UE (note, coef, capitalisation, ...)"
        return self._etud_moy_ues[etudid][ue_id]

    def etud_has_notes_attente(self, etudid):
        """Vrai si cet etudiant a au moins une note en attente dans ce semestre.
        (ne compte que les notes en attente dans des évaluation avec coef. non nul).
        """
        cnx = ndb.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            """SELECT n.*
            FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
            notes_moduleimpl_inscription i
            WHERE n.etudid = %(etudid)s
            and n.value = %(code_attente)s
            and n.evaluation_id = e.id
            and e.moduleimpl_id = m.id
            and m.formsemestre_id = %(formsemestre_id)s
            and e.coefficient != 0
            and m.id = i.moduleimpl_id
            and i.etudid=%(etudid)s
            """,
            {
                "formsemestre_id": self.formsemestre_id,
                "etudid": etudid,
                "code_attente": scu.NOTES_ATTENTE,
            },
        )
        return len(cursor.fetchall()) > 0

    def get_evaluations_etats(self):  # evaluation_list_in_sem
        """[ {...evaluation et son etat...} ]"""
        if self._evaluations_etats is None:
            self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
                self.formsemestre_id
            )

        return self._evaluations_etats

    def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
        """Liste des évaluations de ce module"""
        return [
            e
            for e in self.get_evaluations_etats()
            if e["moduleimpl_id"] == moduleimpl_id
        ]

    def apc_recompute_moyennes(self):
        """recalcule les moyennes en APC (BUT)
        et modifie en place le tableau T.
        XXX Raccord provisoire avant refonte de cette classe.
        """
        assert self.parcours.APC_SAE
        formsemestre = FormSemestre.query.get(self.formsemestre_id)
        results = bulletin_but.ResultatsSemestreBUT(formsemestre)

        # Rappel des épisodes précédents: T est une liste de liste
        # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
        ues = self.get_ues_stat_dict()  # incluant le(s) UE de sport
        for t in self.T:
            etudid = t[-1]
            if etudid in results.etud_moy_gen:  # evite les démissionnaires
                t[0] = results.etud_moy_gen[etudid]
                for i, ue in enumerate(ues, start=1):
                    if ue["type"] != UE_SPORT:
                        # temporaire pour 9.1.29 !
                        if ue["id"] in results.etud_moy_ue:
                            t[i] = results.etud_moy_ue[ue["id"]][etudid]
                        else:
                            t[i] = ""
        # re-trie selon la nouvelle moyenne générale:
        self.T.sort(key=self._row_key)
        # Remplace aussi le rang:
        self.etud_moy_gen_ranks = results.etud_moy_gen_ranks