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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 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
#
##############################################################################

"""Saisie des notes

   Formulaire revu en juillet 2016
"""
import time
import datetime
import psycopg2

import sco_utils as scu
from notes_log import log
from TrivialFormulator import TrivialFormulator, TF
from notesdb import ScoDocCursor, quote_dict, DateISOtoDMY, DateDMYtoISO
from sco_exceptions import (
    InvalidNoteValue,
    AccessDenied,
    NoteProcessError,
    ScoValueError,
)
from sco_permissions import ScoEditAllNotes
import sco_formsemestre
import sco_moduleimpl
import sco_groups
import sco_groups_view
import sco_evaluations
import sco_parcours_dut
import sco_undo_notes
import htmlutils
import sco_excel
import scolars
import sco_news
from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC


def can_edit_notes(context, authuser, moduleimpl_id, allow_ens=True):
    """True if authuser can enter or edit notes in this module.
    If allow_ens, grant access to all ens in this module

    Si des décisions de jury ont déjà été saisies dans ce semestre,
    seul le directeur des études peut saisir des notes (et il ne devrait pas).
    """
    uid = str(authuser)
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0]
    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
    if sem["etat"] != "1":
        return False  # semestre verrouillé

    if sco_parcours_dut.formsemestre_has_decisions(context, sem["formsemestre_id"]):
        # il y a des décisions de jury dans ce semestre !
        return (
            authuser.has_permission(ScoEditAllNotes, context)
            or uid in sem["responsables"]
        )
    else:
        if (
            (not authuser.has_permission(ScoEditAllNotes, context))
            and uid != M["responsable_id"]
            and uid not in sem["responsables"]
        ):
            # enseignant (chargé de TD) ?
            if allow_ens:
                for ens in M["ens"]:
                    if ens["ens_id"] == uid:
                        return True
            return False
        else:
            return True


def convert_note_from_string(
    note,
    note_max,
    note_min=scu.NOTES_MIN,
    etudid=None,
    absents=[],
    tosuppress=[],
    invalids=[],
):
    """converti une valeur (chaine saisie) vers une note numérique (float)
    Les listes absents, tosuppress et invalids sont modifiées
    """
    invalid = False
    note_value = None
    note = note.replace(",", ".")
    if note[:3] == "ABS":
        note_value = None
        absents.append(etudid)
    elif note[:3] == "NEU" or note[:3] == "EXC":
        note_value = scu.NOTES_NEUTRALISE
    elif note[:3] == "ATT":
        note_value = scu.NOTES_ATTENTE
    elif note[:3] == "SUP":
        note_value = scu.NOTES_SUPPRESS
        tosuppress.append(etudid)
    else:
        try:
            note_value = float(note)
            if (note_value < note_min) or (note_value > note_max):
                raise ValueError
        except:
            invalids.append(etudid)
            invalid = True

    return note_value, invalid


def _displayNote(val):
    """Convert note from DB to viewable string.
    Utilisé seulement pour I/O vers formulaires (sans perte de precision)
    (Utiliser fmt_note pour les affichages)
    """
    if val is None:
        val = "ABS"
    elif val == scu.NOTES_NEUTRALISE:
        val = "EXC"  # excuse, note neutralise
    elif val == scu.NOTES_ATTENTE:
        val = "ATT"  # attente, note neutralise
    elif val == scu.NOTES_SUPPRESS:
        val = "SUPR"
    else:
        val = "%g" % val
    return val


def _check_notes(notes, evaluation, mod):
    """notes is a list of tuples (etudid, value)
    mod is the module (used to ckeck type, for malus)
    returns list of valid notes (etudid, float value)
    and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
    """
    note_max = evaluation["note_max"]
    if mod["module_type"] == scu.MODULE_STANDARD:
        note_min = scu.NOTES_MIN
    elif mod["module_type"] == scu.MODULE_MALUS:
        note_min = -20.0
    else:
        raise ValueError("Invalid module type")  # bug
    L = []  # liste (etudid, note) des notes ok (ou absent)
    invalids = []  # etudid avec notes invalides
    withoutnotes = []  # etudid sans notes (champs vides)
    absents = []  # etudid absents
    tosuppress = []  # etudids avec ancienne note à supprimer

    for (etudid, note) in notes:
        note = str(note).strip().upper()
        if note[:3] == "DEM":
            continue  # skip !
        if note:
            value, invalid = convert_note_from_string(
                note,
                note_max,
                note_min=note_min,
                etudid=etudid,
                absents=absents,
                tosuppress=tosuppress,
                invalids=invalids,
            )
            if not invalid:
                L.append((etudid, value))
        else:
            withoutnotes.append(etudid)

    return L, invalids, withoutnotes, absents, tosuppress


