##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################

"""Table récapitulatif des résultats d'un semestre
"""

from flask import g, url_for
import numpy as np

from app.auth.models import User
from app.comp.res_common import ResultatsSemestre
from app.models import Identite, FormSemestre, UniteEns
from app.scodoc.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups
from app.scodoc import sco_utils as scu
from app.tables import table_builder as tb


class TableRecap(tb.Table):
    """Table récap. des résultats.
    allow_html: si vrai, peut mettre du HTML dans les valeurs

    Result: Table, le contenu étant une ligne par étudiant.


    Si convert_values, transforme les notes en chaines ("12.34").
    Les colonnes générées sont:
        etudid
        rang : rang indicatif (basé sur moy gen)
        moy_gen : moy gen indicative
        moy_ue_<ue_id>, ...,  les moyennes d'UE
        moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
        moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE

    On ajoute aussi des classes:
    - les colonnes:
        - la moyenne générale a la classe col_moy_gen
        - les colonnes SAE ont la classe col_sae
        - les colonnes Resources ont la classe col_res
        - les colonnes d'UE ont la classe col_ue
        - les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
    """

    def __init__(
        self,
        res: ResultatsSemestre,
        convert_values=False,
        include_evaluations=False,
        mode_jury=False,
        row_class=None,
        finalize=True,
        read_only: bool = True,
        **kwargs,
    ):
        self.rows: list["RowRecap"] = []  # juste pour que VSCode nous aide sur .rows
        super().__init__(row_class=row_class or RowRecap, **kwargs)
        self.res = res
        self.include_evaluations = include_evaluations
        self.mode_jury = mode_jury
        self.read_only = read_only  # utilisé seulement dans sous-classes
        cursus = res.formsemestre.formation.get_cursus()
        self.barre_moy = cursus.BARRE_MOY - scu.NOTES_TOLERANCE
        self.barre_valid_ue = cursus.NOTES_BARRE_VALID_UE
        self.barre_warning_ue = cursus.BARRE_UE_DISPLAY_WARNING
        self.cache_nomcomplet = {}  # cache uid : nomcomplet
        if convert_values:
            self.fmt_note = scu.fmt_note
        else:
            self.fmt_note = lambda x: x
        # couples (modimpl, ue) effectivement présents dans la table:
        self.modimpl_ue_ids = set()

        ues = res.formsemestre.query_ues(with_sport=True)  # avec bonus
        ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]

        if res.formsemestre.etuds_inscriptions:  # table non vide
            # Fixe l'ordre des groupes de colonnes communs:
            groups = [
                "etud_codes",
                "rang",
                "identite_court",
                "identite_detail",
                "partition",
                "cursus",
                "col_ues_validables",
            ]
            if not res.formsemestre.block_moyenne_generale:
                groups.append("col_moy_gen")
            groups.append("abs")
            self.set_groups(groups)

            for etudid in res.formsemestre.etuds_inscriptions:
                etud = Identite.get_etud(etudid)
                row = self.row_class(self, etud)
                self.add_row(row)
                row.add_etud_cols()
                row.add_moyennes_cols(ues_sans_bonus)
                row.add_abs()

            self.add_partitions()
            self.add_cursus()
            self.add_admissions()

            # Tri par rang croissant
            if not res.formsemestre.block_moyenne_generale:
                self.sort_rows(key=lambda row: row.rang_order or 10000000)
            else:
                self.sort_rows(key=lambda row: row.nb_ues_validables or 0, reverse=True)

            # Lignes footer (min, max, ects, apo, ...)
            self.add_bottom_rows(ues_sans_bonus)

            # Evaluations:
            if include_evaluations:
                self.add_evaluations()

        if finalize:
            self.finalize()

    def finalize(self):
        """Termine la table: ajoute ligne avec les types,
        et ajoute classe sur les colonnes vides"""
        self.mark_empty_cols()
        self.add_type_row()

    def mark_empty_cols(self):
        """Ajoute classe "col_empty" aux colonnes de modules vides"""
        # identifie les col. vides par la classe sur leur moyenne
        row_moy = self.get_row_by_id("moy")
        for col_id in self.column_ids:
            cell: tb.Cell = row_moy.cells.get(col_id)
            if cell and "col_empty" in cell.classes:
                self.column_classes[col_id].add("col_empty")

    def add_type_row(self):
        """Ligne avec la classe de chaque colonne recap."""
        # récupère le type à partir du groupe (enlève le préfixe "col_" si présent)
        row_type = tb.BottomRow(
            self,
            "type_col",
            left_title="Type col.",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info", "type_col"],
        )
        for col_id in self.column_ids:
            group_name = self.column_group.get(col_id, "")
            if group_name.startswith("col_"):
                group_name = group_name[4:]
            row_type.add_cell(col_id, None, group_name)

    def add_bottom_rows(self, ues):
        """Les informations à mettre en bas de la table recap:
        min, max, moy, ECTS, Apo."""
        res = self.res
        # Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
        row_min = tb.BottomRow(
            self,
            "min",
            left_title="Min.",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )
        row_max = tb.BottomRow(
            self,
            "max",
            left_title="Max.",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )
        row_moy = tb.BottomRow(
            self,
            "moy",
            left_title="Moy.",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )
        row_coef = tb.BottomRow(
            self,
            "coef",
            left_title="Coef.",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )
        row_ects = tb.BottomRow(
            self,
            "ects",
            left_title="ECTS",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )
        row_apo = tb.BottomRow(
            self,
            "apo",
            left_title="Code Apogée",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info", "apo"],
        )

        # --- ECTS
        # titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
        for ue in ues:
            col_id = f"moy_ue_{ue.id}"
            row_ects.add_cell(col_id, None, ue.ects)

        # la colonne où placer les valeurs agrégats
        col_id = "moy_gen" if "moy_gen" in self.column_ids else "code_cursus"
        col_group = "col_moy_gen" if "moy_gen" in self.column_ids else "cursus"
        row_ects.add_cell(
            col_id,
            None,
            sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
            group=col_group,
        )
        # --- MIN, MAX, MOY, APO
        row_min.add_cell(
            col_id,
            None,
            self.fmt_note(res.etud_moy_gen.min()),
            group=col_group,
        )
        row_max.add_cell(
            col_id,
            None,
            self.fmt_note(res.etud_moy_gen.max()),
            group=col_group,
        )
        row_moy.add_cell(
            col_id,
            None,
            self.fmt_note(res.etud_moy_gen.mean()),
            group=col_group,
        )

        for ue in ues:
            col_id = f"moy_ue_{ue.id}"
            row_min.add_cell(
                col_id,
                None,
                self.fmt_note(res.etud_moy_ue[ue.id].min()),
                classes=["col_ue", "col_moy_ue"],
            )
            row_max.add_cell(
                col_id,
                None,
                self.fmt_note(res.etud_moy_ue[ue.id].max()),
                classes=["col_ue", "col_moy_ue"],
            )
            row_moy.add_cell(
                col_id,
                None,
                self.fmt_note(res.etud_moy_ue[ue.id].mean()),
                classes=["col_ue", "col_moy_ue"],
            )
            row_apo.add_cell(
                col_id,
                None,
                ue.code_apogee or "",
                classes=["col_ue", "col_moy_ue"],
            )

            for modimpl in res.formsemestre.modimpls_sorted:
                if (modimpl.id, ue.id) in self.modimpl_ue_ids:
                    col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
                    if res.is_apc:
                        coef = res.modimpl_coefs_df[modimpl.id][ue.id]
                    else:
                        coef = modimpl.module.coefficient or 0
                    row_coef.add_cell(
                        col_id,
                        None,
                        self.fmt_note(coef),
                        group=f"col_ue_{ue.id}_modules",
                    )
                    notes = res.modimpl_notes(modimpl.id, ue.id)
                    if np.isnan(notes).all():
                        # aucune note valide
                        row_min.add_cell(col_id, None, np.nan)
                        row_max.add_cell(col_id, None, np.nan)
                        moy = np.nan
                    else:
                        row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes)))
                        row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes)))
                        moy = np.nanmean(notes)
                    row_moy.add_cell(
                        col_id,
                        None,
                        self.fmt_note(moy),
                        # aucune note dans ce module ?
                        classes=["col_empty" if np.isnan(moy) else ""],
                    )
                    row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")

    def add_partitions(self):
        """Ajoute les colonnes indiquant les groupes
        pour tous les étudiants de la table.
        La table contient des rows avec la clé etudid.
        Les colonnes ont la classe css "partition".
        """
        self.group_titles["partition"] = "Partitions"
        partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
            self.res.formsemestre.id
        )
        first_partition = True
        for partition in partitions:
            cid = f"part_{partition['partition_id']}"

            partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
            for row in self.rows:
                etudid = row.id
                group = None  # group (dict) de l'étudiant dans cette partition
                # dans NotesTableCompat, à revoir
                etud_etat = self.res.get_etud_etat(row.id)  # row.id == etudid
                if etud_etat == scu.DEMISSION:
                    gr_name = "Dém."
                    row.add_class("dem")
                elif etud_etat == DEF:
                    gr_name = "Déf."
                    row.add_class("def")
                else:
                    group = partition_etud_groups.get(etudid)
                    gr_name = group["group_name"] if group else ""
                if gr_name:
                    row.add_cell(
                        cid,
                        partition["partition_name"],
                        gr_name,
                        group="partition",
                        classes=[] if first_partition else ["partition_aux"],
                        # la classe "partition" est ajoutée par la Table car c'est le group
                        # la classe "partition_aux" est ajoutée à partir de la 2eme partition affichée
                    )
                    first_partition = False

                # Rangs dans groupe
                if (
                    partition["bul_show_rank"]
                    and (group is not None)
                    and (group["id"] in self.res.moy_gen_rangs_by_group)
                ):
                    rang = self.res.moy_gen_rangs_by_group[group["id"]][0]
                    rg_cid = cid + "_rg"  # rang dans la partition
                    row.add_cell(
                        rg_cid,
                        f"Rg {partition['partition_name']}",
                        rang.get(etudid, ""),
                        group="partition",
                        classes=["partition_aux", "partition_rangs"],
                    )

    def add_evaluations(self):
        """Ajoute les colonnes avec les notes aux évaluations
        pour tous les étudiants de la table.
        Les colonnes ont la classe css "evaluation"
        """
        self.group_titles["eval"] = "Évaluations"
        # nouvelle ligne pour description évaluations:
        row_descr_eval = tb.BottomRow(
            self,
            "evaluations",
            left_title="Description évaluations",
            left_title_col_ids=["prenom", "nom_short"],
            category="bottom_infos",
            classes=["bottom_info"],
        )

        first_eval = True
        for modimpl in self.res.formsemestre.modimpls_sorted:
            evals = self.res.modimpls_results[modimpl.id].get_evaluations_completes(
                modimpl
            )
            eval_index = len(evals) - 1
            inscrits = {i.etudid for i in modimpl.inscriptions}
            first_eval_of_mod = True
            for e in evals:
                col_id = f"eval_{e.id}"
                title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
                col_classes = []
                if first_eval:
                    col_classes.append("first")
                elif first_eval_of_mod:
                    col_classes.append("first_of_mod")
                first_eval_of_mod = first_eval = False
                eval_index -= 1
                notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
                    e.evaluation_id
                )
                for row in self.rows:
                    etudid = row.id
                    if etudid in inscrits:
                        if etudid in notes_db:
                            val = notes_db[etudid]["value"]
                        else:
                            # Note manquante mais prise en compte immédiate: affiche ATT
                            val = scu.NOTES_ATTENTE
                        content = self.fmt_note(val)
                        classes = col_classes + [
                            {
                                "ABS": "abs",
                                "ATT": "att",
                                "EXC": "exc",
                            }.get(content, "")
                        ]
                        row.add_cell(
                            col_id, title, content, group="eval", classes=classes
                        )
                    else:
                        row.add_cell(
                            col_id,
                            title,
                            "ni",
                            group="eval",
                            classes=col_classes + ["non_inscrit"],
                        )

                row_coef = self.get_row_by_id("coef")
                row_coef.add_cell(
                    col_id,
                    None,
                    self.fmt_note(e.coefficient),
                    group="eval",
                )
                row_min = self.get_row_by_id("min")
                row_min.add_cell(
                    col_id,
                    None,
                    0,
                    group="eval",
                )
                row_max = self.get_row_by_id("max")
                row_max.add_cell(
                    col_id,
                    None,
                    self.fmt_note(e.note_max),
                    group="eval",
                )
                row_descr_eval.add_cell(
                    col_id,
                    None,
                    e.description or "",
                    target=url_for(
                        "notes.evaluation_listenotes",
                        scodoc_dept=g.scodoc_dept,
                        evaluation_id=e.id,
                    ),
                    target_attrs={"class": "stdlink"},
                )

    def add_admissions(self):
        """Ajoute les colonnes "admission" pour tous les étduiants de la table
        Les colonnes ont la classe css "admission"
        """
        fields = {
            "bac": "Bac",
            "specialite": "Spécialité",
            "type_admission": "Type Adm.",
            "classement": "Rg. Adm.",
        }
        first = True
        for cid, title in fields.items():
            cell_head, cell_foot = self.add_title(cid, title)
            cell_head.classes.append("admission")
            cell_foot.classes.append("admission")
            if first:
                cell_head.classes.append("admission_first")
                cell_foot.classes.append("admission_first")
                first = False

        for row in self.rows:
            etud = row.etud
            admission = etud.admission.first()
            first = True
            for cid, title in fields.items():
                cell = row.add_cell(
                    cid,
                    title,
                    getattr(admission, cid) or "",
                    "admission",
                )
                if first:
                    cell.classes.append("admission_first")
                    first = False

    def add_cursus(self):
        """Ajoute colonne avec code cursus, eg 'S1 S2 S1'
        pour tous les étudiants de la table"""
        cid = "code_cursus"
        formation_code = self.res.formsemestre.formation.formation_code
        for row in self.rows:
            row.add_cell(
                cid,
                "Cursus",
                " ".join(
                    [
                        f"S{ins.formsemestre.semestre_id}"
                        for ins in reversed(row.etud.inscriptions())
                        if ins.formsemestre.formation.formation_code == formation_code
                    ]
                ),
                group="cursus",
            )

    def html(self, extra_classes: list[str] = None) -> str:
        """HTML: pour les tables recap, un div au contenu variable"""
        return f"""
        <div class="table_recap">
        {
            '<div class="message">aucun étudiant !</div>'
            if self.is_empty()
            else super().html(
                extra_classes=[
                    "table_recap", 
                    "apc" if self.res.formsemestre.formation.is_apc() else "classic",
                    "jury" if self.mode_jury else "",
                    "with_evaluations" if self.include_evaluations else "",
                ])
        }
        </div>
        """


