# -*- 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"] = ( context.NotesURL() + "/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 %(titre)s' % 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 """ 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 # mais ne met pas en attente les evals immediates sans aucune notes: if E["publish_incomplete"] != "0" and nb_notes == 0: 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")) # 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"] ), '
soit %s évaluations planifiées;" % nb_evals, """
Module : %s
' % (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("UE : %(acronyme)s
" % ue) # store min/max values used by JS client-side checks: H.append( '-20.20.' ) else: # date et absences (pas pour evals de malus) jour = E["jour"] or "pas de date" H.append( "Réalisée le %s 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( '
' % (context.ScoURL(), group_id, urllib.quote(E["jour"], safe="")) ) H.append( 'Coefficient dans le module: %s, notes sur %g ' % (E["coefficient"], E["note_max"]) ) H.append('0.') if can_edit: H.append( '(modifier l\'évaluation)' % evaluation_id ) H.append("
") return '" + str(e) + "
" + '' % (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 = ( 'voir toutes les notes du module' % 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 = """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.
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.
L'option Visible sur bulletins 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).
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.
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).
""" mod_descr = '%s %s %s' % ( moduleimpl_id, Mod["code"], Mod["titre"], link, ) if not readonly: H = ["