# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@gmail.com # ############################################################################## """Evaluations """ import collections import datetime import operator import time from flask import url_for from flask import g from flask_login import current_user from flask import request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cal from app.scodoc import sco_evaluation_db from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences from app.scodoc import sco_users import sco_version # -------------------------------------------------------------------- # # MISC AUXILIARY FUNCTIONS # # -------------------------------------------------------------------- def notes_moyenne_median_mini_maxi(notes): "calcule moyenne et mediane d'une liste de valeurs (floats)" notes = [ x for x in notes if (x != None) and (x != scu.NOTES_NEUTRALISE) and (x != scu.NOTES_ATTENTE) ] n = len(notes) if not n: return None, None, None, None moy = sum(notes) / n median = ListMedian(notes) mini = min(notes) maxi = max(notes) return moy, median, mini, maxi def ListMedian(L): """Median of a list L""" n = len(L) if not n: raise ValueError("empty list") L.sort() if n % 2: return L[n // 2] else: return (L[n // 2] + L[n // 2 - 1]) / 2 # -------------------------------------------------------------------- def do_evaluation_etat( evaluation_id: int, partition_id: int = None, select_first_partition=False ) -> dict: """Donne infos sur l'état de l'évaluation. Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul. XXX utilisée par de très nombreuses fonctions, dont - _eval_etat 0) and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE) and (E["evaluation_type"] != scu.EVALUATION_SESSION2) ): complete = False else: complete = True complete = ( (TotalNbMissing == 0) or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE) or (E["evaluation_type"] == scu.EVALUATION_SESSION2) ) evalattente = (TotalNbMissing > 0) and ( (TotalNbMissing == TotalNbAtt) or E["publish_incomplete"] ) # mais ne met pas en attente les evals immediates sans aucune notes: if E["publish_incomplete"] and nb_notes == 0: evalattente = False # Calcul moyenne dans chaque groupe de TD gr_moyennes = [] # group : {moy,median, nb_notes} for group_id, notes in GrNotes.items(): gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes) gr_moyennes.append( { "group_id": group_id, "group_name": groups[group_id]["group_name"], "gr_moy": scu.fmt_note(gr_moy, E["note_max"]), "gr_median": scu.fmt_note(gr_median, E["note_max"]), "gr_mini": scu.fmt_note(gr_mini, E["note_max"]), "gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]), "gr_nb_notes": len(notes), "gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]), } ) gr_moyennes.sort(key=operator.itemgetter("group_name")) return { "evaluation_id": evaluation_id, "nb_inscrits": nb_inscrits, "nb_notes": nb_notes, # nb notes etudiants inscrits "nb_notes_total": nb_notes_total, # nb de notes (incluant desinscrits) "nb_abs": nb_abs, "nb_neutre": nb_neutre, "nb_att": nb_att, "moy": moy, # chaine formattée, sur 20 "median": median, "mini": mini, "maxi": maxi, "maxi_num": maxi_num, # note maximale, en nombre "last_modif": last_modif, "gr_incomplets": gr_incomplets, "gr_moyennes": gr_moyennes, "groups": groups, "evalcomplete": complete, "evalattente": evalattente, "is_malus": is_malus, } def do_evaluation_list_in_sem(formsemestre_id, with_etat=True): """Liste les évaluations de tous les modules de ce semestre. Triée par module, numero desc, date_debut desc Donne pour chaque eval son état (voir do_evaluation_etat) { evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... } Exemple: [ { 'coefficient': 1.0, 'description': 'QCM et cas pratiques', 'etat': { 'evalattente': False, 'evalcomplete': True, 'evaluation_id': 'GEAEVAL82883', 'gr_incomplets': [], 'gr_moyennes': [{ 'gr_median': '12.00', # sur 20 'gr_moy': '11.88', 'gr_nb_att': 0, 'gr_nb_notes': 166, 'group_id': 'GEAG266762', 'group_name': None }], 'groups': {'GEAG266762': {'etudid': 'GEAEID80603', 'group_id': 'GEAG266762', 'group_name': None, 'partition_id': 'GEAP266761'} }, 'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16), 'median': '12.00', 'moy': '11.84', 'nb_abs': 2, 'nb_att': 0, 'nb_inscrits': 166, 'nb_neutre': 0, 'nb_notes': 168, 'nb_notes_total': 169 }, 'evaluation_id': 'GEAEVAL82883', 'evaluation_type': 0, 'heure_debut': datetime.time(8, 0), 'heure_fin': datetime.time(9, 30), 'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900 'moduleimpl_id': 'GEAMIP80490', 'note_max': 20.0, 'numero': 0, 'publish_incomplete': 0, 'visibulletin': 1} ] """ req = """SELECT E.id AS evaluation_id, E.* FROM notes_evaluation E, notes_moduleimpl MI WHERE MI.formsemestre_id = %(formsemestre_id)s and MI.id = E.moduleimpl_id ORDER BY MI.id, numero desc, date_debut desc """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute(req, {"formsemestre_id": formsemestre_id}) res = cursor.dictfetchall() # etat de chaque evaluation: for r in res: if with_etat: r["etat"] = do_evaluation_etat(r["evaluation_id"]) r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1) return res def _eval_etat(evals): """evals: list of mappings (etats) -> nb_eval_completes, nb_evals_en_cours, nb_evals_vides, date derniere modif Une eval est "complete" ssi tous les etudiants *inscrits* ont une note. """ nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0 dates = [] for e in evals: if e["etat"]["evalcomplete"]: nb_evals_completes += 1 elif e["etat"]["nb_notes"] == 0: nb_evals_vides += 1 else: nb_evals_en_cours += 1 last_modif = e["etat"]["last_modif"] if last_modif is not None: dates.append(e["etat"]["last_modif"]) if dates: dates = scu.sort_dates(dates) last_modif = dates[-1] # date de derniere modif d'une note dans un module else: last_modif = "" return { "nb_evals_completes": nb_evals_completes, "nb_evals_en_cours": nb_evals_en_cours, "nb_evals_vides": nb_evals_vides, "last_modif": last_modif, } def do_evaluation_etat_in_sem(formsemestre_id): """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides, date derniere modif, attente XXX utilisé par - formsemestre_status_head - gen_formsemestre_recapcomplet_xml - gen_formsemestre_recapcomplet_json "nb_evals_completes" "nb_evals_en_cours" "nb_evals_vides" "date_derniere_note" "last_modif" "attente" """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) evals = nt.get_evaluations_etats() etat = _eval_etat(evals) # Ajoute information sur notes en attente etat["attente"] = len(nt.get_moduleimpls_attente()) > 0 return etat def do_evaluation_etat_in_mod(nt, moduleimpl_id): """""" evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) etat = _eval_etat(evals) # Il y a-t-il des notes en attente dans ce module ? etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente return etat def formsemestre_evaluations_cal(formsemestre_id): """Page avec calendrier de toutes les evaluations de ce semestre""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) evaluations = formsemestre.get_evaluations() # TODO nb_evals = len(evaluations) color_incomplete = "#FF6060" color_complete = "#A0FFA0" color_futur = "#70E0FF" year = formsemestre.annee_scolaire() events = {} # (day, halfday) : event for e in evaluations: if e.date_debut is None: continue # éval. sans date txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval." if e.date_debut == e.date_fin: heure_debut_txt, heure_fin_txt = "?", "?" else: heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?" heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?" description = f"""{ e.moduleimpl.module.titre }, de {heure_debut_txt} à {heure_fin_txt}""" # Etat (notes completes) de l'évaluation: modimpl_result = nt.modimpls_results[e.moduleimpl.id] if modimpl_result.evaluations_etat[e.id].is_complete: color = color_complete else: color = color_incomplete if e.date_debut > datetime.datetime.now(scu.TIME_ZONE): color = color_futur href = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=e.moduleimpl_id, ) day = e.date_debut.date().isoformat() # yyyy-mm-dd event = events.get(day) if not event: events[day] = [day, txt, color, href, description, e.moduleimpl] else: if event[-1].id != e.moduleimpl.id: # plusieurs evals de modules differents a la meme date event[1] += ", " + txt event[4] += ", " + description if color == color_incomplete: event[2] = color_incomplete if color == color_futur: event[2] = color_futur cal_html = sco_cal.YearTable( year, events=list(events.values()), halfday=False, pad_width=None ) return f""" { html_sco_header.html_sem_header( "Evaluations du semestre", cssstyles=["css/calabs.css"], ) }
{ cal_html }