def do_evaluation_upload_xls(context, REQUEST):
    """
    Soumission d'un fichier XLS (evaluation_id, notefile)
    """
    authuser = REQUEST.AUTHENTICATED_USER
    evaluation_id = REQUEST.form["evaluation_id"]
    comment = REQUEST.form["comment"]
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
    M = sco_moduleimpl.do_moduleimpl_withmodule_list(
        context, moduleimpl_id=E["moduleimpl_id"]
    )[0]
    # Check access
    # (admin, respformation, and responsable_id)
    if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
        # XXX imaginer un redirect + msg erreur
        raise AccessDenied("Modification des notes impossible pour %s" % authuser)
    #
    data = REQUEST.form["notefile"].read()
    diag, lines = sco_excel.Excel_to_list(data)
    try:
        if not lines:
            raise InvalidNoteValue()
        # -- search eval code
        n = len(lines)
        i = 0
        while i < n:
            if not lines[i]:
                diag.append("Erreur: format invalide (ligne vide ?)")
                raise InvalidNoteValue()
            f0 = lines[i][0].strip()
            if f0 and f0[0] == "!":
                break
            i = i + 1
        if i == n:
            diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
            raise InvalidNoteValue()

        eval_id = lines[i][0].strip()[1:]
        if eval_id != evaluation_id:
            diag.append(
                "Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('%s' != '%s')"
                % (eval_id, evaluation_id)
            )
            raise InvalidNoteValue()
        # --- get notes -> list (etudid, value)
        # ignore toutes les lignes ne commençant pas par !
        notes = []
        ni = i + 1
        try:
            for line in lines[i + 1 :]:
                if line:
                    cell0 = line[0].strip()
                    if cell0 and cell0[0] == "!":
                        etudid = cell0[1:]
                        if len(line) > 4:
                            val = line[4].strip()
                        else:
                            val = ""  # ligne courte: cellule vide
                        if etudid:
                            notes.append((etudid, val))
                ni += 1
        except:
            diag.append(
                'Erreur: feuille invalide ! (erreur ligne %d)<br/>"%s"'
                % (ni, str(lines[ni]))
            )
            raise InvalidNoteValue()
        # -- check values
        L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
        if len(invalids):
            diag.append(
                "Erreur: la feuille contient %d notes invalides</p>" % len(invalids)
            )
            if len(invalids) < 25:
                etudsnames = [
                    context.getEtudInfo(etudid=etudid, filled=True)[0]["nomprenom"]
                    for etudid in invalids
                ]
                diag.append("Notes invalides pour: " + ", ".join(etudsnames))
            raise InvalidNoteValue()
        else:
            nb_changed, nb_suppress, existing_decisions = _notes_add(
                context, authuser, evaluation_id, L, comment
            )
            # news
            E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
            M = sco_moduleimpl.do_moduleimpl_list(
                context, moduleimpl_id=E["moduleimpl_id"]
            )[0]
            mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
            mod["moduleimpl_id"] = M["moduleimpl_id"]
            mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
            sco_news.add(
                context,
                REQUEST,
                typ=NEWS_NOTE,
                object=M["moduleimpl_id"],
                text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
                url=mod["url"],
            )

            msg = (
                "<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
                % (nb_changed, len(withoutnotes), len(absents), nb_suppress)
            )
            if existing_decisions:
                msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
            # msg += '<p>' + str(notes) # debug
            return 1, msg

    except InvalidNoteValue:
        if diag:
            msg = (
                '<ul class="tf-msg"><li class="tf_msg">'
                + '</li><li class="tf_msg">'.join(diag)
                + "</li></ul>"
            )
        else:
            msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
        return 0, msg + "<p>(pas de notes modifiées)</p>"


