# -*- 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@gmail.com
#
##############################################################################

"""Evaluations
"""
import time
import urllib
import operator
import datetime

from notes_log import log, logCallStack
import sco_utils as scu
from notesdb import ScoDocCursor
from sco_exceptions import AccessDenied, ScoValueError
import VERSION
from gen_tables import GenTable
from TrivialFormulator import TrivialFormulator
import sco_news
import sco_formsemestre
import sco_moduleimpl
import sco_groups
import sco_abs
import sco_evaluations
import sco_saisie_notes

# --------------------------------------------------------------------
#
#    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_delete(context, REQUEST, evaluation_id):
    "delete evaluation"
    the_evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
    if not the_evals:
        raise ValueError("evaluation inexistante !")

    NotesDB = context._notes_getall(evaluation_id)  # { etudid : value }
    notes = [x["value"] for x in NotesDB.values()]
    if notes:
        raise ScoValueError(
            "Impossible de supprimer cette évaluation: il reste des notes"
        )

    moduleimpl_id = the_evals[0]["moduleimpl_id"]
    context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
    cnx = context.GetDBConnexion()

    context._evaluationEditor.delete(cnx, evaluation_id)
    # inval cache pour ce semestre
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0]
    context._inval_cache(formsemestre_id=M["formsemestre_id"])  # > eval delete
    # news
    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=sco_news.NEWS_NOTE,
        object=moduleimpl_id,
        text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
        url=mod["url"],
    )


_DEE_TOT = 0


