# -*- 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
#
##############################################################################

"""Liste des notes d'une évaluation
"""
from collections import defaultdict
import numpy as np

import flask
from flask import url_for, g, request

from app import log
from app import models
from app.comp import res_sem
from app.comp import moy_mod
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator

from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
import sco_version
from app.scodoc.gen_tables import GenTable
from app.scodoc.htmlutils import histogram_notes


def do_evaluation_listenotes(
    evaluation_id=None, moduleimpl_id=None, format="html"
) -> tuple[str, str]:
    """
    Affichage des notes d'une évaluation (si evaluation_id)
    ou de toutes les évaluations d'un module (si moduleimpl_id)
    """
    mode = None
    if moduleimpl_id:
        mode = "module"
        evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
    elif evaluation_id:
        mode = "eval"
        evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
    else:
        raise ValueError("missing argument: evaluation or module")
    if not evals:
        return "<p>Aucune évaluation !</p>", f"ScoDoc"

    E = evals[0]  # il y a au moins une evaluation
    modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
    # description de l'evaluation
    if mode == "eval":
        H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
        page_title = f"Notes {E['description'] or modimpl.module.code}"
    else:
        H = []
        page_title = f"Notes {modimpl.module.code}"
    # groupes
    groups = sco_groups.do_evaluation_listegroupes(
        E["evaluation_id"], include_default=True
    )
    grlabs = [g["group_name"] or "tous" for g in groups]  # legendes des boutons
    grnams = [str(g["group_id"]) for g in groups]  # noms des checkbox

    if len(evals) > 1:
        descr = [
            ("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"})
        ]
    else:
        descr = [
            ("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"})
        ]
    if len(grnams) > 1:
        descr += [
            (
                "s",
                {
                    "input_type": "separator",
                    "title": "<b>Choix du ou des groupes d'étudiants:</b>",
                },
            ),
            (
                "group_ids",
                {
                    "input_type": "checkbox",
                    "title": "",
                    "allowed_values": grnams,
                    "labels": grlabs,
                    "attributes": ('onclick="document.tf.submit();"',),
                },
            ),
        ]
    else:
        if grnams:
            def_nam = grnams[0]
        else:
            def_nam = ""
        descr += [
            (
                "group_ids",
                {"input_type": "hidden", "type": "list", "default": [def_nam]},
            )
        ]
    descr += [
        (
            "anonymous_listing",
            {
                "input_type": "checkbox",
                "title": "",
                "allowed_values": ("yes",),
                "labels": ('listing "anonyme"',),
                "attributes": ('onclick="document.tf.submit();"',),
                "template": '<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s &nbsp;&nbsp;',
            },
        ),
        (
            "note_sur_20",
            {
                "input_type": "checkbox",
                "title": "",
                "allowed_values": ("yes",),
                "labels": ("notes sur 20",),
                "attributes": ('onclick="document.tf.submit();"',),
                "template": "%(elem)s &nbsp;&nbsp;",
            },
        ),
        (
            "hide_groups",
            {
                "input_type": "checkbox",
                "title": "",
                "allowed_values": ("yes",),
                "labels": ("masquer les groupes",),
                "attributes": ('onclick="document.tf.submit();"',),
                "template": "%(elem)s &nbsp;&nbsp;",
            },
        ),
        (
            "with_emails",
            {
                "input_type": "checkbox",
                "title": "",
                "allowed_values": ("yes",),
                "labels": ("montrer les e-mails",),
                "attributes": ('onclick="document.tf.submit();"',),
                "template": "%(elem)s</td></tr>",
            },
        ),
    ]
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        descr,
        cancelbutton=None,
        submitbutton=None,
        bottom_buttons=False,
        method="GET",
        cssclass="noprint",
        name="tf",
        is_submitted=True,  # toujours "soumis" (démarre avec liste complète)
    )
    if tf[0] == 0:
        return "\n".join(H) + "\n" + tf[1], page_title
    elif tf[0] == -1:
        return (
            flask.redirect(
                "%s/Notes/moduleimpl_status?moduleimpl_id=%s"
                % (scu.ScoURL(), E["moduleimpl_id"])
            ),
            "",
        )
    else:
        anonymous_listing = tf[2]["anonymous_listing"]
        note_sur_20 = tf[2]["note_sur_20"]
        hide_groups = tf[2]["hide_groups"]
        with_emails = tf[2]["with_emails"]
        group_ids = [x for x in tf[2]["group_ids"] if x != ""]
        return (
            _make_table_notes(
                tf[1],
                evals,
                format=format,
                note_sur_20=note_sur_20,
                anonymous_listing=anonymous_listing,
                group_ids=group_ids,
                hide_groups=hide_groups,
                with_emails=with_emails,
                mode=mode,
            ),
            page_title,
        )