class RowRecap(tb.Row):
    "Ligne de la table recap, pour un étudiant"

    def __init__(self, table: TableRecap, etud: Identite, *args, **kwargs):
        super().__init__(table, etud.id, *args, **kwargs)
        self.etud = etud
        self.rang_order = None
        "valeur pour tri rangs"
        self.nb_ues_validables = None
        self.nb_ues_warning = None
        self.nb_ues_etud_parcours = None

    def add_etud_cols(self):
        """Ajoute colonnes étudiant: codes, noms"""
        res = self.table.res
        etud = self.etud
        self.table.group_titles.update(
            {
                "etud_codes": "Codes",
                "identite_detail": "",
                "identite_court": "",
            }
        )
        # --- Codes (seront cachés, mais exportés en excel)
        self.add_cell("etudid", "etudid", etud.id, "etud_codes")
        self.add_cell(
            "code_nip",
            "code_nip",
            etud.code_nip or "",
            "etud_codes",
        )

        # --- Rang
        if not res.formsemestre.block_moyenne_generale:
            self.rang_order = res.etud_moy_gen_ranks_int[etud.id]
            self.add_cell(
                "rang",
                "Rg",
                res.etud_moy_gen_ranks[etud.id],
                "rang",
                data={"order": f"{self.rang_order:05d}"},
            )
        else:
            self.rang_order = -1
        # --- Identité étudiant
        url_bulletin = url_for(
            "notes.formsemestre_bulletinetud",
            scodoc_dept=g.scodoc_dept,
            formsemestre_id=res.formsemestre.id,
            etudid=etud.id,
        )
        self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
        self.add_cell(
            "nom_disp",
            "Nom",
            etud.nom_disp(),
            "identite_detail",
            data={"order": etud.sort_key},
            target=url_bulletin,
            target_attrs={"class": "etudinfo", "id": str(etud.id)},
        )
        self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
        self.add_cell(
            "nom_short",
            "Nom",
            etud.nom_short,
            "identite_court",
            data={
                "order": etud.sort_key,
                "etudid": etud.id,
                "nomprenom": etud.nomprenom,
            },
            target=url_bulletin,
            target_attrs={"class": "etudinfo", "id": str(etud.id)},
        )

    def add_abs(self):
        "Ajoute les colonnes absences"
        # Absences (nb d'abs non just. dans ce semestre)
        nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id)
        self.add_cell("nbabs", "Abs", nbabs, "abs")
        self.add_cell("nbabsjust", "Just.", nbabsjust, "abs")

    def add_moyennes_cols(
        self,
        ues_sans_bonus: list[UniteEns],
    ):
        """Ajoute cols moy_gen moy_ue et tous les modules..."""
        etud = self.etud
        table = self.table
        res = table.res
        # --- Si DEM ou DEF, ne montre aucun résultat d'UE ni moy. gen.
        if res.get_etud_etat(etud.id) != scu.INSCRIT:
            return
        # --- Moyenne générale
        if not res.formsemestre.block_moyenne_generale:
            moy_gen = res.etud_moy_gen.get(etud.id, False)
            note_class = ""
            if moy_gen is False:
                moy_gen = scu.NO_NOTE_STR
            elif isinstance(moy_gen, float) and moy_gen < table.barre_moy:
                note_class = "moy_ue_warning"  # en rouge
            self.add_cell(
                "moy_gen",
                "Moy",
                table.fmt_note(moy_gen),
                group="col_moy_gen",
                classes=[note_class],
            )
        # Ajoute bulle sur titre du pied de table:
        cell = table.foot_title_row.cells.get("moy_gen")
        if cell:
            table.foot_title_row.cells["moy_gen"].target_attrs["title"] = (
                "Moyenne générale indicative"
                if res.is_apc
                else "Moyenne générale du semestre"
            )

        # --- Moyenne d'UE
        self.nb_ues_validables, self.nb_ues_warning = 0, 0
        for ue in ues_sans_bonus:
            ue_status = res.get_etud_ue_status(etud.id, ue.id)
            if ue_status is not None:
                self.add_ue_cols(ue, ue_status)
                if table.mode_jury:
                    # pas d'autre colonnes de résultats
                    continue
                # Bonus (sport) dans cette UE ?
                # Le bonus sport appliqué sur cette UE
                if (res.bonus_ues is not None) and (ue.id in res.bonus_ues):
                    val = res.bonus_ues[ue.id][etud.id] or ""
                    val_fmt = val_fmt_html = table.fmt_note(val)
                    if val:
                        val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l">{
                            val_fmt
                        }</span>"""
                    self.add_cell(
                        f"bonus_ue_{ue.id}",
                        f"Bonus {ue.acronyme}",
                        val_fmt_html,
                        raw_content=val_fmt,
                        group=f"col_ue_{ue.id}",
                        column_classes={"col_ue_bonus", "col_res"},
                    )
                # Les moyennes des modules (ou ressources et SAÉs) dans cette UE
                self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"])

        self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id))
        ue_valid_txt = (
            ue_valid_txt_html
        ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
        if self.nb_ues_warning:
            ue_valid_txt_html += " " + scu.EMO_WARNING
        cell_class = ""
        if self.nb_ues_warning:
            cell_class = "moy_ue_warning"
        elif self.nb_ues_validables < len(ues_sans_bonus):
            cell_class = "moy_inf"
        self.add_cell(
            "ues_validables",
            "UEs",
            ue_valid_txt_html,
            group="col_ues_validables",
            classes=[cell_class],
            column_classes={"col_ue"},
            raw_content=ue_valid_txt,
            data={"order": self.nb_ues_validables},  #  tri
        )

    def add_ue_cols(self, ue: UniteEns, ue_status: dict, col_group: str = None):
        "Ajoute résultat UE au row (colonne col_ue)"
        # sous-classé par JuryRow pour ajouter les codes
        table = self.table
        formsemestre: FormSemestre = table.res.formsemestre
        table.group_titles[
            "col_ue"
        ] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
        col_id = f"moy_ue_{ue.id}"
        val = ue_status["moy"]
        note_classes = []
        if isinstance(val, float):
            if val < table.barre_moy:
                note_classes = ["moy_inf"]
            elif val >= table.barre_valid_ue:
                note_classes = ["moy_ue_valid"]
                self.nb_ues_validables += 1
            if val < table.barre_warning_ue:
                note_classes = ["moy_ue_warning"]  # notes très basses
                self.nb_ues_warning += 1
        if ue_status["is_capitalized"]:
            note_classes.append("ue_capitalized")

        self.add_cell(
            col_id,
            ue.acronyme,
            table.fmt_note(val),
            group=col_group or f"col_ue_{ue.id}",
            classes=note_classes,
            column_classes={f"col_ue_{ue.id}", "col_moy_ue", "col_ue"},
        )
        table.foot_title_row.cells[col_id].target_attrs[
            "title"
        ] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""

    def add_ue_modimpls_cols(self, ue: UniteEns, is_capitalized: bool):
        """Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE"""
        # pylint: disable=invalid-unary-operand-type
        etud = self.etud
        table = self.table
        res = table.res
        for modimpl in res.modimpls_in_ue(ue, etud.id, with_bonus=False):
            if is_capitalized:
                val = "-c-"
            else:
                modimpl_results = res.modimpls_results.get(modimpl.id)
                if modimpl_results:  # pas bonus
                    if res.is_apc:  # BUT
                        moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id)
                        val = (
                            moys_vers_ue.get(etud.id, "?")
                            if moys_vers_ue is not None
                            else ""
                        )
                    else:  # classique: Series indépendantes de l'UE
                        val = modimpl_results.etuds_moy_module.get(etud.id, "?")
                else:
                    val = ""

            col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"

            val_fmt_html = val_fmt = table.fmt_note(val)
            if modimpl.module.module_type == scu.ModuleType.MALUS:
                if val and not isinstance(val, str) and not np.isnan(val):
                    if val >= 0:
                        val_fmt_html = f"""<span class="sp2l">-{
                            val_fmt
                        }</span>"""
                    else:
                        val_fmt_html = f"""<span class="sp2l malus_negatif">+{
                            table.fmt_note(-val)}</span>"""
                else:
                    val_fmt = val_fmt_html = ""  # inscrit à ce malus, mais sans note

            cell = self.add_cell(
                col_id,
                modimpl.module.code,
                val_fmt_html,
                raw_content=val_fmt,
                group=f"col_ue_{ue.id}_modules",
                column_classes={
                    f"col_{modimpl.module.type_abbrv()}",
                    f"mod_ue_{ue.id}",
                },
            )
            if modimpl.module.module_type == scu.ModuleType.MALUS:
                # positionne la colonne à droite de l'UE
                cell.group = f"col_ue_{ue.id}_malus"
                table.insert_group(cell.group, after=f"col_ue_{ue.id}")

            table.foot_title_row.cells[col_id].target = url_for(
                "notes.moduleimpl_status",
                scodoc_dept=g.scodoc_dept,
                moduleimpl_id=modimpl.id,
            )

            nom_resp = table.cache_nomcomplet.get(modimpl.responsable_id)
            if nom_resp is None:
                user = User.query.get(modimpl.responsable_id)
                nom_resp = user.get_nomcomplet() if user else ""
                table.cache_nomcomplet[modimpl.responsable_id] = nom_resp
            table.foot_title_row.cells[col_id].target_attrs[
                "title"
            ] = f"{modimpl.module.titre} ({nom_resp})"
            table.modimpl_ue_ids.add((modimpl.id, ue.id))