def do_evaluation_etat(
    context, evaluation_id, partition_id=None, select_first_partition=False
):
    """donne infos sur l'etat du evaluation
    { nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att,
    moyenne, mediane, mini, maxi,
    date_last_modif, 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
    """
    # global _DEE_TOT
    # t0=time.time()
    # if evaluation_id == 'GEAEVAL82883':
    #     log('do_evaluation_etat: evaluation_id=%s  partition_id=%s sfp=%s' % (evaluation_id, partition_id, select_first_partition))

    nb_inscrits = len(
        sco_groups.do_evaluation_listeetuds_groups(
            context, evaluation_id, getallstudents=True
        )
    )
    NotesDB = context._notes_getall(evaluation_id)  # { etudid : value }
    notes = [x["value"] for x in NotesDB.values()]
    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 = "", ""
        median_num, moy_num = None, None
        mini, maxi = "", ""
        mini_num, maxi_num = None, None
    else:
        median = scu.fmt_note(median_num)
        moy = scu.fmt_note(moy_num)
        mini = scu.fmt_note(mini_num)
        maxi = scu.fmt_note(maxi_num)
    # cherche date derniere modif note
    if len(NotesDB):
        t = [x["date"] for x in NotesDB.values()]
        last_modif = max(t)
    else:
        last_modif = None
    # ---- Liste des groupes complets et incomplets
    E = context.do_evaluation_list(args={"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]
    is_malus = Mod["module_type"] == scu.MODULE_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(context, formsemestre_id)
            partition = partitions[0]
        else:
            partition = sco_groups.get_default_partition(context, 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 = context.do_formsemestre_inscription_listinscrits(formsemestre_id)
    insmod = sco_moduleimpl.do_moduleimpl_inscription_list(
        context, 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]

    # Nombre de notes valides d'étudiants inscrits au module
    # (car il peut y avoir des notes d'étudiants désinscrits depuis l'évaluation)
    nb_notes = len(insmodset.intersection(NotesDB))
    nb_notes_total = len(NotesDB)

    # On considere une note "manquante" lorsqu'elle n'existe pas
    # ou qu'elle est en attente (ATT)
    GrNbMissing = scu.DictDefault()  # group_id : nb notes manquantes
    GrNotes = scu.DictDefault(defaultvalue=[])  # group_id: liste notes valides
    TotalNbMissing = 0
    TotalNbAtt = 0
    groups = {}  # group_id : group
    etud_groups = sco_groups.get_etud_groups_in_partition(context, 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 NotesDB.has_key(i["etudid"]):
            val = NotesDB[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 not is_malus
    ):
        complete = False
    else:
        complete = True
    if (
        TotalNbMissing > 0
        and (TotalNbMissing == TotalNbAtt or E["publish_incomplete"] != "0")
        and not is_malus
    ):
        evalattente = True
    else:
        evalattente = False

    # Calcul moyenne dans chaque groupe de TD
    gr_moyennes = []  # group : {moy,median, nb_notes}
    for group_id in GrNotes.keys():
        notes = GrNotes[group_id]
        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_num": gr_moy,
                "gr_moy": scu.fmt_note(gr_moy),
                "gr_median_num": gr_median,
                "gr_median": scu.fmt_note(gr_median),
                "gr_mini": scu.fmt_note(gr_mini),
                "gr_maxi": scu.fmt_note(gr_maxi),
                "gr_mini_num": gr_mini,
                "gr_maxi_num": gr_maxi,
                "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"))
    # log('gr_moyennes=%s' % gr_moyennes)
    # _DEE_TOT += (time.time() - t0)
    # log('%s\t_DEE_TOT=%f' % (evaluation_id, _DEE_TOT))
    # if evaluation_id == 'GEAEVAL82883':
    #    logCallStack()

    # retourne mapping
    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,
        "moy_num": moy_num,
        "median": median,
        "mini": mini,
        "mini_num": mini_num,
        "maxi": maxi,
        "maxi_num": maxi_num,
        "median_num": median_num,
        "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(context, formsemestre_id):
    """Liste les evaluations de tous les modules de ce semestre.
       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',
                              'gr_median_num' : 12.,
                              'gr_moy': '11.88',
                              'gr_moy_num' : 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/1
    'moduleimpl_id': 'GEAMIP80490',
    'note_max': 20.0,
    'numero': 0,
    'publish_incomplete': 0,
    'visibulletin': 1} ]

    """
    req = "select E.* from notes_evaluation E, notes_moduleimpl MI where MI.formsemestre_id = %(formsemestre_id)s and MI.moduleimpl_id = E.moduleimpl_id order by moduleimpl_id, numero desc, jour desc, heure_debut desc"
    cnx = context.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
    cursor.execute(req, {"formsemestre_id": formsemestre_id})
    res = cursor.dictfetchall()
    # etat de chaque evaluation:
    for r in res:
        r["jour"] = r["jour"] or datetime.date(1900, 1, 1)  # pour les comparaisons
        r["etat"] = do_evaluation_etat(context, r["evaluation_id"])

    return res


# remplacé par nt.get_sem_evaluation_etat_list()
#
# def formsemestre_evaluations_list(context, formsemestre_id):
#     """Liste (non triée) des evals pour ce semestre"""
#     req = "select E.* from notes_evaluation E, notes_moduleimpl MI where MI.formsemestre_id = %(formsemestre_id)s and MI.moduleimpl_id = E.moduleimpl_id"
#     cnx = context.GetDBConnexion()
#     cursor = cnx.cursor(cursor_factory=ScoDocCursor)
#     cursor.execute( req, { 'formsemestre_id' : formsemestre_id } )
#     return cursor.dictfetchall()


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
        dates.append(e["etat"]["last_modif"])

    dates = scu.sort_dates(dates)

    if len(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(context, formsemestre_id, REQUEST=None):
    """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
    date derniere modif, attente"""
    nt = context._getNotesCache().get_NotesTable(
        context, formsemestre_id
    )  # > liste evaluations et moduleimpl en attente
    evals = nt.get_sem_evaluation_etat_list()
    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(context, nt, moduleimpl_id):
    """"""
    evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
    etat = _eval_etat(evals)
    etat["attente"] = moduleimpl_id in [
        m["moduleimpl_id"] for m in nt.get_moduleimpls_attente()
    ]  # > liste moduleimpl en attente
    return etat


def formsemestre_evaluations_cal(context, formsemestre_id, REQUEST=None):
    """Page avec calendrier de toutes les evaluations de ce semestre"""
    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
    nt = context._getNotesCache().get_NotesTable(
        context, formsemestre_id
    )  # > liste evaluations

    evals = nt.get_sem_evaluation_etat_list()
    nb_evals = len(evals)

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

    today = time.strftime("%Y-%m-%d")

    year = int(sem["annee_debut"])
    if sem["mois_debut_ord"] < 8:
        year -= 1  # calendrier septembre a septembre
    events = {}  # (day, halfday) : event
    for e in evals:
        etat = e["etat"]
        if not e["jour"]:
            continue
        day = e["jour"].strftime("%Y-%m-%d")
        mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
            context, moduleimpl_id=e["moduleimpl_id"]
        )[0]
        txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
        if e["heure_debut"]:
            debut = e["heure_debut"].strftime("%Hh%M")
        else:
            debut = "?"
        if e["heure_fin"]:
            fin = e["heure_fin"].strftime("%Hh%M")
        else:
            fin = "?"
        description = "%s, de %s à %s" % (mod["module"]["titre"], debut, fin)
        if etat["evalcomplete"]:
            color = color_complete
        else:
            color = color_incomplete
        if day > today:
            color = color_futur
        href = "moduleimpl_status?moduleimpl_id=%s" % e["moduleimpl_id"]
        # if e['heure_debut'].hour < 12:
        #    halfday = True
        # else:
        #    halfday = False
        if not day in events:
            # events[(day,halfday)] = [day, txt, color, href, halfday, description, mod]
            events[day] = [day, txt, color, href, description, mod]
        else:
            e = events[day]
            if e[-1]["moduleimpl_id"] != mod["moduleimpl_id"]:
                # plusieurs evals de modules differents a la meme date
                e[1] += ", " + txt
                e[4] += ", " + description
                if not etat["evalcomplete"]:
                    e[2] = color_incomplete
                if day > today:
                    e[2] = color_futur

    CalHTML = sco_abs.YearTable(
        context.Absences, year, events=events.values(), halfday=False, pad_width=None
    )

    H = [
        context.html_sem_header(
            REQUEST, "Evaluations du semestre", sem, cssstyles=["css/calabs.css"]
        ),
        '<div class="cal_evaluations">',
        CalHTML,
        "</div>",
        "<p>soit %s évaluations planifiées;" % nb_evals,
        """<ul><li>en <span style="background-color: %s">rouge</span> les évaluations passées auxquelles il manque des notes</li>
          <li>en <span style="background-color: %s">vert</span> les évaluations déjà notées</li>
          <li>en <span style="background-color: %s">bleu</span> les évaluations futures</li></ul></p>"""
        % (color_incomplete, color_complete, color_futur),
        """<p><a href="formsemestre_evaluations_delai_correction?formsemestre_id=%s" class="stdlink">voir les délais de correction</a></p>
          """
        % (formsemestre_id,),
        context.sco_footer(REQUEST),
    ]
    return "\n".join(H)


def evaluation_date_first_completion(context, evaluation_id):
    """Première date à laquelle l'évaluation a été complète
    ou None si actuellement incomplète
    """
    etat = do_evaluation_etat(context, 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 = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
    # M = sco_moduleimpl.do_moduleimpl_list(context,moduleimpl_id=E["moduleimpl_id"])[0]
    # formsemestre_id = M["formsemestre_id"]
    # insem = context.do_formsemestre_inscription_listinscrits(formsemestre_id)
    # insmod = sco_moduleimpl.do_moduleimpl_inscription_list(context,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 = context._notes_getall(evaluation_id, filter_suppressed=False).values()
    notes_log = context._notes_getall(
        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(
    context, formsemestre_id, format="html", REQUEST=None
):
    """Experimental: un tableau indiquant pour chaque évaluation
    le nombre de jours avant la publication des notes.

    N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus.
    """
    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
    nt = context._getNotesCache().get_NotesTable(
        context, formsemestre_id
    )  # > liste evaluations

    evals = nt.get_sem_evaluation_etat_list()
    T = []
    for e in evals:
        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]
        if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
            Mod["module_type"] == scu.MODULE_MALUS
        ):
            continue
        e["date_first_complete"] = evaluation_date_first_completion(
            context, e["evaluation_id"]
        )
        if e["date_first_complete"]:
            e["delai_correction"] = (e["date_first_complete"].date() - e["jour"]).days
        else:
            e["delai_correction"] = None

        e["module_code"] = Mod["code"]
        e["_module_code_target"] = (
            "moduleimpl_status?moduleimpl_id=" + M["moduleimpl_id"]
        )
        e["module_titre"] = Mod["titre"]
        e["responsable_id"] = M["responsable_id"]
        e["responsable_nomplogin"] = context.Users.user_info(M["responsable_id"])[
            "nomplogin"
        ]
        e["_jour_target"] = "evaluation_listenotes?evaluation_id=" + e["evaluation_id"]
        T.append(e)

    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=T,
        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=context.get_preferences(formsemestre_id),
        base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
        origin="Généré par %s le " % VERSION.SCONAME + scu.timedate_human_repr() + "",
        filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]),
    )
    return tab.make_page(context, format=format, REQUEST=REQUEST)