def _make_table_notes(
    html_form,
    evals,
    format="",
    note_sur_20=False,
    anonymous_listing=False,
    hide_groups=False,
    with_emails=False,
    group_ids=[],
    mode="module",  # "eval" or "module"
):
    """Table liste notes (une seule évaluation ou toutes celles d'un module)"""
    # Code à ré-écrire !
    if not evals:
        return "<p>Aucune évaluation !</p>"
    E = evals[0]
    moduleimpl_id = E["moduleimpl_id"]
    modimpl_o = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
    module = models.Module.query.get(modimpl_o["module_id"])
    is_apc = module.formation.get_parcours().APC_SAE
    if is_apc:
        modimpl = ModuleImpl.query.get(moduleimpl_id)
        is_conforme = modimpl.check_apc_conformity()
        evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
        if not ues:
            is_apc = False
    else:
        evals_poids, ues = None, None
        is_conforme = True
    sem = sco_formsemestre.get_formsemestre(modimpl_o["formsemestre_id"])
    # (debug) check that all evals are in same module:
    for e in evals:
        if e["moduleimpl_id"] != moduleimpl_id:
            raise ValueError("invalid evaluations list")

    if format == "xls":
        keep_numeric = True  # pas de conversion des notes en strings
    else:
        keep_numeric = False
    # Si pas de groupe, affiche tout
    if not group_ids:
        group_ids = [sco_groups.get_default_group(modimpl_o["formsemestre_id"])]
    groups = sco_groups.listgroups(group_ids)

    gr_title = sco_groups.listgroups_abbrev(groups)
    gr_title_filename = sco_groups.listgroups_filename(groups)

    if anonymous_listing:
        columns_ids = ["code"]  # cols in table
    else:
        if format == "xls" or format == "xml":
            columns_ids = ["nom", "prenom"]
        else:
            columns_ids = ["nomprenom"]
    if not hide_groups:
        columns_ids.append("group")

    titles = {
        "code": "Code",
        "group": "Groupe",
        "nom": "Nom",
        "prenom": "Prénom",
        "nomprenom": "Nom",
        "expl_key": "Rem.",
        "email": "e-mail",
        "emailperso": "e-mail perso",
        "signatures": "Signatures",
    }
    rows = []

    class KeyManager(dict):  # comment : key (pour regrouper les comments a la fin)
        def __init__(self):
            self.lastkey = 1

        def nextkey(self):
            r = self.lastkey
            self.lastkey += 1
            # self.lastkey = chr(ord(self.lastkey)+1)
            return str(r)

    key_mgr = KeyManager()

    # code pour listings anonyme, à la place du nom
    if sco_preferences.get_preference("anonymous_lst_code") == "INE":
        anonymous_lst_key = "code_ine"
    elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
        anonymous_lst_key = "code_nip"
    else:
        anonymous_lst_key = "etudid"

    etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
        E["evaluation_id"], groups, include_demdef=True
    )
    for etudid, etat in etudid_etats:
        css_row_class = None
        # infos identite etudiant
        etud = Identite.query.get(etudid)
        if etud is None:
            continue

        if etat == "I":  # si inscrit, indique groupe
            groups = sco_groups.get_etud_groups(etudid, modimpl_o["formsemestre_id"])
            grc = sco_groups.listgroups_abbrev(groups)
        else:
            if etat == "D":
                grc = "DEM"  # attention: ce code est re-ecrit plus bas, ne pas le changer (?)
                css_row_class = "etuddem"
            else:
                grc = etat

        code = getattr(etud, anonymous_lst_key)
        if not code:  # laisser le code vide n'aurait aucun sens, prenons l'etudid
            code = etudid

        rows.append(
            {
                "code": str(code),  # INE, NIP ou etudid
                "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
                "etudid": etudid,
                "nom": etud.nom.upper(),
                "_nomprenom_target": url_for(
                    "notes.formsemestre_bulletinetud",
                    scodoc_dept=g.scodoc_dept,
                    formsemestre_id=modimpl_o["formsemestre_id"],
                    etudid=etudid,
                ),
                "_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """,
                "prenom": etud.prenom.lower().capitalize(),
                "nomprenom": etud.nomprenom,
                "group": grc,
                "_group_td_attrs": 'class="group"',
                "email": etud.get_first_email(),
                "emailperso": etud.get_first_email("emailperso"),
                "_css_row_class": css_row_class or "",
            }
        )

    # Lignes en tête:
    row_coefs = {
        "nom": "",
        "prenom": "",
        "nomprenom": "",
        "group": "",
        "code": "",
        "_css_row_class": "sorttop fontitalic",
        "_table_part": "head",
    }
    row_poids = {
        "nom": "",
        "prenom": "",
        "nomprenom": "",
        "group": "",
        "code": "",
        "_css_row_class": "sorttop poids",
        "_table_part": "head",
    }
    row_note_max = {
        "nom": "",
        "prenom": "",
        "nomprenom": "",
        "group": "",
        "code": "",
        "_css_row_class": "sorttop fontitalic",
        "_table_part": "head",
    }
    row_moys = {
        "_css_row_class": "moyenne sortbottom",
        "_table_part": "foot",
        #'_nomprenom_td_attrs' : 'colspan="2" ',
        "nomprenom": "Moyenne :",
        "comment": "",
    }
    # Ajoute les notes de chaque évaluation:
    for e in evals:
        e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
        notes, nb_abs, nb_att = _add_eval_columns(
            e,
            evals_poids,
            ues,
            rows,
            titles,
            row_coefs,
            row_poids,
            row_note_max,
            row_moys,
            is_apc,
            key_mgr,
            note_sur_20,
            keep_numeric,
            format=format,
        )
        columns_ids.append(e["evaluation_id"])
    #
    if anonymous_listing:
        rows.sort(key=lambda x: x["code"] or "")
    else:
        rows.sort(
            key=lambda x: (x["nom"] or "", x["prenom"] or "")
        )  # sort by nom, prenom

    # Si module, ajoute la (les) "moyenne(s) du module:
    if mode == "module":
        if len(evals) > 1:
            # Moyenne de l'étudiant dans le module
            # Affichée même en APC à titre indicatif
            _add_moymod_column(
                sem["formsemestre_id"],
                moduleimpl_id,
                rows,
                columns_ids,
                titles,
                row_coefs,
                row_poids,
                row_note_max,
                row_moys,
                is_apc,
                keep_numeric,
            )
        if is_apc:
            # Ajoute une colonne par UE
            _add_apc_columns(
                modimpl,
                evals_poids,
                ues,
                rows,
                columns_ids,
                titles,
                is_conforme,
                row_coefs,
                row_poids,
                row_note_max,
                row_moys,
                keep_numeric,
            )

    # Ajoute colonnes emails tout à droite:
    if with_emails:
        columns_ids += ["email", "emailperso"]
    # Ajoute lignes en tête et moyennes
    if len(evals) > 0 and format != "bordereau":
        rows_head = [row_coefs]
        if is_apc:
            rows_head.append(row_poids)
        rows_head.append(row_note_max)
        rows = rows_head + rows
    rows.append(row_moys)
    # ajout liens HTMl vers affichage une evaluation:
    if format == "html" and len(evals) > 1:
        rlinks = {"_table_part": "head"}
        for e in evals:
            rlinks[e["evaluation_id"]] = "afficher"
            rlinks[
                "_" + str(e["evaluation_id"]) + "_help"
            ] = "afficher seulement les notes de cette évaluation"
            rlinks["_" + str(e["evaluation_id"]) + "_target"] = url_for(
                "notes.evaluation_listenotes",
                scodoc_dept=g.scodoc_dept,
                evaluation_id=e["evaluation_id"],
            )
            rlinks["_" + str(e["evaluation_id"]) + "_td_attrs"] = ' class="tdlink" '
        rows.append(rlinks)

    if len(evals) == 1:  # colonne "Rem." seulement si une eval
        if format == "html":  # pas d'indication d'origine en pdf (pour affichage)
            columns_ids.append("expl_key")
        elif format == "xls" or format == "xml":
            columns_ids.append("comment")
        elif format == "bordereau":
            columns_ids.append("signatures")

    # titres divers:
    gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids])
    if note_sur_20:
        gl = "&note_sur_20%3Alist=yes" + gl
    if anonymous_listing:
        gl = "&anonymous_listing%3Alist=yes" + gl
    if hide_groups:
        gl = "&hide_groups%3Alist=yes" + gl
    if with_emails:
        gl = "&with_emails%3Alist=yes" + gl
    if len(evals) == 1:
        evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"]))
        hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats))
        filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename))

        if format == "bordereau":
            hh = " %d étudiants" % (len(etudid_etats))
            hh += " %d absent" % (nb_abs)
            if nb_abs > 1:
                hh += "s"
            hh += ", %d en attente." % (nb_att)

            pdf_title = "<br/> BORDEREAU DE SIGNATURES"
            pdf_title += "<br/><br/>%(titre)s" % sem
            pdf_title += "<br/>(%(mois_debut)s - %(mois_fin)s)" % sem
            pdf_title += " semestre %s %s" % (
                sem["semestre_id"],
                sem.get("modalite", ""),
            )
            pdf_title += f"<br/>Notes du module {module.code} - {module.titre}"
            pdf_title += "<br/>Evaluation : %(description)s " % e
            if len(e["jour"]) > 0:
                pdf_title += " (%(jour)s)" % e
            pdf_title += "(noté sur %(note_max)s )<br/><br/>" % e
        else:
            hh = " %s, %s (%d étudiants)" % (
                E["description"],
                gr_title,
                len(etudid_etats),
            )
            if len(e["jour"]) > 0:
                pdf_title = "%(description)s (%(jour)s)" % e
            else:
                pdf_title = "%(description)s " % e

        caption = hh
        html_title = ""
        base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl
        html_next_section = (
            '<div class="notes_evaluation_stats">%d absents, %d en attente.</div>'
            % (nb_abs, nb_att)
        )
    else:
        filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
        title = f"Notes {module.type_name()} {module.code} {module.titre}"
        title += " semestre %(titremois)s" % sem
        if gr_title and gr_title != "tous":
            title += " %s" % gr_title
        caption = title
        html_next_section = ""
        if format == "pdf" or format == "bordereau":
            caption = ""  # same as pdf_title
        pdf_title = title
        html_title = f"""<h2 class="formsemestre">Notes {module.type_name()} <a href="{
                url_for("notes.moduleimpl_status", 
                scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
            }">{module.code} {module.titre}</a></h2>
            """
        if not is_conforme:
            html_title += (
                """<div class="warning">Poids des évaluations non conformes !</div>"""
            )
        base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl
    # display
    tab = GenTable(
        titles=titles,
        columns_ids=columns_ids,
        rows=rows,
        html_sortable=True,
        base_url=base_url,
        filename=filename,
        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
        caption=caption,
        html_next_section=html_next_section,
        page_title="Notes de " + sem["titremois"],
        html_title=html_title,
        pdf_title=pdf_title,
        html_class="notes_evaluation",
        preferences=sco_preferences.SemPreferences(modimpl_o["formsemestre_id"]),
        # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
    )
    if format == "bordereau":
        format = "pdf"
    t = tab.make_page(format=format, with_html_headers=False)
    if format != "html":
        return t

    if len(evals) > 1:
        all_complete = True
        for e in evals:
            if not e["eval_state"]["evalcomplete"]:
                all_complete = False
        if all_complete:
            eval_info = '<span class="eval_info"><span class="eval_complete">Evaluations prises en compte dans les moyennes.</span>'
        else:
            eval_info = """<span class="eval_info help">
            Les évaluations en vert et orange sont prises en compte dans les moyennes. 
            Celles en rouge n'ont pas toutes leurs notes."""
        if is_apc:
            eval_info += """ <span>La moyenne indicative est la moyenne des moyennes d'UE, et n'est pas utilisée en BUT. 
            Les moyennes sur le groupe sont estimées sans les absents (sauf pour les moyennes des moyennes d'UE) ni les démissionnaires.</span>"""
        eval_info += """</span>"""
        return html_form + eval_info + t + "<p></p>"
    else:
        # Une seule evaluation: ajoute histogramme
        histo = histogram_notes(notes)
        # 2 colonnes: histo, comments
        C = [
            f'<br><a class="stdlink" href="{base_url}&format=bordereau">Bordereau de Signatures (version PDF)</a>',
            "<table><tr><td><div><h4>Répartition des notes:</h4>"
            + histo
            + "</div></td>\n",
            '<td style="padding-left: 50px; vertical-align: top;"><p>',
        ]
        commentkeys = list(key_mgr.items())  # [ (comment, key), ... ]
        commentkeys.sort(key=lambda x: int(x[1]))
        for (comment, key) in commentkeys:
            C.append(
                '<span class="colcomment">(%s)</span> <em>%s</em><br/>' % (key, comment)
            )
        if commentkeys:
            C.append(
                '<span><a class=stdlink" href="evaluation_list_operations?evaluation_id=%s">Gérer les opérations</a></span><br/>'
                % E["evaluation_id"]
            )
        eval_info = "xxx"
        if E["eval_state"]["evalcomplete"]:
            eval_info = '<span class="eval_info eval_complete">Evaluation prise en compte dans les moyennes</span>'
        elif E["eval_state"]["evalattente"]:
            eval_info = '<span class="eval_info eval_attente">Il y a des notes en attente (les autres sont prises en compte)</span>'
        else:
            eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'

        return (
            sco_evaluations.evaluation_describe(evaluation_id=E["evaluation_id"])
            + eval_info
            + html_form
            + t
            + "\n".join(C)
        )


