Skip to content
Snippets Groups Projects
Select Git revision
  • 0332553587b0f22502f83603a1f8d45308a79d6b
  • master default protected
2 results

recap.py

Blame
  • Forked from Jean-Marie Place / SCODOC_R6A06
    796 commits behind the upstream repository.
    recap.py 30.90 KiB
    ##############################################################################
    # ScoDoc
    # Copyright (c) 1999 - 2024 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 import db
    from app.auth.models import User
    from app.comp.res_common import ResultatsSemestre
    from app.models import Identite, Evaluation, 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.get_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, "")
                            row_max.add_cell(col_id, None, "")
                            moy = ""
                        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 (moy == "" or 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.date_debut.strftime("%d/%m/%Y") if e.date_debut 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
                                    if e.evaluation_type != Evaluation.EVALUATION_BONUS
                                    else ""
                                )
                            content = self.fmt_note(val)
                            if e.evaluation_type != Evaluation.EVALUATION_BONUS:
                                classes = col_classes + [
                                    {
                                        "ABS": "abs",
                                        "ATT": "att",
                                        "EXC": "exc",
                                    }.get(content, "")
                                ]
                            else:
                                classes = col_classes + ["col_eval_bonus"]
                            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,
                        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 étudiants 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
                if admission:
                    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": "",
                    "rang": "",
                }
            )
            # --- 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)
            _, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id)
            self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs)
            self.add_cell(
                "nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust
            )
    
        def add_moyennes_cols(
            self,
            ues_sans_bonus: list[UniteEns],
        ):
            """Ajoute cols moy_gen moy_ue et tous les modules..."""
            etud = self.etud
            table: TableRecap = 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_parcours_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: TableRecap = 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"]
                if (self.etud.id, ue.id) not in table.res.dispense_ues
                else "="
            )
            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 or ue.acronyme} 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 = db.session.get(User, 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))