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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@gmail.com
#
##############################################################################

"""Evaluations"""

import collections
import datetime
import operator

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

from app import db
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre, ModuleImpl, Module

import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_gen_cal
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
import sco_version


# --------------------------------------------------------------------
#
#    MISC AUXILIARY FUNCTIONS
#
# --------------------------------------------------------------------
def notes_moyenne_median_mini_maxi(notes):
    "calcule moyenne et mediane d'une liste de valeurs (floats)"
    notes = [
        x
        for x in notes
        if (x != None) and (x != scu.NOTES_NEUTRALISE) and (x != scu.NOTES_ATTENTE)
    ]
    n = len(notes)
    if not n:
        return None, None, None, None
    moy = sum(notes) / n
    median = list_median(notes)
    mini = min(notes)
    maxi = max(notes)
    return moy, median, mini, maxi


def list_median(a_list: list):
    """Median of a list L"""
    n = len(a_list)
    if not n:
        raise ValueError("empty list")
    a_list.sort()
    if n % 2:
        return a_list[n // 2]
    return (a_list[n // 2] + a_list[n // 2 - 1]) / 2


# --------------------------------------------------------------------


def do_evaluation_etat(
    evaluation_id: int, partition_id: int = None, select_first_partition=False
) -> dict:
    """Donne infos sur l'état de l'évaluation.
    Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
    XXX utilisée par de très nombreuses fonctions, dont
    - _eval_etat<do_evaluation_etat_in_sem (en cours de remplacement)

    - _eval_etat<do_evaluation_etat_in_mod<formsemestre_tableau_modules
        qui a seulement besoin de
            nb_evals_completes, nb_evals_en_cours, nb_evals_vides, attente

    renvoie:
    {
        nb_inscrits : inscrits au module
        nb_notes
        nb_abs,
        nb_neutre,
        nb_att,
        moy, median, mini, maxi : # notes, en chaine, sur 20
        maxi_num : note max, numérique
        last_modif: datetime, *
        gr_complets, gr_incomplets,
        evalcomplete *
    }
    evalcomplete est vrai si l'eval est complete (tous les inscrits
    à ce module ont des notes)
    evalattente est vrai s'il ne manque que des notes en attente
    """
    nb_inscrits = len(
        sco_groups.do_evaluation_listeetuds_groups(evaluation_id, getallstudents=True)
    )
    etuds_notes_dict = sco_evaluation_db.do_evaluation_get_all_notes(
        evaluation_id
    )  # { etudid : note }

    # ---- Liste des groupes complets et incomplets
    evaluation = Evaluation.get_evaluation(evaluation_id)
    modimpl: ModuleImpl = evaluation.moduleimpl
    module: Module = modimpl.module

    is_malus = module.module_type == ModuleType.MALUS  # True si module de malus
    formsemestre_id = modimpl.formsemestre_id
    # Si partition_id is None, prend 'all' ou bien la premiere:
    if partition_id is None:
        if select_first_partition:
            partitions = sco_groups.get_partitions_list(formsemestre_id)
            partition = partitions[0]
        else:
            partition = sco_groups.get_default_partition(formsemestre_id)
        partition_id = partition["partition_id"]

    # Il faut considerer les inscriptions au semestre
    # (pour avoir l'etat et le groupe) et aussi les inscriptions
    # au module (pour gerer les modules optionnels correctement)
    insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
        formsemestre_id
    )
    insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
    insmodset = {x["etudid"] for x in insmod}
    # retire de insem ceux qui ne sont pas inscrits au module
    ins = [i for i in insem if i["etudid"] in insmodset]

    # Nombre de notes valides d'étudiants inscrits au module
    # (car il peut y avoir des notes d'étudiants désinscrits depuis l'évaluation)
    etudids_avec_note = insmodset.intersection(etuds_notes_dict)
    nb_notes = len(etudids_avec_note)
    # toutes saisies, y compris chez des non-inscrits:
    nb_notes_total = len(etuds_notes_dict)

    notes = [etuds_notes_dict[etudid]["value"] for etudid in etudids_avec_note]
    nb_abs = len([x for x in notes if x is None])
    nb_neutre = len([x for x in notes if x == scu.NOTES_NEUTRALISE])
    nb_att = len([x for x in notes if x == scu.NOTES_ATTENTE])
    moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
    if moy_num is None:
        median, moy = "", ""
        mini, maxi = "", ""
        maxi_num = None
    else:
        median = scu.fmt_note(median_num)
        moy = scu.fmt_note(moy_num, evaluation.note_max)
        mini = scu.fmt_note(mini_num, evaluation.note_max)
        maxi = scu.fmt_note(maxi_num, evaluation.note_max)
    # cherche date derniere modif note
    if len(etuds_notes_dict):
        t = [x["date"] for x in etuds_notes_dict.values()]
        last_modif = max(t)
    else:
        last_modif = None

    # On considere une note "manquante" lorsqu'elle n'existe pas
    # ou qu'elle est en attente (ATT)
    group_nb_missing = collections.defaultdict(int)  # group_id : nb notes manquantes
    group_notes = collections.defaultdict(list)  # group_id: liste notes valides
    total_nb_missing = 0
    total_nb_att = 0
    group_by_id = {}  # group_id : group
    etud_groups = sco_groups.get_etud_groups_in_partition(partition_id)

    for i in ins:
        group = etud_groups.get(i["etudid"], None)
        if group and not group["group_id"] in group_by_id:
            group_by_id[group["group_id"]] = group
        #
        is_missing = False
        if i["etudid"] in etuds_notes_dict:
            val = etuds_notes_dict[i["etudid"]]["value"]
            if val == scu.NOTES_ATTENTE:
                is_missing = True
                total_nb_att += 1
            if group:
                group_notes[group["group_id"]].append(val)
        else:
            if group:
                _ = group_notes[group["group_id"]]  # create group
            is_missing = True
        if is_missing:
            total_nb_missing += 1
            if group:
                group_nb_missing[group["group_id"]] += 1

    gr_incomplets = list(group_nb_missing.keys())
    gr_incomplets.sort()

    complete = (
        (total_nb_missing == 0)
        or (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
        and not evaluation.is_blocked()
    )
    evalattente = (total_nb_missing > 0) and (
        (total_nb_missing == total_nb_att) or evaluation.publish_incomplete
    )
    # mais ne met pas en attente les evals immediates sans aucune notes:
    if evaluation.publish_incomplete and nb_notes == 0:
        evalattente = False

    # Calcul moyenne dans chaque groupe de TD
    gr_moyennes = []  # group : {moy,median, nb_notes}
    for group_id, notes in group_notes.items():
        gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
        gr_moyennes.append(
            {
                "group_id": group_id,
                "group_name": group_by_id[group_id]["group_name"],
                "gr_moy": scu.fmt_note(gr_moy, evaluation.note_max),
                "gr_median": scu.fmt_note(gr_median, evaluation.note_max),
                "gr_mini": scu.fmt_note(gr_mini, evaluation.note_max),
                "gr_maxi": scu.fmt_note(gr_maxi, evaluation.note_max),
                "gr_nb_notes": len(notes),
                "gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
            }
        )
    gr_moyennes.sort(key=operator.itemgetter("group_name"))

    return {
        "evaluation_id": evaluation_id,
        "nb_inscrits": nb_inscrits,
        "nb_notes": nb_notes,  # nb notes etudiants inscrits
        "nb_notes_total": nb_notes_total,  # nb de notes (incluant desinscrits)
        "nb_abs": nb_abs,
        "nb_neutre": nb_neutre,
        "nb_att": nb_att,
        "moy": moy,  # chaine formattée, sur 20
        "median": median,
        "mini": mini,
        "maxi": maxi,
        "maxi_num": maxi_num,  # note maximale, en nombre
        "last_modif": last_modif,
        "gr_incomplets": gr_incomplets,
        "gr_moyennes": gr_moyennes,
        "groups": group_by_id,
        "evalcomplete": complete,
        "evalattente": evalattente,
        "is_malus": is_malus,
    }


def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
    """Synthétise les états d'une liste d'évaluations
    evals: list of mappings (etats),
    utilise e["blocked"], e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
    ->
    nb_evals : nb total qcq soit état
    nb_eval_completes (= prises en compte)
    nb_evals_en_cours (= avec des notes, mais pas complete)
    nb_evals_vides (= sans aucune note)
    nb_evals_attente (= avec des notes en ATTente et pas bloquée)
    date derniere modif

    Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
    """
    (
        nb_evals_completes,
        nb_evals_en_cours,
        nb_evals_vides,
        nb_evals_blocked,
        nb_evals_attente,
    ) = (0, 0, 0, 0, 0)
    dates = []
    for e in etat_evals:
        if e["etat"]["blocked"]:
            nb_evals_blocked += 1
        if e["etat"]["evalcomplete"]:
            nb_evals_completes += 1
        elif e["etat"]["nb_notes"] == 0:
            nb_evals_vides += 1
        elif not e["etat"]["blocked"]:
            nb_evals_en_cours += 1
        if e["etat"]["nb_attente"] and not e["etat"]["blocked"]:
            nb_evals_attente += 1
        last_modif = e["etat"]["last_modif"]
        if last_modif is not None:
            dates.append(e["etat"]["last_modif"])

    # date de derniere modif d'une note dans un module
    last_modif = sorted(dates)[-1] if dates else ""

    return {
        "nb_evals": len(etat_evals),
        "nb_evals_attente": nb_evals_attente,
        "nb_evals_blocked": nb_evals_blocked,
        "nb_evals_completes": nb_evals_completes,
        "nb_evals_en_cours": nb_evals_en_cours,
        "nb_evals_vides": nb_evals_vides,
        "last_modif": last_modif,
    }


def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict:
    """-> { nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
    date derniere modif, attente }
    """
    # Note: utilisé par
    # - formsemestre_status_head
    #     nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
    #     pour la ligne
    #     Évaluations: 	20 ok, 8 en cours, 5 vides (dernière note saisie le 11/01/2024 à 19h49)
    #     attente
    #
    # - gen_formsemestre_recapcomplet_xml
    # - gen_formsemestre_recapcomplet_json
    #     nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
    #
    # "nb_evals_completes"
    # "nb_evals_en_cours"
    # "nb_evals_vides"
    # "last_modif"
    # "attente"

    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    evaluations_etats = nt.get_evaluations_etats()
    # raccordement moche...
    etat = _summarize_evals_etats([{"etat": v} for v in evaluations_etats.values()])
    # Ajoute information sur notes en attente
    etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
    return etat


def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
    """état des évaluations dans ce module"""
    etat_evals = nt.get_mod_evaluation_etat_list(modimpl)
    etat = _summarize_evals_etats(etat_evals)
    # Il y a-t-il des notes en attente dans ce module ?
    etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
    return etat


class JourEval(sco_gen_cal.Jour):
    """
    Représentation d'un jour dans un calendrier d'évaluations
    """

    COLOR_INCOMPLETE = "#FF6060"
    COLOR_COMPLETE = "#A0FFA0"
    COLOR_FUTUR = "#70E0FF"

    def __init__(
        self,
        date: datetime.date,
        evaluations: list[Evaluation],
        parent: "CalendrierEval",
    ):
        super().__init__(date)

        self.evaluations: list[Evaluation] = evaluations
        self.evaluations.sort(key=lambda e: e.date_debut)

        self.parent: "CalendrierEval" = parent

    def get_html(self) -> str:
        htmls = []

        for e in self.evaluations:
            url: str = url_for(
                "notes.moduleimpl_status",
                scodoc_dept=g.scodoc_dept,
                moduleimpl_id=e.moduleimpl_id,
            )
            title: str = (
                e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
            )
            htmls.append(
                f"""<a
                href="{url}"
                style="{self._get_eval_style(e)}"
                title="{self._get_eval_title(e)}"
                class="stdlink"
            >{title}</a>"""
            )

        return ", ".join(htmls)

    def _get_eval_style(self, e: Evaluation) -> str:
        color: str = ""
        # Etat (notes completes) de l'évaluation:
        modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
        if modimpl_result.evaluations_etat[e.id].is_complete:
            color = JourEval.COLOR_COMPLETE
        else:
            color = JourEval.COLOR_INCOMPLETE
        if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
            color = JourEval.COLOR_FUTUR

        return f"background-color: {color};"

    def _get_eval_title(self, e: Evaluation) -> str:
        heure_debut_txt, heure_fin_txt = "", ""
        if e.date_debut != e.date_fin:
            heure_debut_txt = (
                e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
            )
            heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""

        title = f"{e.description or e.moduleimpl.module.titre_str()}"
        if heure_debut_txt:
            title += f" de {heure_debut_txt} à {heure_fin_txt}"

        return title


class CalendrierEval(sco_gen_cal.Calendrier):
    """
    Représentation des évaluations d'un semestre dans un calendrier
    """

    def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
        # On prend du 01/09 au 31/08
        date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
        date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
        super().__init__(date_debut, date_fin)

        # évalutions du semestre
        self.evals: dict[datetime.date, list[Evaluation]] = {}
        for e in evals:
            if e.date_debut is not None:
                day = e.date_debut.date()
                if day not in self.evals:
                    self.evals[day] = []
                self.evals[day].append(e)

        self.nt: NotesTableCompat = nt

    def instanciate_jour(self, date: datetime.date) -> JourEval:
        return JourEval(date, self.evals.get(date, []), parent=self)


# View
def formsemestre_evaluations_cal(formsemestre_id):
    """Page avec calendrier de toutes les evaluations de ce semestre"""
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

    evaluations = formsemestre.get_evaluations()
    nb_evals = len(evaluations)

    year = formsemestre.annee_scolaire()
    cal = CalendrierEval(year, evaluations, nt)
    cal_html = cal.get_html()

    return f"""
    {
        html_sco_header.html_sem_header(
            "Evaluations du semestre",
            cssstyles=["css/calabs.css"],
        )
    }
    <div class="cal_evaluations">
    { cal_html }
    </div>
    <p>soit {nb_evals} évaluations planifiées;
    </p>
    <ul>
        <li>en <span style=
            "background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
        les évaluations passées auxquelles il manque des notes
        </li>
        <li>en <span style=
            "background-color: {JourEval.COLOR_COMPLETE}">vert</span>
        les évaluations déjà notées
        </li>
        <li>en <span style=
            "background-color: {JourEval.COLOR_FUTUR}">bleu</span>
        les évaluations futures
        </li>
    </ul>
    <p><a href="{
        url_for("notes.formsemestre_evaluations_delai_correction",
                scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
        )
        }" class="stdlink">voir les délais de correction</a>
    </p>
    { html_sco_header.sco_footer() }
    """


def evaluation_date_first_completion(evaluation_id) -> datetime.datetime:
    """Première date à laquelle l'évaluation a été complète
    ou None si actuellement incomplète
    """
    etat = do_evaluation_etat(evaluation_id)
    if not etat["evalcomplete"]:
        return None

    # XXX inachevé ou à revoir ?
    # Il faut considerer les inscriptions au semestre
    # (pour avoir l'etat et le groupe) et aussi les inscriptions
    # au module (pour gerer les modules optionnels correctement)
    # E = get_evaluation_dict({"id":evaluation_id})[0]
    # M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
    # formsemestre_id = M["formsemestre_id"]
    # insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
    # insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"])
    # insmodset = set([x["etudid"] for x in insmod])
    # retire de insem ceux qui ne sont pas inscrits au module
    # ins = [i for i in insem if i["etudid"] in insmodset]

    notes = list(
        sco_evaluation_db.do_evaluation_get_all_notes(
            evaluation_id, filter_suppressed=False
        ).values()
    )
    notes_log = list(
        sco_evaluation_db.do_evaluation_get_all_notes(
            evaluation_id, filter_suppressed=False, table="notes_notes_log"
        ).values()
    )
    date_premiere_note = {}  # etudid : date
    for note in notes + notes_log:
        etudid = note["etudid"]
        if etudid in date_premiere_note:
            date_premiere_note[etudid] = min(note["date"], date_premiere_note[etudid])
        else:
            date_premiere_note[etudid] = note["date"]

    if not date_premiere_note:
        return None  # complete mais aucun etudiant non démissionnaires
    # complet au moment du max (date la plus tardive) des premieres dates de saisie
    return max(date_premiere_note.values())


def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
    """Experimental: un tableau indiquant pour chaque évaluation
    le nombre de jours avant la publication des notes.

    N'indique que les évaluations "normales" (pas rattrapage, ni bonus, ni session2,
    ni celles des modules de bonus/malus).
    """
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    evaluations = formsemestre.get_evaluations()
    rows = []
    for e in evaluations:
        if (e.evaluation_type != Evaluation.EVALUATION_NORMALE) or (
            e.moduleimpl.module.module_type == ModuleType.MALUS
        ):
            continue
        date_first_complete = evaluation_date_first_completion(e.id)
        if date_first_complete and e.date_fin:
            delai_correction = (date_first_complete.date() - e.date_fin.date()).days
        else:
            delai_correction = None

        rows.append(
            {
                "date_first_complete": date_first_complete,
                "delai_correction": delai_correction,
                "jour": (
                    e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "sans date"
                ),
                "_jour_target": url_for(
                    "notes.evaluation_listenotes",
                    scodoc_dept=g.scodoc_dept,
                    evaluation_id=e.id,
                ),
                "module_code": e.moduleimpl.module.code,
                "_module_code_target": url_for(
                    "notes.moduleimpl_status",
                    scodoc_dept=g.scodoc_dept,
                    moduleimpl_id=e.moduleimpl.id,
                ),
                "module_titre": e.moduleimpl.module.abbrev
                or e.moduleimpl.module.titre
                or "",
                "responsable_id": e.moduleimpl.responsable_id,
                "responsable_nomplogin": sco_users.user_info(
                    e.moduleimpl.responsable_id
                )["nomplogin"],
            }
        )

    columns_ids = (
        "module_code",
        "module_titre",
        "responsable_nomplogin",
        "jour",
        "date_first_complete",
        "delai_correction",
        "description",
    )
    titles = {
        "module_code": "Code",
        "module_titre": "Module",
        "responsable_nomplogin": "Responsable",
        "jour": "Date",
        "date_first_complete": "Fin saisie",
        "delai_correction": "Délai",
        "description": "Description",
    }
    tab = GenTable(
        titles=titles,
        columns_ids=columns_ids,
        rows=rows,
        html_class="table_leftalign table_coldate",
        html_sortable=True,
        html_title="<h2>Correction des évaluations du semestre</h2>",
        caption="Correction des évaluations du semestre",
        preferences=sco_preferences.SemPreferences(formsemestre_id),
        base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
        origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
        filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
        table_id="formsemestre_evaluations_delai_correction",
    )
    return tab.make_page(fmt=fmt)


#  -------------- VIEWS
def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True) -> str:
    """HTML description of evaluation, for page headers
    edit_in_place: allow in-place editing when permitted (not implemented)
    """
    evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
    modimpl = evaluation.moduleimpl
    responsable: User = db.session.get(User, modimpl.responsable_id)
    resp_nomprenom = responsable.get_prenomnom()
    resp_nomcomplet = responsable.get_nomcomplet()
    can_edit = modimpl.can_edit_notes(current_user, allow_ens=False)

    mod_descr = f"""<a class="stdlink" href="{url_for("notes.moduleimpl_status",
        scodoc_dept=g.scodoc_dept,
        moduleimpl_id=modimpl.id,
    )}">{modimpl.module.code or ""} {modimpl.module.abbrev or modimpl.module.titre or "?"}</a>
    <span class="resp">(resp. <a title="{resp_nomcomplet}">{resp_nomprenom}</a>)</span>
    <span class="evallink"><a class="stdlink"
        href="{url_for(
            "notes.evaluation_listenotes",
            scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
        }">voir toutes les notes du module</a></span>
    """

    eval_titre = f' "{evaluation.description}"' if evaluation.description else ""
    if modimpl.module.module_type == ModuleType.MALUS:
        eval_titre += ' <span class="eval_malus">(points de malus)</span>'
    H = [
        f"""<span class="eval_title">Évaluation{eval_titre}</span>
        <p><b>Module : {mod_descr}</b>
        </p>"""
    ]
    if modimpl.module.module_type == ModuleType.MALUS:
        # Indique l'UE
        ue = modimpl.module.ue
        H.append(f"<p><b>UE : {ue.acronyme}</b></p>")
    if (
        modimpl.module.module_type == ModuleType.MALUS
        or evaluation.evaluation_type == Evaluation.EVALUATION_BONUS
    ):
        # store min/max values used by JS client-side checks:
        H.append(
            """<span id="eval_note_min" class="sco-hidden">-20.</span>
            <span id="eval_note_max" class="sco-hidden">20.</span>"""
        )
    else:
        # date et absences (pas pour evals bonus ni des modules de malus)
        if evaluation.date_debut is not None:
            H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
            group_id = sco_groups.get_default_group(modimpl.formsemestre_id)
            H.append(
                f"""<span class="evallink"><a class="stdlink" href="{url_for(
                    'assiduites.etat_abs_date',
                    scodoc_dept=g.scodoc_dept,
                    group_ids=group_id,
                    evaluation_id=evaluation.id,
                    date_debut=evaluation.date_debut.isoformat(),
                    date_fin=evaluation.date_fin.isoformat() if evaluation.date_fin else "",
                    )
                    }">absences ce jour</a>
                    </span>
                    <span class="evallink"><a class="stdlink" href="{url_for(
                        'notes.evaluation_check_absences_html',
                        scodoc_dept=g.scodoc_dept,
                        evaluation_id = evaluation.id)
                    }">vérifier notes vs absences</a>
                    </span>
                    """
            )
        else:
            H.append("<p><em>sans date</em> ")

        H.append(
            f"""</p><p>Coefficient dans le module: <b>{evaluation.coefficient or "0"}</b>,
            notes sur <span id="eval_note_max">{(evaluation.note_max or 0):g}</span> """
        )
        H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
    if can_edit:
        H.append(
            f"""
            <a class="stdlink" href="{url_for(
                "notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
            }">modifier l'évaluation</a>
            """
        )
        if link_saisie:
            H.append(
                f"""
            <a style="margin-left: 12px;" class="stdlink" href="{url_for(
                "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
            }">saisie des notes</a>
            """
            )
    H.append("</p>")

    return '<div class="eval_description">' + "\n".join(H) + "</div>"