soit {nb_evals} évaluations planifiées;

voir les délais de correction

{ html_sco_header.sco_footer() } """ def evaluation_date_first_completion(evaluation_id) -> datetime.datetime: """Première date à laquelle l'évaluation a été complète ou None si actuellement incomplète """ etat = do_evaluation_etat(evaluation_id) if not etat["evalcomplete"]: return None # XXX inachevé ou à revoir ? # Il faut considerer les inscriptions au semestre # (pour avoir l'etat et le groupe) et aussi les inscriptions # au module (pour gerer les modules optionnels correctement) # E = get_evaluation_dict({"id":evaluation_id})[0] # M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] # formsemestre_id = M["formsemestre_id"] # insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id) # insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"]) # insmodset = set([x["etudid"] for x in insmod]) # retire de insem ceux qui ne sont pas inscrits au module # ins = [i for i in insem if i["etudid"] in insmodset] notes = list( sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, filter_suppressed=False ).values() ) notes_log = list( sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, filter_suppressed=False, table="notes_notes_log" ).values() ) date_premiere_note = {} # etudid : date for note in notes + notes_log: etudid = note["etudid"] if etudid in date_premiere_note: date_premiere_note[etudid] = min(note["date"], date_premiere_note[etudid]) else: date_premiere_note[etudid] = note["date"] if not date_premiere_note: return None # complete mais aucun etudiant non démissionnaires # complet au moment du max (date la plus tardive) des premieres dates de saisie return max(date_premiere_note.values()) def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): """Experimental: un tableau indiquant pour chaque évaluation le nombre de jours avant la publication des notes. N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) evaluations = formsemestre.get_evaluations() rows = [] for e in evaluations: if (e.evaluation_type != scu.EVALUATION_NORMALE) or ( e.moduleimpl.module.module_type == ModuleType.MALUS ): continue date_first_complete = evaluation_date_first_completion(e.id) if date_first_complete and e.date_fin: delai_correction = (date_first_complete.date() - e.date_fin).days else: delai_correction = None rows.append( { "date_first_complete": date_first_complete, "delai_correction": delai_correction, "jour": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date", "_jour_target": url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e["evaluation_id"], ), "module_code": e.moduleimpl.module.code, "_module_code_target": url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=e.moduleimpl.id, ), "module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre, "responsable_id": e.moduleimpl.responsable_id, "responsable_nomplogin": sco_users.user_info( e.moduleimpl.responsable_id )["nomplogin"], } ) columns_ids = ( "module_code", "module_titre", "responsable_nomplogin", "jour", "date_first_complete", "delai_correction", "description", ) titles = { "module_code": "Code", "module_titre": "Module", "responsable_nomplogin": "Responsable", "jour": "Date", "date_first_complete": "Fin saisie", "delai_correction": "Délai", "description": "Description", } tab = GenTable( titles=titles, columns_ids=columns_ids, rows=rows, html_class="table_leftalign table_coldate", html_sortable=True, html_title="