def module_evaluation_insert_before(context, ModEvals, next_eval, REQUEST):
    """Renumber evals such that an evaluation with can be inserted before next_eval
    Returns numero suitable for the inserted evaluation
    """
    if next_eval:
        n = next_eval["numero"]
        if not n:
            log("renumbering old evals")
            module_evaluation_renumber(context, next_eval["moduleimpl_id"], REQUEST)
            next_eval = context.do_evaluation_list(
                args={"evaluation_id": next_eval["evaluation_id"]}
            )[0]
            n = next_eval["numero"]
    else:
        n = 1
    # log('inserting at position numero %s' % n )
    # all numeros >= n are incremented
    for e in ModEvals:
        if e["numero"] >= n:
            e["numero"] += 1
            # log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
            context.do_evaluation_edit(REQUEST, e)

    return n


def module_evaluation_move(context, evaluation_id, after=0, REQUEST=None, redirect=1):
    """Move before/after previous one (decrement/increment numero)
    (published)
    """
    e = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
    redirect = int(redirect)

    # access: can change eval ? (raises exception)
    context._evaluation_check_write_access(REQUEST, moduleimpl_id=e["moduleimpl_id"])

    module_evaluation_renumber(
        context, e["moduleimpl_id"], REQUEST=REQUEST, only_if_unumbered=True
    )
    e = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]

    after = int(after)  # 0: deplace avant, 1 deplace apres
    if after not in (0, 1):
        raise ValueError('invalid value for "after"')
    ModEvals = context.do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
    # log('ModEvals=%s' % [ x['evaluation_id'] for x in ModEvals] )
    if len(ModEvals) > 1:
        idx = [p["evaluation_id"] for p in ModEvals].index(evaluation_id)
        neigh = None  # object to swap with
        if after == 0 and idx > 0:
            neigh = ModEvals[idx - 1]
        elif after == 1 and idx < len(ModEvals) - 1:
            neigh = ModEvals[idx + 1]
        if neigh:  #
            # swap numero with neighbor
            e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
            context.do_evaluation_edit(REQUEST, e)
            context.do_evaluation_edit(REQUEST, neigh)
    # redirect to moduleimpl page:
    if redirect:
        return REQUEST.RESPONSE.redirect(
            "moduleimpl_status?moduleimpl_id=" + e["moduleimpl_id"]
        )