def _add_eval_columns(
    e,
    evals_poids,
    ues,
    rows,
    titles,
    row_coefs,
    row_poids,
    row_note_max,
    row_moys,
    is_apc,
    K,
    note_sur_20,
    keep_numeric,
    format="html",
):
    """Add eval e"""
    nb_notes = 0
    nb_abs = 0
    nb_att = 0
    sum_notes = 0
    notes = []  # liste des notes numeriques, pour calcul histogramme uniquement
    evaluation_id = e["evaluation_id"]
    e_o = Evaluation.query.get(evaluation_id)  # XXX en attendant ré-écriture
    inscrits = e_o.moduleimpl.formsemestre.etudids_actifs  # set d'etudids
    notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)

    if len(e["jour"]) > 0:
        titles[evaluation_id] = "%(description)s (%(jour)s)" % e
    else:
        titles[evaluation_id] = "%(description)s " % e

    if e["eval_state"]["evalcomplete"]:
        klass = "eval_complete"
    elif e["eval_state"]["evalattente"]:
        klass = "eval_attente"
    else:
        klass = "eval_incomplete"
        titles[evaluation_id] += " (non prise en compte)"
    titles[f"_{evaluation_id}_td_attrs"] = f'class="{klass}"'

    for row in rows:
        etudid = row["etudid"]
        if etudid in notes_db:
            val = notes_db[etudid]["value"]
            if val is None:
                nb_abs += 1
            if val == scu.NOTES_ATTENTE:
                nb_att += 1
            # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
            if (
                (etudid in inscrits)
                and val is not None
                and val != scu.NOTES_NEUTRALISE
                and val != scu.NOTES_ATTENTE
            ):
                if e["note_max"] > 0:
                    valsur20 = val * 20.0 / e["note_max"]  # remet sur 20
                else:
                    valsur20 = 0
                notes.append(valsur20)  # toujours sur 20 pour l'histogramme
                if note_sur_20:
                    val = valsur20  # affichage notes / 20 demandé
                nb_notes = nb_notes + 1
                sum_notes += val
            val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric)
            comment = notes_db[etudid]["comment"]
            if comment is None:
                comment = ""
            explanation = "%s (%s) %s" % (
                notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
                sco_users.user_info(notes_db[etudid]["uid"])["nomcomplet"],
                comment,
            )
        else:
            if (etudid in inscrits) and e["publish_incomplete"]:
                # Note manquante mais prise en compte immédiate: affiche ATT
                val = scu.NOTES_ATTENTE
                val_fmt = "ATT"
                explanation = "non saisie mais prise en compte immédiate"
            else:
                explanation = ""
                val_fmt = ""
                val = None

        cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get(
            val_fmt, ""
        )

        if val is None:
            row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" '
            if not row.get("_css_row_class", ""):
                row["_css_row_class"] = "etudabs"
        else:
            row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" '
        # regroupe les commentaires
        if explanation:
            if explanation in K:
                expl_key = "(%s)" % K[explanation]
            else:
                K[explanation] = K.nextkey()
                expl_key = "(%s)" % K[explanation]
        else:
            expl_key = ""

        row.update(
            {
                evaluation_id: val_fmt,
                "_" + str(evaluation_id) + "_help": explanation,
                # si plusieurs evals seront ecrasés et non affichés:
                "comment": explanation,
                "expl_key": expl_key,
                "_expl_key_help": explanation,
            }
        )

        row_coefs[evaluation_id] = "coef. %s" % e["coefficient"]
        if is_apc:
            if format == "html":
                row_poids[evaluation_id] = _mini_table_eval_ue_poids(
                    evaluation_id, evals_poids, ues
                )
            else:
                row_poids[evaluation_id] = e_o.get_ue_poids_str()
        if note_sur_20:
            nmax = 20.0
        else:
            nmax = e["note_max"]
        if keep_numeric:
            row_note_max[evaluation_id] = nmax
        else:
            row_note_max[evaluation_id] = "/ %s" % nmax

        if nb_notes > 0:
            row_moys[evaluation_id] = scu.fmt_note(
                sum_notes / nb_notes, keep_numeric=keep_numeric
            )
            row_moys[
                "_" + str(evaluation_id) + "_help"
            ] = "moyenne sur %d notes (%s le %s)" % (
                nb_notes,
                e["description"],
                e["jour"],
            )
        else:
            row_moys[evaluation_id] = ""

    return notes, nb_abs, nb_att  # pour histogramme