Correction des évaluations du semestre

", caption="Correction des évaluations du semestre", preferences=sco_preferences.SemPreferences(formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""", filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()), ) return tab.make_page(format=format) # -------------- VIEWS def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True): """HTML description of evaluation, for page headers edit_in_place: allow in-place editing when permitted (not implemented) """ E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0] moduleimpl_id = E["moduleimpl_id"] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] formsemestre_id = M["formsemestre_id"] u = sco_users.user_info(M["responsable_id"]) resp = u["prenomnom"] nomcomplet = u["nomcomplet"] can_edit = sco_permissions_check.can_edit_notes( current_user, moduleimpl_id, allow_ens=False ) link = ( 'voir toutes les notes du module' % moduleimpl_id ) mod_descr = ( '%s %s (resp. %s) %s' % ( moduleimpl_id, Mod["code"] or "", Mod["titre"] or "?", nomcomplet, resp, link, ) ) etit = E["description"] or "" if etit: etit = ' "' + etit + '"' if Mod["module_type"] == ModuleType.MALUS: etit += ' (points de malus)' H = [ 'Évaluation%s

Module : %s

' % (etit, mod_descr) ] if Mod["module_type"] == ModuleType.MALUS: # Indique l'UE ue = sco_edit_ue.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) if E["jour"]: jour = E["jour"] H.append("

Réalisée le %s " % (jour)) if E["heure_debut"] != E["heure_fin"]: H.append("de %s à %s " % (E["heure_debut"], E["heure_fin"])) group_id = sco_groups.get_default_group(formsemestre_id) H.append( f"""(absences ce jour)""" ) else: jour = "pas de date" H.append("

Réalisée le %s " % (jour)) H.append( '

Coefficient dans le module: %s, notes sur %g ' % (E["coefficient"], E["note_max"]) ) H.append('0.') if can_edit: H.append( f""" modifier l'évaluation """ ) if link_saisie: H.append( f""" saisie des notes """ ) H.append("

") return '
' + "\n".join(H) + "
"