def module_evaluation_renumber(
    context, moduleimpl_id, REQUEST=None, only_if_unumbered=False, redirect=0
):
    """Renumber evaluations in this module, according to their date. (numero=0: oldest one)
    Needed because previous versions of ScoDoc did not have eval numeros
    Note: existing numeros are ignored
    """
    redirect = int(redirect)
    # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
    # List sorted according to date/heure, ignoring numeros:
    # (note that we place  evaluations with NULL date at the end)
    ModEvals = context.do_evaluation_list(
        args={"moduleimpl_id": moduleimpl_id}, sortkey="jour asc, heure_debut asc"
    )

    all_numbered = False not in [x["numero"] > 0 for x in ModEvals]
    if all_numbered and only_if_unumbered:
        return  # all ok

    # log('module_evaluation_renumber')
    # Reset all numeros:
    i = 1
    for e in ModEvals:
        e["numero"] = i
        context.do_evaluation_edit(REQUEST, e)
        i += 1

    # If requested, redirect to moduleimpl page:
    if redirect:
        return REQUEST.RESPONSE.redirect(
            "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
        )


#  -------------- VIEWS
def evaluation_describe(context, evaluation_id="", edit_in_place=True, REQUEST=None):
    """HTML description of evaluation, for page headers
    edit_in_place: allow in-place editing when permitted (not implemented)
    """
    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
    moduleimpl_id = E["moduleimpl_id"]
    M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0]
    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    formsemestre_id = M["formsemestre_id"]
    u = context.Users.user_info(M["responsable_id"])
    resp = u["prenomnom"]
    nomcomplet = u["nomcomplet"]
    can_edit = sco_saisie_notes.can_edit_notes(
        context, REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False
    )

    link = (
        '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
        % moduleimpl_id
    )
    mod_descr = (
        '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
        % (moduleimpl_id, Mod["code"], Mod["titre"], nomcomplet, resp, link)
    )

    etit = E["description"] or ""
    if etit:
        etit = ' "' + etit + '"'
    if Mod["module_type"] == scu.MODULE_MALUS:
        etit += ' <span class="eval_malus">(points de malus)</span>'
    H = [
        '<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>'
        % (etit, mod_descr)
    ]
    if Mod["module_type"] == scu.MODULE_MALUS:
        # Indique l'UE
        ue = context.do_ue_list(args={"ue_id": Mod["ue_id"]})[0]
        H.append("<p><b>UE : %(acronyme)s</b></p>" % ue)
        # 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)
        jour = E["jour"] or "<em>pas de date</em>"
        H.append(
            "<p>Réalisée le <b>%s</b> de %s à %s "
            % (jour, E["heure_debut"], E["heure_fin"])
        )
        if E["jour"]:
            group_id = sco_groups.get_default_group(context, formsemestre_id)
            H.append(
                '<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>'
                % (context.ScoURL(), group_id, urllib.quote(E["jour"], safe=""))
            )
        H.append(
            '</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> '
            % (E["coefficient"], E["note_max"])
        )
        H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
    if can_edit:
        H.append(
            '<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>'
            % evaluation_id
        )
    H.append("</p>")

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