def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues):
    "contenu de la cellule: poids"
    return (
        """<table class="eval_poids" title="poids vers les UE"><tr><td>"""
        + "</td><td>".join([f"{ue.acronyme}" for ue in ues])
        + "</td></tr>"
        + "<tr><td>"
        + "</td><td>".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues])
        + "</td></tr></table>"
    )


def _add_moymod_column(
    formsemestre_id,
    moduleimpl_id,
    rows,
    columns_ids,
    titles,
    row_coefs,
    row_poids,
    row_note_max,
    row_moys,
    is_apc,
    keep_numeric,
):
    """Ajoute la colonne moymod à rows"""
    col_id = "moymod"
    formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    inscrits = formsemestre.etudids_actifs

    nb_notes = 0
    sum_notes = 0
    notes = []  # liste des notes numeriques, pour calcul histogramme uniquement
    for row in rows:
        etudid = row["etudid"]
        val = nt.get_etud_mod_moy(moduleimpl_id, etudid)  # note sur 20, ou 'NA','NI'
        row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
        row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
        if etudid in inscrits and not isinstance(val, str):
            notes.append(val)
            nb_notes = nb_notes + 1
            sum_notes += val
    row_coefs[col_id] = "(avec abs)"
    if is_apc:
        row_poids[col_id] = "à titre indicatif"
    if keep_numeric:
        row_note_max[col_id] = 20.0
    else:
        row_note_max[col_id] = "/ 20"
    titles[col_id] = "Moyenne module"
    columns_ids.append(col_id)
    if nb_notes > 0:
        row_moys[col_id] = "%.3g" % (sum_notes / nb_notes)
        row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
    else:
        row_moys[col_id] = ""