def do_evaluation_set_missing(
    context, evaluation_id, value, REQUEST=None, dialog_confirmed=False
):
    """Initialisation des notes manquantes"""
    authuser = REQUEST.AUTHENTICATED_USER
    evaluation_id = REQUEST.form["evaluation_id"]
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
    M = sco_moduleimpl.do_moduleimpl_withmodule_list(
        context, moduleimpl_id=E["moduleimpl_id"]
    )[0]
    # Check access
    # (admin, respformation, and responsable_id)
    if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
        # XXX imaginer un redirect + msg erreur
        raise AccessDenied("Modification des notes impossible pour %s" % authuser)
    #
    NotesDB = context._notes_getall(evaluation_id)
    etudids = sco_groups.do_evaluation_listeetuds_groups(
        context, evaluation_id, getallstudents=True, include_dems=False
    )
    notes = []
    for etudid in etudids:  # pour tous les inscrits
        if not NotesDB.has_key(etudid):  # pas de note
            notes.append((etudid, value))
    # Check value
    L, invalids, _, _, _ = _check_notes(notes, E, M["module"])
    diag = ""
    if len(invalids):
        diag = "Valeur %s invalide" % value
    if diag:
        return (
            context.sco_header(REQUEST)
            + '<h2>%s</h2><p><a href="saisie_notes?evaluation_id=%s">Recommencer</a>'
            % (diag, evaluation_id)
            + context.sco_footer(REQUEST)
        )
    # Confirm action
    if not dialog_confirmed:
        return context.confirmDialog(
            """<h2>Mettre toutes les notes manquantes de l'évaluation
            à la valeur %s ?</h2>
            <p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
            n'a été rentrée seront affectés.</p>
            <p><b>%d étudiants concernés par ce changement de note.</b></p>
            <p class="warning">Attention, les étudiants sans notes de tous les groupes de ce semestre seront affectés.</p>
            """
            % (value, len(L)),
            dest_url="",
            REQUEST=REQUEST,
            cancel_url="saisie_notes?evaluation_id=%s" % evaluation_id,
            parameters={"evaluation_id": evaluation_id, "value": value},
        )
    # ok
    comment = "Initialisation notes manquantes"
    nb_changed, _, _ = _notes_add(context, authuser, evaluation_id, L, comment)
    # news
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    mod["moduleimpl_id"] = M["moduleimpl_id"]
    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
    sco_news.add(
        context,
        REQUEST,
        typ=NEWS_NOTE,
        object=M["moduleimpl_id"],
        text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
        url=mod["url"],
    )
    return (
        context.sco_header(REQUEST)
        + """<h2>%d notes changées</h2>
               <ul>
               <li><a class="stdlink" href="saisie_notes?evaluation_id=%s">
               Revenir au formulaire de saisie des notes</a></li>
               <li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">
               Tableau de bord du module</a></li>
               </ul>
               """
        % (nb_changed, evaluation_id, M["moduleimpl_id"])
        + context.sco_footer(REQUEST)
    )