def evaluation_create_form(
    context,
    moduleimpl_id=None,
    evaluation_id=None,
    REQUEST=None,
    edit=False,
    readonly=False,
    page_title="Evaluation",
):
    "formulaire creation/edition des evaluations (pas des notes)"
    if evaluation_id != None:
        the_eval = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
        moduleimpl_id = the_eval["moduleimpl_id"]
    #
    M = sco_moduleimpl.do_moduleimpl_withmodule_list(
        context, moduleimpl_id=moduleimpl_id
    )[0]
    is_malus = M["module"]["module_type"] == scu.MODULE_MALUS  # True si module de malus
    formsemestre_id = M["formsemestre_id"]
    min_note_max = scu.NOTES_PRECISION  # le plus petit bareme possible
    if not readonly:
        try:
            context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
        except AccessDenied as e:
            return (
                context.sco_header(REQUEST)
                + "<h2>Opération non autorisée</h2><p>"
                + str(e)
                + "</p>"
                + '<p><a href="%s">Revenir</a></p>' % (str(REQUEST.HTTP_REFERER),)
                + context.sco_footer(REQUEST)
            )
    if readonly:
        edit = True  # montre les donnees existantes
    if not edit:
        # creation nouvel
        if moduleimpl_id is None:
            raise ValueError("missing moduleimpl_id parameter")
        initvalues = {
            "note_max": 20,
            "jour": time.strftime("%d/%m/%Y", time.localtime()),
            "publish_incomplete": is_malus,
        }
        submitlabel = "Créer cette évaluation"
        action = "Création d'une é"
        link = ""
    else:
        # edition donnees existantes
        # setup form init values
        if evaluation_id is None:
            raise ValueError("missing evaluation_id parameter")
        initvalues = the_eval
        moduleimpl_id = initvalues["moduleimpl_id"]
        submitlabel = "Modifier les données"
        if readonly:
            action = "E"
            link = (
                '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
                % M["moduleimpl_id"]
            )
        else:
            action = "Modification d'une é"
            link = ""
        # Note maximale actuelle dans cette eval ?
        etat = do_evaluation_etat(context, evaluation_id)
        if etat["maxi_num"] is not None:
            min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
        else:
            min_note_max = scu.NOTES_PRECISION
    #
    if min_note_max > scu.NOTES_PRECISION:
        min_note_max_str = scu.fmt_note(min_note_max)
    else:
        min_note_max_str = "0"
    #
    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
    #
    help = """<div class="help"><p class="help">
    Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
    Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes
    (examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note
    moyenne de chaque étudiant dans ce module.
    </p><p class="help">
    Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme
    pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir
    les moyennes d'UE et la moyenne générale.
    </p><p class="help">
    L'option <em>Visible sur bulletins</em> indique que la note sera reportée sur les bulletins
    en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines
    notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
    les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
    </p><p class="help">
    La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules
    si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont
    pas besoin d'être rentrées.
    </p>
    <p class="help">
    Les évaluations des modules de type "malus" sont spéciales: le coefficient n'est pas utilisé. 
    Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne
    de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
    </p>
    """
    mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
        moduleimpl_id,
        Mod["code"],
        Mod["titre"],
        link,
    )
    if not readonly:
        H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)]
    else:
        return sco_evaluations.evaluation_describe(
            context, evaluation_id, REQUEST=REQUEST
        )

    heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
    #
    initvalues["visibulletin"] = initvalues.get("visibulletin", "1")
    if initvalues["visibulletin"] == "1":
        initvalues["visibulletinlist"] = ["X"]
    else:
        initvalues["visibulletinlist"] = []
    if REQUEST.form.get("tf-submitted", False) and not REQUEST.form.has_key(
        "visibulletinlist"
    ):
        REQUEST.form["visibulletinlist"] = []
    #
    form = [
        ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
        ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
        ("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
        # ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
        (
            "jour",
            {
                "input_type": "date",
                "title": "Date",
                "size": 12,
                "explanation": "date de l'examen, devoir ou contrôle",
            },
        ),
        (
            "heure_debut",
            {
                "title": "Heure de début",
                "explanation": "heure du début de l'épreuve",
                "input_type": "menu",
                "allowed_values": heures,
                "labels": heures,
            },
        ),
        (
            "heure_fin",
            {
                "title": "Heure de fin",
                "explanation": "heure de fin de l'épreuve",
                "input_type": "menu",
                "allowed_values": heures,
                "labels": heures,
            },
        ),
    ]
    if is_malus:  # pas de coefficient
        form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
    else:
        form.append(
            (
                "coefficient",
                {
                    "size": 10,
                    "type": "float",
                    "explanation": "coef. dans le module (choisi librement par l'enseignant)",
                    "allow_null": False,
                },
            )
        )
    form += [
        (
            "note_max",
            {
                "size": 4,
                "type": "float",
                "title": "Notes de 0 à",
                "explanation": "barème (note max actuelle: %s)" % min_note_max_str,
                "allow_null": False,
                "max_value": scu.NOTES_MAX,
                "min_value": min_note_max,
            },
        ),
        (
            "description",
            {
                "size": 36,
                "type": "text",
                "explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
            },
        ),
        (
            "visibulletinlist",
            {
                "input_type": "checkbox",
                "allowed_values": ["X"],
                "labels": [""],
                "title": "Visible sur bulletins",
                "explanation": "(pour les bulletins en version intermédiaire)",
            },
        ),
        (
            "publish_incomplete",
            {
                "input_type": "boolcheckbox",
                "title": "Prise en compte immédiate",
                "explanation": "notes utilisées même si incomplètes",
            },
        ),
        (
            "evaluation_type",
            {
                "input_type": "menu",
                "title": "Modalité",
                "allowed_values": (scu.EVALUATION_NORMALE, scu.EVALUATION_RATTRAPAGE),
                "type": "int",
                "labels": ("Normale", "Rattrapage"),
            },
        ),
    ]
    tf = TrivialFormulator(
        REQUEST.URL0,
        REQUEST.form,
        form,
        cancelbutton="Annuler",
        submitlabel=submitlabel,
        initvalues=initvalues,
        readonly=readonly,
    )

    dest_url = "moduleimpl_status?moduleimpl_id=%s" % M["moduleimpl_id"]
    if tf[0] == 0:
        head = context.sco_header(REQUEST, page_title=page_title)
        return head + "\n".join(H) + "\n" + tf[1] + help + context.sco_footer(REQUEST)
    elif tf[0] == -1:
        return REQUEST.RESPONSE.redirect(dest_url)
    else:
        # form submission
        if tf[2]["visibulletinlist"]:
            tf[2]["visibulletin"] = 1
        else:
            tf[2]["visibulletin"] = 0
        if not edit:
            # creation d'une evaluation
            evaluation_id = context.do_evaluation_create(REQUEST=REQUEST, **tf[2])
            return REQUEST.RESPONSE.redirect(dest_url)
        else:
            context.do_evaluation_edit(REQUEST, tf[2])
            return REQUEST.RESPONSE.redirect(dest_url)