def _add_apc_columns(
    modimpl,
    evals_poids,
    ues,
    rows,
    columns_ids,
    titles,
    is_conforme: bool,
    row_coefs,
    row_poids,
    row_note_max,
    row_moys,
    keep_numeric,
):
    """Ajoute les colonnes moyennes vers les UE"""
    # On raccorde ici les nouveaux calculs de notes (BUT 2021)
    # sur l'ancien code ScoDoc
    # => On recharge tout dans les nouveaux modèles
    # rows est une liste de dict avec une clé "etudid"
    # on va y ajouter une clé par UE du semestre
    nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre)
    modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
    inscrits = modimpl.formsemestre.etudids_actifs
    # les UE dans lesquelles ce module a un coef non nul:
    ues_with_coef = nt.modimpl_coefs_df[modimpl.id][
        nt.modimpl_coefs_df[modimpl.id] > 0
    ].index
    ues = [ue for ue in ues if ue.id in ues_with_coef]
    sum_by_ue = defaultdict(float)
    nb_notes_by_ue = defaultdict(int)
    if is_conforme:
        # valeur des moyennes vers les UEs:
        for row in rows:
            for ue in ues:
                moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?")
                row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
                row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
                if (
                    isinstance(moy_ue, float)
                    and not np.isnan(moy_ue)
                    and row["etudid"] in inscrits
                ):
                    sum_by_ue[ue.id] += moy_ue
                    nb_notes_by_ue[ue.id] += 1
    # Nom et coefs des UE (lignes titres):
    ue_coefs = modimpl.module.ue_coefs
    if is_conforme:
        coef_class = "coef_mod_ue"
    else:
        coef_class = "coef_mod_ue_non_conforme"
    for ue in ues:
        col_id = f"moy_ue_{ue.id}"
        titles[col_id] = ue.acronyme
        columns_ids.append(col_id)
        coefs = [uc for uc in ue_coefs if uc.ue_id == ue.id]
        if coefs:
            row_coefs[f"moy_ue_{ue.id}"] = coefs[0].coef
            row_coefs[f"_moy_ue_{ue.id}_td_attrs"] = f' class="{coef_class}" '
            if nb_notes_by_ue[ue.id] > 0:
                row_moys[col_id] = "%.3g" % (sum_by_ue[ue.id] / nb_notes_by_ue[ue.id])
                row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
            else:
                row_moys[col_id] = ""