def evaluation_suppress_alln(context, evaluation_id, REQUEST, dialog_confirmed=False):
    "suppress all notes in this eval"
    authuser = REQUEST.AUTHENTICATED_USER
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]

    if can_edit_notes(context, authuser, E["moduleimpl_id"], allow_ens=False):
        # On a le droit de modifier toutes les notes
        # recupere les etuds ayant une note
        NotesDB = context._notes_getall(evaluation_id)
    elif can_edit_notes(context, authuser, E["moduleimpl_id"], allow_ens=True):
        # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
        NotesDB = context._notes_getall(evaluation_id, by_uid=str(authuser))
    else:
        raise AccessDenied("Modification des notes impossible pour %s" % authuser)

    notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]

    if not dialog_confirmed:
        nb_changed, nb_suppress, existing_decisions = _notes_add(
            context, authuser, evaluation_id, notes, do_it=False
        )
        msg = "<p>Confirmer la suppression des %d notes ?</p>" % nb_suppress
        if existing_decisions:
            msg += """<p class="warning">Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !</p>"""
        return context.confirmDialog(
            msg,
            dest_url="",
            REQUEST=REQUEST,
            OK="Supprimer les notes",
            cancel_url="moduleimpl_status?moduleimpl_id=%s" % E["moduleimpl_id"],
            parameters={"evaluation_id": evaluation_id},
        )

    # modif
    nb_changed, nb_suppress, existing_decisions = _notes_add(
        context, authuser, evaluation_id, notes, comment="effacer tout"
    )
    assert nb_changed == nb_suppress
    H = ["<p>%s notes supprimées</p>" % nb_suppress]
    if existing_decisions:
        H.append(
            """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
        )
    H += [
        '<p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">continuer</a>'
        % E["moduleimpl_id"]
    ]
    # news
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    mod["moduleimpl_id"] = M["moduleimpl_id"]
    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
    sco_news.add(
        context,
        REQUEST,
        typ=NEWS_NOTE,
        object=M["moduleimpl_id"],
        text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
        % mod,
        url=mod["url"],
    )

    return context.sco_header(REQUEST) + "\n".join(H) + context.sco_footer(REQUEST)


def _notes_add(context, uid, evaluation_id, notes, comment=None, do_it=True):
    """
    Insert or update notes
    notes is a list of tuples (etudid,value)
    If do_it is False, simulate the process and returns the number of values that
    WOULD be changed or suppressed.
    Nota:
    - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
    Return number of changed notes
    """
    uid = str(uid)
    now = apply(
        psycopg2.Timestamp, time.localtime()[:6]
    )  # datetime.datetime.now().isoformat()
    # Verifie inscription et valeur note
    _ = {}.fromkeys(
        sco_groups.do_evaluation_listeetuds_groups(
            context, evaluation_id, getallstudents=True, include_dems=True
        )
    )
    for (etudid, value) in notes:
        if not ((value is None) or (type(value) == type(1.0))):
            raise NoteProcessError(
                "etudiant %s: valeur de note invalide (%s)" % (etudid, value)
            )
    # Recherche notes existantes
    NotesDB = context._notes_getall(evaluation_id)
    # Met a jour la base
    cnx = context.GetDBConnexion(autocommit=False)
    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
    nb_changed = 0
    nb_suppress = 0
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    existing_decisions = (
        []
    )  # etudids pour lesquels il y a une decision de jury et que la note change
    try:
        for (etudid, value) in notes:
            changed = False
            if not NotesDB.has_key(etudid):
                # nouvelle note
                if value != scu.NOTES_SUPPRESS:
                    if do_it:
                        aa = {
                            "etudid": etudid,
                            "evaluation_id": evaluation_id,
                            "value": value,
                            "comment": comment,
                            "uid": uid,
                            "date": now,
                        }
                        quote_dict(aa)
                        cursor.execute(
                            "insert into notes_notes (etudid,evaluation_id,value,comment,date,uid) values (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)",
                            aa,
                        )
                    changed = True
            else:
                # il y a deja une note
                oldval = NotesDB[etudid]["value"]
                if type(value) != type(oldval):
                    changed = True
                elif type(value) == type(1.0) and (
                    abs(value - oldval) > scu.NOTES_PRECISION
                ):
                    changed = True
                elif value != oldval:
                    changed = True
                if changed:
                    # recopie l'ancienne note dans notes_notes_log, puis update
                    if do_it:
                        cursor.execute(
                            "insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) select etudid,evaluation_id,value,comment,date,uid from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
                            {"etudid": etudid, "evaluation_id": evaluation_id},
                        )
                        aa = {
                            "etudid": etudid,
                            "evaluation_id": evaluation_id,
                            "value": value,
                            "date": now,
                            "comment": comment,
                            "uid": uid,
                        }
                        quote_dict(aa)
                    if value != scu.NOTES_SUPPRESS:
                        if do_it:
                            cursor.execute(
                                "update notes_notes set value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
                                aa,
                            )
                    else:  # suppression ancienne note
                        if do_it:
                            log(
                                "_notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
                                % (evaluation_id, etudid, oldval)
                            )
                            cursor.execute(
                                "delete from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
                                aa,
                            )
                            # garde trace de la suppression dans l'historique:
                            aa["value"] = scu.NOTES_SUPPRESS
                            cursor.execute(
                                "insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) values (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)",
                                aa,
                            )
                        nb_suppress += 1
            if changed:
                nb_changed += 1
                if has_existing_decision(context, M, E, etudid):
                    existing_decisions.append(etudid)
    except:
        log("*** exception in _notes_add")
        if do_it:
            # inval cache
            context._inval_cache(
                formsemestre_id=M["formsemestre_id"]
            )  # > modif notes (exception)
            cnx.rollback()  # abort
        raise  # re-raise exception
    if do_it:
        cnx.commit()
        context._inval_cache(formsemestre_id=M["formsemestre_id"])  # > modif notes
        context.get_evaluations_cache().inval_cache(key=evaluation_id)
    return nb_changed, nb_suppress, existing_decisions


def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None):
    """Saisie des notes via un fichier Excel"""
    authuser = REQUEST.AUTHENTICATED_USER
    authusername = str(authuser)
    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
    if not evals:
        raise ScoValueError("invalid evaluation_id")
    E = evals[0]
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    formsemestre_id = M["formsemestre_id"]
    if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
        return (
            context.sco_header(REQUEST)
            + "<h2>Modification des notes impossible pour %s</h2>" % authusername
            + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
               avez l'autorisation d'effectuer cette opération)</p>
               <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
               """
            % E["moduleimpl_id"]
            + context.sco_footer(REQUEST)
        )

    if E["description"]:
        page_title = 'Saisie des notes de "%s"' % E["description"]
    else:
        page_title = "Saisie des notes"

    # Informations sur les groupes à afficher:
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context,
        group_ids=group_ids,
        formsemestre_id=formsemestre_id,
        select_all_when_unspecified=True,
        etat=None,
        REQUEST=REQUEST,
    )

    H = [
        context.sco_header(
            REQUEST,
            page_title=page_title,
            javascripts=sco_groups_view.JAVASCRIPTS,
            cssstyles=sco_groups_view.CSSSTYLES,
            init_qtip=True,
        ),
        sco_evaluations.evaluation_describe(
            context, evaluation_id=evaluation_id, REQUEST=REQUEST
        ),
        """<span class="eval_title">Saisie des notes par fichier</span>""",
    ]

    # Menu choix groupe:
    H.append("""<div id="group-tabs"><table><tr><td>""")
    H.append(sco_groups_view.form_groups_choice(context, groups_infos))
    H.append("</td></tr></table></div>")

    H.append(
        """<div class="saisienote_etape1">
        <span class="titredivsaisienote">Etape 1 : </span>
        <ul>
        <li><a href="feuille_saisie_notes?evaluation_id=%s&%s" class="stdlink" id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a></li>
        <li>ou <a class="stdlink" href="saisie_notes?evaluation_id=%s">aller au formulaire de saisie</a></li>
        </ul>
        </div>
        <form><input type="hidden" name="evaluation_id" id="formnotes_evaluation_id" value="%s"/></form>
        """
        % (evaluation_id, groups_infos.groups_query_args, evaluation_id, evaluation_id)
    )

    H.append(
        """<div class="saisienote_etape2">
    <span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>"""  #'
    )

    nf = TrivialFormulator(
        REQUEST.URL0,
        REQUEST.form,
        (
            ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
            (
                "notefile",
                {"input_type": "file", "title": "Fichier de note (.xls)", "size": 44},
            ),
            (
                "comment",
                {
                    "size": 44,
                    "title": "Commentaire",
                    "explanation": "(la colonne remarque du fichier excel est ignorée)",
                },
            ),
        ),
        formid="notesfile",
        submitlabel="Télécharger",
    )
    if nf[0] == 0:
        H.append(
            """<p>Le fichier doit être un fichier tableur obtenu via
        l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
        </p>"""
        )
        H.append(nf[1])
    elif nf[0] == -1:
        H.append("<p>Annulation</p>")
    elif nf[0] == 1:
        updiag = do_evaluation_upload_xls(context, REQUEST)
        if updiag[0]:
            H.append(updiag[1])
            H.append(
                """<p>Notes chargées.&nbsp;&nbsp;&nbsp;
            <a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">
            Revenir au tableau de bord du module</a>
            &nbsp;&nbsp;&nbsp;
            <a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
            </p>"""
                % E
            )
        else:
            H.append("""<p class="redboldtext">Notes non chargées !</p>""" + updiag[1])
            H.append(
                """
            <p><a class="stdlink" href="saisie_notes_tableur?evaluation_id=%(evaluation_id)s">
            Reprendre</a>
            </p>"""
                % E
            )
    #
    H.append("""</div><h3>Autres opérations</h3><ul>""")
    if can_edit_notes(
        context, REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
    ):
        H.append(
            """
        <li>
        <form action="do_evaluation_set_missing" method="get">
        Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
        <input type="submit" value="OK"/> 
        <input type="hidden" name="evaluation_id" value="%s"/> 
        <em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
        </form>
        </li>        
        <li><a class="stdlink" href="evaluation_suppress_alln?evaluation_id=%s">Effacer toutes les notes de cette évaluation</a> (ceci permet ensuite de supprimer l'évaluation si besoin)
        </li>"""
            % (evaluation_id, evaluation_id)
        )  #'
    H.append(
        """<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a></li>
    <li><a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Revenir au formulaire de saisie</a></li>
    </ul>"""
        % E
    )

    H.append(
        """<h3>Explications</h3>
<ol>
<li>Etape 1: 
<ol><li>choisir le ou les groupes d'étudiants;</li>
    <li>télécharger le fichier Excel à remplir.</li>
</ol>
</li>
<li>Etape 2 (cadre vert): Indiquer le fichier Excel <em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes. Remarques:
<ul>
<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;</li>
<li>seules les valeurs des notes modifiées sont prises en compte;</li>
<li>seules les notes sont extraites du fichier Excel;</li>
<li>on peut optionnellement ajouter un commentaire (type "copies corrigées par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
</li>
<li>le fichier Excel <em>doit impérativement être celui chargé à l'étape 1 pour cette évaluation</em>. Il n'est pas possible d'utiliser une liste d'appel ou autre document Excel téléchargé d'une autre page.</li>
</ul>
</li>
</ol>
"""
    )
    H.append(context.sco_footer(REQUEST))
    return "\n".join(H)


