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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 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_login import current_user
from flask import request

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

import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
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_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
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 = ListMedian(notes)
    mini = min(notes)
    maxi = max(notes)
    return moy, median, mini, maxi


def ListMedian(L):
    """Median of a list L"""
    n = len(L)
    if not n:
        raise ValueError("empty list")
    L.sort()
    if n % 2:
        return L[n // 2]
    else:
        return (L[n // 2] + L[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
        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
    E = sco_evaluation_db.get_evaluations_dict(args={"evaluation_id": evaluation_id})[0]
    M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
    Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
    is_malus = Mod["module_type"] == ModuleType.MALUS  # True si module de malus
    formsemestre_id = M["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=E["moduleimpl_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, E["note_max"])
        mini = scu.fmt_note(mini_num, E["note_max"])
        maxi = scu.fmt_note(maxi_num, E["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)
    GrNbMissing = collections.defaultdict(int)  # group_id : nb notes manquantes
    GrNotes = collections.defaultdict(list)  # group_id: liste notes valides
    TotalNbMissing = 0
    TotalNbAtt = 0
    groups = {}  # 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 groups:
            groups[group["group_id"]] = group
        #
        isMissing = False
        if i["etudid"] in etuds_notes_dict:
            val = etuds_notes_dict[i["etudid"]]["value"]
            if val == scu.NOTES_ATTENTE:
                isMissing = True
                TotalNbAtt += 1
            if group:
                GrNotes[group["group_id"]].append(val)
        else:
            if group:
                _ = GrNotes[group["group_id"]]  # create group
            isMissing = True
        if isMissing:
            TotalNbMissing += 1
            if group:
                GrNbMissing[group["group_id"]] += 1

    gr_incomplets = [x for x in GrNbMissing.keys()]
    gr_incomplets.sort()
    if (
        (TotalNbMissing > 0)
        and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
        and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
    ):
        complete = False
    else:
        complete = True

    complete = (
        (TotalNbMissing == 0)
        or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
        or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
    )
    evalattente = (TotalNbMissing > 0) and (
        (TotalNbMissing == TotalNbAtt) or E["publish_incomplete"]
    )
    # mais ne met pas en attente les evals immediates sans aucune notes:
    if E["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 GrNotes.items():
        gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
        gr_moyennes.append(
            {
                "group_id": group_id,
                "group_name": groups[group_id]["group_name"],
                "gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
                "gr_median": scu.fmt_note(gr_median, E["note_max"]),
                "gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
                "gr_maxi": scu.fmt_note(gr_maxi, E["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": groups,
        "evalcomplete": complete,
        "evalattente": evalattente,
        "is_malus": is_malus,
    }


def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
    """Liste les évaluations de tous les modules de ce semestre.
       Triée par module, numero desc, date_debut desc
       Donne pour chaque eval son état (voir do_evaluation_etat)
       { evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }

       Exemple:
       [ {
       'coefficient': 1.0,
       'description': 'QCM et cas pratiques',
       'etat': {
             'evalattente': False,
             'evalcomplete': True,
             'evaluation_id': 'GEAEVAL82883',
             'gr_incomplets': [],
             'gr_moyennes': [{
                'gr_median': '12.00', # sur 20
                'gr_moy': '11.88',
                'gr_nb_att': 0,
                'gr_nb_notes': 166,
                'group_id': 'GEAG266762',
                'group_name': None
             }],
             'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
                                       'group_id': 'GEAG266762',
                                       'group_name': None,
                                       'partition_id': 'GEAP266761'}
              },
             'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16),
             'median': '12.00',
             'moy': '11.84',
             'nb_abs': 2,
             'nb_att': 0,
             'nb_inscrits': 166,
             'nb_neutre': 0,
             'nb_notes': 168,
             'nb_notes_total': 169
     },
    'evaluation_id': 'GEAEVAL82883',
    'evaluation_type': 0,
    'heure_debut': datetime.time(8, 0),
    'heure_fin': datetime.time(9, 30),
    'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
    'moduleimpl_id': 'GEAMIP80490',
    'note_max': 20.0,
    'numero': 0,
    'publish_incomplete': 0,
    'visibulletin': 1} ]

    """
    req = """SELECT E.id AS evaluation_id, E.*
    FROM notes_evaluation E, notes_moduleimpl MI 
    WHERE MI.formsemestre_id = %(formsemestre_id)s 
    and MI.id = E.moduleimpl_id 
    ORDER BY MI.id, numero desc, date_debut desc
    """
    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(req, {"formsemestre_id": formsemestre_id})
    res = cursor.dictfetchall()
    # etat de chaque evaluation:
    for r in res:
        if with_etat:
            r["etat"] = do_evaluation_etat(r["evaluation_id"])
            r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)

    return res


def _eval_etat(evals):
    """evals: list of mappings (etats)
    -> nb_eval_completes, nb_evals_en_cours,
    nb_evals_vides, 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 = 0, 0, 0
    dates = []
    for e in evals:
        if e["etat"]["evalcomplete"]:
            nb_evals_completes += 1
        elif e["etat"]["nb_notes"] == 0:
            nb_evals_vides += 1
        else:
            nb_evals_en_cours += 1
        last_modif = e["etat"]["last_modif"]
        if last_modif is not None:
            dates.append(e["etat"]["last_modif"])

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

    return {
        "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_id):
    """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
    date derniere modif, attente

    XXX utilisé par
    - formsemestre_status_head
    - gen_formsemestre_recapcomplet_xml
    - gen_formsemestre_recapcomplet_json

    "nb_evals_completes"
    "nb_evals_en_cours"
    "nb_evals_vides"
    "date_derniere_note"
    "last_modif"
    "attente"
    """
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    evals = nt.get_evaluations_etats()
    etat = _eval_etat(evals)
    # Ajoute information sur notes en attente
    etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
    return etat


def do_evaluation_etat_in_mod(nt, moduleimpl_id):
    """"""
    evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
    etat = _eval_etat(evals)
    # Il y a-t-il des notes en attente dans ce module ?
    etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
    return etat


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()  # TODO
    nb_evals = len(evaluations)

    color_incomplete = "#FF6060"
    color_complete = "#A0FFA0"
    color_futur = "#70E0FF"

    year = formsemestre.annee_scolaire()
    events = {}  # (day, halfday) : event
    for e in evaluations:
        if e.date_debut is None:
            continue  # éval. sans date
        txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
        if e.date_debut == e.date_fin:
            heure_debut_txt, heure_fin_txt = "?", "?"
        else:
            heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
            heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"

        description = f"""{
            e.moduleimpl.module.titre
        }, de {heure_debut_txt} à {heure_fin_txt}"""

        # Etat (notes completes) de l'évaluation:
        modimpl_result = nt.modimpls_results[e.moduleimpl.id]
        if modimpl_result.evaluations_etat[e.id].is_complete:
            color = color_complete
        else:
            color = color_incomplete
        if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
            color = color_futur
        href = url_for(
            "notes.moduleimpl_status",
            scodoc_dept=g.scodoc_dept,
            moduleimpl_id=e.moduleimpl_id,
        )
        day = e.date_debut.date().isoformat()  # yyyy-mm-dd
        event = events.get(day)
        if not event:
            events[day] = [day, txt, color, href, description, e.moduleimpl]
        else:
            if event[-1].id != e.moduleimpl.id:
                # plusieurs evals de modules differents a la meme date
                event[1] += ", " + txt
                event[4] += ", " + description
                if color == color_incomplete:
                    event[2] = color_incomplete
                if color == color_futur:
                    event[2] = color_futur

    cal_html = sco_cal.YearTable(
        year, events=list(events.values()), halfday=False, pad_width=None
    )

    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: {color_incomplete}">rouge</span> 
        les évaluations passées auxquelles il manque des notes
        </li>
        <li>en <span style=
            "background-color: {color_complete}">vert</span>
        les évaluations déjà notées
        </li>
        <li>en <span style=
            "background-color: {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 pas les évaluations de rattrapage 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 != scu.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("%d/%m/%Y")
                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,
                "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()),
    )
    return tab.make_page(fmt=fmt)


#  -------------- VIEWS
def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
    """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>")
        # 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 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,
                    desc=evaluation.description or "",
                    date_debut=evaluation.date_debut.isoformat(),
                    date_fin=evaluation.date_fin.isoformat(),
                    )
                    }">absences ce jour</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>"