def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
    """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
    if not evals:
        raise ScoValueError("invalid evaluation_id")
    E = evals[0]
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    formsemestre_id = M["formsemestre_id"]
    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
    if E["jour"]:
        indication_date = DateDMYtoISO(E["jour"])
    else:
        indication_date = scu.sanitize_filename(E["description"])[:12]
    evalname = "%s-%s" % (Mod["code"], indication_date)

    if E["description"]:
        evaltitre = "%s du %s" % (E["description"], E["jour"])
    else:
        evaltitre = "évaluation du %s" % E["jour"]
    description = "%s en %s (%s) resp. %s" % (
        evaltitre,
        Mod["abbrev"],
        Mod["code"],
        scu.strcapitalize(M["responsable_id"]),
    )

    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context,
        group_ids=group_ids,
        formsemestre_id=formsemestre_id,
        select_all_when_unspecified=True,
        etat=None,
        REQUEST=REQUEST,
    )
    groups = sco_groups.listgroups(context, groups_infos.group_ids)
    gr_title_filename = sco_groups.listgroups_filename(groups)
    # gr_title = sco_groups.listgroups_abbrev(groups)
    if None in [g["group_name"] for g in groups]:  # tous les etudiants
        getallstudents = True
        # gr_title = "tous"
        gr_title_filename = "tous"
    else:
        getallstudents = False
    etudids = sco_groups.do_evaluation_listeetuds_groups(
        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True
    )

    # une liste de liste de chaines: lignes de la feuille de calcul
    L = []

    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
    for e in etuds:
        etudid = e["etudid"]
        groups = sco_groups.get_etud_groups(context, etudid, sem)
        grc = sco_groups.listgroups_abbrev(groups)

        L.append(
            [
                "%s" % etudid,
                scu.strupper(e["nom"]),
                scu.strcapitalize(scu.strlower(e["prenom"])),
                e["inscr"]["etat"],
                grc,
                e["val"],
                e["explanation"],
            ]
        )

    filename = "notes_%s_%s.xls" % (evalname, gr_title_filename)
    xls = sco_excel.Excel_feuille_saisie(E, sem["titreannee"], description, lines=L)
    return sco_excel.sendExcelFile(REQUEST, xls, filename)


def has_existing_decision(context, M, E, etudid):
    """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
    Si oui, return True
    """
    formsemestre_id = M["formsemestre_id"]
    nt = context._getNotesCache().get_NotesTable(
        context, formsemestre_id
    )  # > get_etud_decision_sem, get_etud_decision_ues
    if nt.get_etud_decision_sem(etudid):
        return True
    dec_ues = nt.get_etud_decision_ues(etudid)
    if dec_ues:
        mod = context.do_module_list({"module_id": M["module_id"]})[0]
        ue_id = mod["ue_id"]
        if ue_id in dec_ues:
            return True  # decision pour l'UE a laquelle appartient cette evaluation

    return False  # pas de decision de jury affectee par cette note


# -----------------------------
# Nouveau formulaire saisie notes (2016)


def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
    """Formulaire saisie notes d'une évaluation pour un groupe"""
    authuser = REQUEST.AUTHENTICATED_USER
    authusername = str(authuser)

    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
    if not evals:
        raise ScoValueError("invalid evaluation_id")
    E = evals[0]
    M = sco_moduleimpl.do_moduleimpl_withmodule_list(
        context, moduleimpl_id=E["moduleimpl_id"]
    )[0]
    formsemestre_id = M["formsemestre_id"]
    # Check access
    # (admin, respformation, and responsable_id)
    if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
        return (
            context.sco_header(REQUEST)
            + "<h2>Modification des notes impossible pour %s</h2>" % authusername
            + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
               avez l'autorisation d'effectuer cette opération)</p>
               <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
               """
            % E["moduleimpl_id"]
            + context.sco_footer(REQUEST)
        )

    # Informations sur les groupes à afficher:
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context,
        group_ids=group_ids,
        formsemestre_id=formsemestre_id,
        select_all_when_unspecified=True,
        etat=None,
        REQUEST=REQUEST,
    )

    if E["description"]:
        page_title = 'Saisie "%s"' % E["description"]
    else:
        page_title = "Saisie des notes"

    # HTML page:
    H = [
        context.sco_header(
            REQUEST,
            page_title=page_title,
            javascripts=sco_groups_view.JAVASCRIPTS + ["js/saisie_notes.js"],
            cssstyles=sco_groups_view.CSSSTYLES,
            init_qtip=True,
        ),
        sco_evaluations.evaluation_describe(
            context, evaluation_id=evaluation_id, REQUEST=REQUEST
        ),
        '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
    ]
    H.append("""<div id="group-tabs"><table><tr><td>""")
    H.append(sco_groups_view.form_groups_choice(context, groups_infos))
    H.append('</td><td style="padding-left: 35px;">')
    H.append(
        htmlutils.make_menu(
            "Autres opérations",
            [
                {
                    "title": "Saisie par fichier tableur",
                    "id": "menu_saisie_tableur",
                    "url": "/saisie_notes_tableur?evaluation_id=%s&%s"
                    % (E["evaluation_id"], groups_infos.groups_query_args),
                },
                {
                    "title": "Voir toutes les notes du module",
                    "url": "/evaluation_listenotes?moduleimpl_id=%s"
                    % E["moduleimpl_id"],
                },
                {
                    "title": "Effacer toutes les notes de cette évaluation",
                    "url": "/evaluation_suppress_alln?evaluation_id=%s"
                    % (E["evaluation_id"],),
                },
            ],
            base_url=context.absolute_url(),
            alone=True,
        )
    )
    H.append("""</td></tr></table></div>""")

    # Le formulaire de saisie des notes:
    destination = "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % (
        context.ScoURL(),
        E["moduleimpl_id"],
    )
    form = _form_saisie_notes(
        context, E, M, groups_infos.group_ids, destination=destination, REQUEST=REQUEST
    )
    if form is None:
        return REQUEST.RESPONSE.redirect(destination)
    H.append(form)
    #
    H.append("</div>")  # /saisie_notes

    H.append(
        """<div class="sco_help">
    <p>Les modifications sont enregistrées au fur et à mesure.</p>
    <h4>Codes spéciaux:</h4>
    <ul>
    <li>ABS: absent (compte comme un zéro)</li>
    <li>EXC: excusé (note neutralisée)</li>
    <li>SUPR: pour supprimer une note existante</li>
    <li>ATT: note en attente (permet de publier une évaluation avec des notes manquantes)</li>
    </ul>
    </div>"""
    )

    H.append(context.sco_footer(REQUEST))
    return "\n".join(H)


def _get_sorted_etuds(context, E, etudids, formsemestre_id):
    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
    NotesDB = context._notes_getall(E["evaluation_id"])  # Notes existantes
    cnx = context.GetDBConnexion()
    etuds = []
    for etudid in etudids:
        # infos identite etudiant
        e = scolars.etudident_list(cnx, {"etudid": etudid})[0]
        scolars.format_etud_ident(e)
        etuds.append(e)
        # infos inscription dans ce semestre
        e["inscr"] = context.do_formsemestre_inscription_list(
            {"etudid": etudid, "formsemestre_id": formsemestre_id}
        )[0]
        # Groupes auxquels appartient cet étudiant:
        e["groups"] = sco_groups.get_etud_groups(context, etudid, sem)

        # Information sur absence (tenant compte de la demi-journée)
        jour_iso = DateDMYtoISO(E["jour"])
        warn_abs_lst = []
        if E["matin"]:
            nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=1)
            nbabsjust = context.Absences.CountAbsJust(
                etudid, jour_iso, jour_iso, matin=1
            )
            if nbabs:
                if nbabsjust:
                    warn_abs_lst.append("absent justifié le matin !")
                else:
                    warn_abs_lst.append("absent le matin !")
        if E["apresmidi"]:
            nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=0)
            nbabsjust = context.Absences.CountAbsJust(
                etudid, jour_iso, jour_iso, matin=0
            )
            if nbabs:
                if nbabsjust:
                    warn_abs_lst.append("absent justifié l'après-midi !")
                else:
                    warn_abs_lst.append("absent l'après-midi !")

        e["absinfo"] = '<span class="sn_abs">' + " ".join(warn_abs_lst) + "</span>  "

        # Note actuelle de l'étudiant:
        if NotesDB.has_key(etudid):
            e["val"] = _displayNote(NotesDB[etudid]["value"])
            comment = NotesDB[etudid]["comment"]
            if comment is None:
                comment = ""
            e["explanation"] = "%s (%s) %s" % (
                NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
                NotesDB[etudid]["uid"],
                comment,
            )
        else:
            e["val"] = ""
            e["explanation"] = ""
        # Démission ?
        if e["inscr"]["etat"] == "D":
            # if not e['val']:
            e["val"] = "DEM"
            e["explanation"] = "Démission"

    etuds.sort(key=lambda x: (x["nom"], x["prenom"]))

    return etuds


def _form_saisie_notes(context, E, M, group_ids, destination="", REQUEST=None):
    """Formulaire HTML saisie des notes  dans l'évaluation E du moduleimpl M
    pour les groupes indiqués.

    On charge tous les étudiants, ne seront montrés que ceux
    des groupes sélectionnés grace a un filtre en javascript.
    """
    evaluation_id = E["evaluation_id"]
    formsemestre_id = M["formsemestre_id"]

    etudids = sco_groups.do_evaluation_listeetuds_groups(
        context, evaluation_id, getallstudents=True, include_dems=True
    )
    if not etudids:
        return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'

    # Decisions de jury existantes ?
    decisions_jury = {
        etudid: has_existing_decision(context, M, E, etudid) for etudid in etudids
    }
    nb_decisions = sum(
        decisions_jury.values()
    )  # Nb de decisions de jury (pour les inscrits à l'évaluation)

    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)

    # Build form:
    descr = [
        ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
        ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
        ("group_ids", {"default": group_ids, "input_type": "hidden", "type": "list"}),
        # ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}),
        ("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
        ("changed", {"default": "0", "input_type": "hidden"}),  # changed in JS
    ]
    if M["module"]["module_type"] == scu.MODULE_STANDARD:
        descr.append(
            (
                "s3",
                {
                    "input_type": "text",  # affiche le barème
                    "title": "Notes ",
                    "cssclass": "formnote_bareme",
                    "readonly": True,
                    "default": "&nbsp;/ %g" % E["note_max"],
                },
            )
        )
    elif M["module"]["module_type"] == scu.MODULE_MALUS:
        descr.append(
            (
                "s3",
                {
                    "input_type": "text",  # affiche le barème
                    "title": "",
                    "cssclass": "formnote_bareme",
                    "readonly": True,
                    "default": "Points de malus (soustraits à la moyenne de l'UE, entre -20 et 20)",
                },
            )
        )
    else:
        raise ValueError("invalid module type (%s)" % M["module"]["module_type"])  # bug

    initvalues = {}
    for e in etuds:
        etudid = e["etudid"]
        disabled = e["val"] == "DEM"
        etud_classes = []
        if disabled:
            classdem = " etud_dem"
            etud_classes.append("etud_dem")
            disabled_attr = 'disabled="%d"' % disabled
        else:
            classdem = ""
            disabled_attr = ""
        # attribue a chaque element une classe css par groupe:
        for group_info in e["groups"]:
            etud_classes.append(group_info["group_id"])

        label = (
            '<span class="%s">' % classdem
            + e["civilite_str"]
            + " "
            + scolars.format_nomprenom(e, reverse=True)
            + "</span>"
        )

        # Historique des saisies de notes:
        if not disabled:
            explanation = (
                '<span id="hist_%s">' % etudid
                + get_note_history_menu(context, evaluation_id, etudid)
                + "</span>"
            )
        else:
            explanation = ""
        explanation = e["absinfo"] + explanation

        # Lien modif decision de jury:
        explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid

        # Valeur actuelle du champ:
        initvalues["note_" + etudid] = e["val"]
        label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)

        # Element de formulaire:
        descr.append(
            (
                "note_" + etudid,
                {
                    "size": 5,
                    "title": label_link,
                    "explanation": explanation,
                    "return_focus_next": True,
                    "attributes": [
                        'class="note%s"' % classdem,
                        disabled_attr,
                        "data-last-saved-value=%s" % e["val"],
                        "data-orig-value=%s" % e["val"],
                        "data-etudid=%s" % etudid,
                    ],
                    "template": """<tr%(item_dom_attr)s class="etud_elem """
                    + " ".join(etud_classes)
                    + """"><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>""",
                },
            )
        )
    #
    H = []
    if nb_decisions > 0:
        H.append(
            """<div class="saisie_warn">
        <ul class="tf-msg">
        <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !</li>
        </ul>
        </div>"""
            % nb_decisions
        )
    # H.append('''<div id="sco_msg" class="head_message"></div>''')

    tf = TF(
        destination,
        REQUEST.form,
        descr,
        initvalues=initvalues,
        submitlabel="Terminer",
        formid="formnotes",
    )
    H.append(tf.getform())  # check and init
    if tf.canceled():
        return None
    elif (not tf.submitted()) or not tf.result:
        # ajout formularie saisie notes manquantes
        H.append(
            """
        <div>
        <form action="do_evaluation_set_missing" method="get">
        Mettre <em>toutes</em> les notes manquantes à <input type="text" size="5" name="value"/>
        <input type="submit" value="OK"/> 
        <input type="hidden" name="evaluation_id" value="%s"/> 
        <em>affecte tous les groupes. ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
        </form>
        </div>
        """
            % evaluation_id
        )
        # affiche formulaire
        return "\n".join(H)
    else:
        # form submission
        # rien à faire
        return None


def save_note(
    context, etudid=None, evaluation_id=None, value=None, comment="", REQUEST=None
):
    """Enregistre une note (ajax)"""
    authuser = REQUEST.AUTHENTICATED_USER
    log(
        "save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
        % (evaluation_id, etudid, authuser, value)
    )
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    Mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % M
    result = {"nbchanged": 0}  # JSON
    # Check access: admin, respformation, or responsable_id
    if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
        result["status"] = "unauthorized"
    else:
        L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
        if L:
            nbchanged, _, existing_decisions = _notes_add(
                context, authuser, evaluation_id, L, comment=comment, do_it=True
            )
            sco_news.add(
                context,
                REQUEST,
                typ=NEWS_NOTE,
                object=M["moduleimpl_id"],
                text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
                url=Mod["url"],
                max_frequency=30 * 60,  # 30 minutes
            )
            result["nbchanged"] = nbchanged
            result["existing_decisions"] = existing_decisions
            if nbchanged > 0:
                result["history_menu"] = get_note_history_menu(
                    context, evaluation_id, etudid
                )
            else:
                result["history_menu"] = ""  # no update needed
        result["status"] = "ok"
    return scu.sendJSON(REQUEST, result)


def get_note_history_menu(context, evaluation_id, etudid):
    """Menu HTML historique de la note"""
    history = sco_undo_notes.get_note_history(context, evaluation_id, etudid)
    if not history:
        return ""

    H = []
    if len(history) > 1:
        H.append(
            '<select data-etudid="%s" class="note_history" onchange="change_history(this);">'
            % etudid
        )
        envir = "select"
        item = "option"
    else:
        # pas de menu
        H.append('<span class="history">')
        envir = "span"
        item = "span"

    first = True
    for i in history:
        jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
        dispnote = _displayNote(i["value"])
        if first:
            nv = ""  # ne repete pas la valeur de la note courante
        else:
            # ancienne valeur
            nv = '<span class="histvalue">: %s</span>' % dispnote
        first = False
        if i["comment"]:
            comment = ' <span class="histcomment">%s</span>' % i["comment"]
        else:
            comment = ""
        H.append(
            '<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)
        )

    H.append("</%s>" % envir)
    return "\n".join(H)
