# -*- 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 # ############################################################################## """Liste des notes d'une évaluation """ import flask from flask import url_for, g, request from app import models from app.models.evaluations import Evaluation from app.models.moduleimpls import ModuleImpl import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.comp import moy_mod from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc import sco_cache from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences from app.scodoc import sco_etud from app.scodoc import sco_users import sco_version from app.scodoc.gen_tables import GenTable from app.scodoc.htmlutils import histogram_notes def do_evaluation_listenotes( evaluation_id=None, moduleimpl_id=None, format="html" ) -> tuple[str, str]: """ Affichage des notes d'une évaluation (si evaluation_id) ou de toutes les évaluations d'un module (si moduleimpl_id) """ mode = None if moduleimpl_id: mode = "module" evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) elif evaluation_id: mode = "eval" evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) else: raise ValueError("missing argument: evaluation or module") if not evals: return "

Aucune évaluation !

", f"ScoDoc" E = evals[0] # il y a au moins une evaluation modimpl = ModuleImpl.query.get(E["moduleimpl_id"]) # description de l'evaluation if mode == "eval": H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)] page_title = f"Notes {E['description'] or modimpl.module.code}" else: H = [] page_title = f"Notes {modimpl.module.code}" # groupes groups = sco_groups.do_evaluation_listegroupes( E["evaluation_id"], include_default=True ) grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons grnams = [str(g["group_id"]) for g in groups] # noms des checkbox if len(evals) > 1: descr = [ ("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"}) ] else: descr = [ ("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"}) ] if len(grnams) > 1: descr += [ ( "s", { "input_type": "separator", "title": "Choix du ou des groupes d'étudiants:", }, ), ( "group_ids", { "input_type": "checkbox", "title": "", "allowed_values": grnams, "labels": grlabs, "attributes": ('onclick="document.tf.submit();"',), }, ), ] else: if grnams: def_nam = grnams[0] else: def_nam = "" descr += [ ( "group_ids", {"input_type": "hidden", "type": "list", "default": [def_nam]}, ) ] descr += [ ( "anonymous_listing", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ('listing "anonyme"',), "attributes": ('onclick="document.tf.submit();"',), "template": '%(label)s%(elem)s   ', }, ), ( "note_sur_20", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("notes sur 20",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s   ", }, ), ( "hide_groups", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("masquer les groupes",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s   ", }, ), ( "with_emails", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("montrer les e-mails",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s", }, ), ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, cancelbutton=None, submitbutton=None, bottom_buttons=False, method="GET", cssclass="noprint", name="tf", is_submitted=True, # toujours "soumis" (démarre avec liste complète) ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1], page_title elif tf[0] == -1: return ( flask.redirect( "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % (scu.ScoURL(), E["moduleimpl_id"]) ), "", ) else: anonymous_listing = tf[2]["anonymous_listing"] note_sur_20 = tf[2]["note_sur_20"] hide_groups = tf[2]["hide_groups"] with_emails = tf[2]["with_emails"] return ( _make_table_notes( tf[1], evals, format=format, note_sur_20=note_sur_20, anonymous_listing=anonymous_listing, group_ids=tf[2]["group_ids"], hide_groups=hide_groups, with_emails=with_emails, mode=mode, ), page_title, ) def _make_table_notes( html_form, evals, format="", note_sur_20=False, anonymous_listing=False, hide_groups=False, with_emails=False, group_ids=[], mode="module", # "eval" or "module" ): """Table liste notes (une seule évaluation ou toutes celles d'un module)""" # Code à ré-écrire ! if not evals: return "

Aucune évaluation !

" E = evals[0] moduleimpl_id = E["moduleimpl_id"] modimpl_o = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] module = models.Module.query.get(modimpl_o["module_id"]) is_apc = module.formation.get_parcours().APC_SAE if is_apc: modimpl = ModuleImpl.query.get(moduleimpl_id) is_conforme = modimpl.check_apc_conformity() evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id) if not ues: is_apc = False else: evals_poids, ues = None, None is_conforme = True sem = sco_formsemestre.get_formsemestre(modimpl_o["formsemestre_id"]) # (debug) check that all evals are in same module: for e in evals: if e["moduleimpl_id"] != moduleimpl_id: raise ValueError("invalid evaluations list") if format == "xls": keep_numeric = True # pas de conversion des notes en strings else: keep_numeric = False # Si pas de groupe, affiche tout if not group_ids: group_ids = [sco_groups.get_default_group(modimpl_o["formsemestre_id"])] groups = sco_groups.listgroups(group_ids) gr_title = sco_groups.listgroups_abbrev(groups) gr_title_filename = sco_groups.listgroups_filename(groups) if anonymous_listing: columns_ids = ["code"] # cols in table else: if format == "xls" or format == "xml": columns_ids = ["nom", "prenom"] else: columns_ids = ["nomprenom"] if not hide_groups: columns_ids.append("group") titles = { "code": "Code", "group": "Groupe", "nom": "Nom", "prenom": "Prénom", "nomprenom": "Nom", "expl_key": "Rem.", "email": "e-mail", "emailperso": "e-mail perso", "signatures": "Signatures", } rows = [] class KeyManager(dict): # comment : key (pour regrouper les comments a la fin) def __init__(self): self.lastkey = 1 def nextkey(self): r = self.lastkey self.lastkey += 1 # self.lastkey = chr(ord(self.lastkey)+1) return str(r) key_mgr = KeyManager() # code pour listings anonyme, à la place du nom if sco_preferences.get_preference("anonymous_lst_code") == "INE": anonymous_lst_key = "code_ine" elif sco_preferences.get_preference("anonymous_lst_code") == "NIP": anonymous_lst_key = "code_nip" else: anonymous_lst_key = "etudid" etudid_etats = sco_groups.do_evaluation_listeetuds_groups( E["evaluation_id"], groups, include_dems=True ) for etudid, etat in etudid_etats: css_row_class = None # infos identite etudiant etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] if etat == "I": # si inscrit, indique groupe groups = sco_groups.get_etud_groups(etudid, sem) grc = sco_groups.listgroups_abbrev(groups) else: if etat == "D": grc = "DEM" # attention: ce code est re-ecrit plus bas, ne pas le changer (?) css_row_class = "etuddem" else: grc = etat code = etud.get(anonymous_lst_key) if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid code = etudid rows.append( { "code": str(code), # INE, NIP ou etudid "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"', "etudid": etudid, "nom": etud["nom"].upper(), "_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % (modimpl_o["formsemestre_id"], etudid), "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]), "prenom": etud["prenom"].lower().capitalize(), "nomprenom": etud["nomprenom"], "group": grc, "email": etud["email"], "emailperso": etud["emailperso"], "_css_row_class": css_row_class or "", } ) # Lignes en tête: row_coefs = { "nom": "", "prenom": "", "nomprenom": "", "group": "", "code": "", "_css_row_class": "sorttop fontitalic", "_table_part": "head", } row_poids = { "nom": "", "prenom": "", "nomprenom": "", "group": "", "code": "", "_css_row_class": "sorttop poids", "_table_part": "head", } row_note_max = { "nom": "", "prenom": "", "nomprenom": "", "group": "", "code": "", "_css_row_class": "sorttop fontitalic", "_table_part": "head", } row_moys = { "_css_row_class": "moyenne sortbottom", "_table_part": "foot", #'_nomprenom_td_attrs' : 'colspan="2" ', "nomprenom": "Moyenne (sans les absents) :", "comment": "", } # Ajoute les notes de chaque évaluation: for e in evals: e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) notes, nb_abs, nb_att = _add_eval_columns( e, evals_poids, ues, rows, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, key_mgr, note_sur_20, keep_numeric, format=format, ) columns_ids.append(e["evaluation_id"]) # if anonymous_listing: rows.sort(key=lambda x: x["code"] or "") else: rows.sort( key=lambda x: (x["nom"] or "", x["prenom"] or "") ) # sort by nom, prenom # Si module, ajoute la (les) "moyenne(s) du module: if mode == "module": if len(evals) > 1: # Moyenne de l'étudiant dans le module # Affichée même en APC à titre indicatif _add_moymod_column( sem["formsemestre_id"], moduleimpl_id, rows, columns_ids, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, keep_numeric, ) if is_apc: # Ajoute une colonne par UE _add_apc_columns( moduleimpl_id, evals_poids, ues, rows, columns_ids, titles, is_conforme, row_coefs, row_poids, row_note_max, row_moys, keep_numeric, ) # Ajoute colonnes emails tout à droite: if with_emails: columns_ids += ["email", "emailperso"] # Ajoute lignes en tête et moyennes if len(evals) > 0 and format != "bordereau": rows_head = [row_coefs] if is_apc: rows_head.append(row_poids) rows_head.append(row_note_max) rows = rows_head + rows rows.append(row_moys) # ajout liens HTMl vers affichage une evaluation: if format == "html" and len(evals) > 1: rlinks = {"_table_part": "head"} for e in evals: rlinks[e["evaluation_id"]] = "afficher" rlinks[ "_" + str(e["evaluation_id"]) + "_help" ] = "afficher seulement les notes de cette évaluation" rlinks["_" + str(e["evaluation_id"]) + "_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e["evaluation_id"], ) rlinks["_" + str(e["evaluation_id"]) + "_td_attrs"] = ' class="tdlink" ' rows.append(rlinks) if len(evals) == 1: # colonne "Rem." seulement si une eval if format == "html": # pas d'indication d'origine en pdf (pour affichage) columns_ids.append("expl_key") elif format == "xls" or format == "xml": columns_ids.append("comment") elif format == "bordereau": columns_ids.append("signatures") # titres divers: gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids]) if note_sur_20: gl = "¬e_sur_20%3Alist=yes" + gl if anonymous_listing: gl = "&anonymous_listing%3Alist=yes" + gl if hide_groups: gl = "&hide_groups%3Alist=yes" + gl if with_emails: gl = "&with_emails%3Alist=yes" + gl if len(evals) == 1: evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"])) hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats)) filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) if format == "bordereau": hh = " %d étudiants" % (len(etudid_etats)) hh += " %d absent" % (nb_abs) if nb_abs > 1: hh += "s" hh += ", %d en attente." % (nb_att) pdf_title = "
BORDEREAU DE SIGNATURES" pdf_title += "

%(titre)s" % sem pdf_title += "
(%(mois_debut)s - %(mois_fin)s)" % sem pdf_title += " semestre %s %s" % ( sem["semestre_id"], sem.get("modalite", ""), ) pdf_title += f"
Notes du module {module.code} - {module.titre}" pdf_title += "
Evaluation : %(description)s " % e if len(e["jour"]) > 0: pdf_title += " (%(jour)s)" % e pdf_title += "(noté sur %(note_max)s )

" % e else: hh = " %s, %s (%d étudiants)" % ( E["description"], gr_title, len(etudid_etats), ) if len(e["jour"]) > 0: pdf_title = "%(description)s (%(jour)s)" % e else: pdf_title = "%(description)s " % e caption = hh html_title = "" base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl html_next_section = ( '
%d absents, %d en attente.
' % (nb_abs, nb_att) ) else: filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename)) title = f"Notes {module.type_name()} {module.code} {module.titre}" title += " semestre %(titremois)s" % sem if gr_title and gr_title != "tous": title += " %s" % gr_title caption = title html_next_section = "" if format == "pdf" or format == "bordereau": caption = "" # same as pdf_title pdf_title = title html_title = f"""

Notes {module.type_name()} {module.code} {module.titre}

""" if not is_conforme: html_title += ( """
Poids des évaluations non conformes !
""" ) base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl # display tab = GenTable( titles=titles, columns_ids=columns_ids, rows=rows, html_sortable=True, base_url=base_url, filename=filename, origin="Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + "", caption=caption, html_next_section=html_next_section, page_title="Notes de " + sem["titremois"], html_title=html_title, pdf_title=pdf_title, html_class="table_leftalign notes_evaluation", preferences=sco_preferences.SemPreferences(modimpl_o["formsemestre_id"]), # html_generate_cells=False # la derniere ligne (moyennes) est incomplete ) if format == "bordereau": format = "pdf" t = tab.make_page(format=format, with_html_headers=False) if format != "html": return t if len(evals) > 1: all_complete = True for e in evals: if not e["eval_state"]["evalcomplete"]: all_complete = False if all_complete: eval_info = 'Evaluations prises en compte dans les moyennes' else: eval_info = 'Les évaluations en vert et orange sont prises en compte dans les moyennes. Celles en rouge n\'ont pas toutes leurs notes.' return html_form + eval_info + t + "

" else: # Une seule evaluation: ajoute histogramme histo = histogram_notes(notes) # 2 colonnes: histo, comments C = [ f'
Bordereau de Signatures (version PDF)', "\n", '

Répartition des notes:

" + histo + "

', ] commentkeys = list(key_mgr.items()) # [ (comment, key), ... ] commentkeys.sort(key=lambda x: int(x[1])) for (comment, key) in commentkeys: C.append( '(%s) %s
' % (key, comment) ) if commentkeys: C.append( 'Gérer les opérations
' % E["evaluation_id"] ) eval_info = "xxx" if E["eval_state"]["evalcomplete"]: eval_info = 'Evaluation prise en compte dans les moyennes' elif E["eval_state"]["evalattente"]: eval_info = 'Il y a des notes en attente (les autres sont prises en compte)' else: eval_info = 'Notes incomplètes, évaluation non prise en compte dans les moyennes' return ( sco_evaluations.evaluation_describe(evaluation_id=E["evaluation_id"]) + eval_info + html_form + t + "\n".join(C) ) def _add_eval_columns( e, evals_poids, ues, rows, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, K, note_sur_20, keep_numeric, format="html", ): """Add eval e""" nb_notes = 0 nb_abs = 0 nb_att = 0 sum_notes = 0 notes = [] # liste des notes numeriques, pour calcul histogramme uniquement evaluation_id = e["evaluation_id"] e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) for row in rows: etudid = row["etudid"] if etudid in NotesDB: val = NotesDB[etudid]["value"] if val is None: nb_abs += 1 if val == scu.NOTES_ATTENTE: nb_att += 1 # calcul moyenne SANS LES ABSENTS if val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE: if e["note_max"] > 0: valsur20 = val * 20.0 / e["note_max"] # remet sur 20 else: valsur20 = 0 notes.append(valsur20) # toujours sur 20 pour l'histogramme if note_sur_20: val = valsur20 # affichage notes / 20 demandé nb_notes = nb_notes + 1 sum_notes += val val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric) comment = NotesDB[etudid]["comment"] if comment is None: comment = "" explanation = "%s (%s) %s" % ( NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"), sco_users.user_info(NotesDB[etudid]["uid"])["nomcomplet"], comment, ) else: explanation = "" val_fmt = "" val = None if val is None: row["_" + str(evaluation_id) + "_td_attrs"] = 'class="etudabs" ' if not row.get("_css_row_class", ""): row["_css_row_class"] = "etudabs" # regroupe les commentaires if explanation: if explanation in K: expl_key = "(%s)" % K[explanation] else: K[explanation] = K.nextkey() expl_key = "(%s)" % K[explanation] else: expl_key = "" row.update( { evaluation_id: val_fmt, "_" + str(evaluation_id) + "_help": explanation, # si plusieurs evals seront ecrasés et non affichés: "comment": explanation, "expl_key": expl_key, "_expl_key_help": explanation, } ) row_coefs[evaluation_id] = "coef. %s" % e["coefficient"] if is_apc: if format == "html": row_poids[evaluation_id] = _mini_table_eval_ue_poids( evaluation_id, evals_poids, ues ) else: row_poids[evaluation_id] = e_o.get_ue_poids_str() if note_sur_20: nmax = 20.0 else: nmax = e["note_max"] if keep_numeric: row_note_max[evaluation_id] = nmax else: row_note_max[evaluation_id] = "/ %s" % nmax if nb_notes > 0: row_moys[evaluation_id] = scu.fmt_note( sum_notes / nb_notes, keep_numeric=keep_numeric ) row_moys[ "_" + str(evaluation_id) + "_help" ] = "moyenne sur %d notes (%s le %s)" % ( nb_notes, e["description"], e["jour"], ) else: row_moys[evaluation_id] = "" if len(e["jour"]) > 0: titles[evaluation_id] = "%(description)s (%(jour)s)" % e else: titles[evaluation_id] = "%(description)s " % e if e["eval_state"]["evalcomplete"]: titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"' elif e["eval_state"]["evalattente"]: titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_attente"' else: titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_incomplete"' return notes, nb_abs, nb_att # pour histogramme def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues): "contenu de la cellule: poids" return ( """" + "
""" + "".join([f"{ue.acronyme}" for ue in ues]) + "
" + "".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues]) + "
" ) def _add_moymod_column( formsemestre_id, moduleimpl_id, rows, columns_ids, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, keep_numeric, ): """Ajoute la colonne moymod à rows""" col_id = "moymod" nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_mod_moy nb_notes = 0 sum_notes = 0 notes = [] # liste des notes numeriques, pour calcul histogramme uniquement for row in rows: etudid = row["etudid"] val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI' row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric) row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' if not isinstance(val, str): notes.append(val) nb_notes = nb_notes + 1 sum_notes += val row_coefs[col_id] = "(avec abs)" if is_apc: row_poids[col_id] = "à titre indicatif" if keep_numeric: row_note_max[col_id] = 20.0 else: row_note_max[col_id] = "/ 20" titles[col_id] = "Moyenne module" columns_ids.append(col_id) if nb_notes > 0: row_moys[col_id] = "%.3g" % (sum_notes / nb_notes) row_moys["_" + col_id + "_help"] = "moyenne des moyennes" else: row_moys[col_id] = "" def _add_apc_columns( moduleimpl_id, evals_poids, ues, rows, columns_ids, titles, is_conforme: bool, row_coefs, row_poids, row_note_max, row_moys, keep_numeric, ): """Ajoute les colonnes moyennes vers les UE""" # On raccorde ici les nouveaux calculs de notes (BUT 2021) # sur l'ancien code ScoDoc # => On recharge tout dans les nouveaux modèles # rows est une liste de dict avec une clé "etudid" # on va y ajouter une clé par UE du semestre modimpl = ModuleImpl.query.get(moduleimpl_id) evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( moduleimpl_id ) etuds_moy_module = moy_mod.compute_module_moy( evals_notes, evals_poids, evaluations, evaluations_completes ) if is_conforme: # valeur des moyennes vers les UEs: for row in rows: for ue in ues: moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?") row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric) row[f"_moy_ue_{ue.id}_class"] = "moy_ue" # Nom et coefs des UE (lignes titres): ue_coefs = modimpl.module.ue_coefs if is_conforme: coef_class = "coef_mod_ue" else: coef_class = "coef_mod_ue_non_conforme" for ue in ues: col_id = f"moy_ue_{ue.id}" titles[col_id] = ue.acronyme columns_ids.append(col_id) coefs = [uc for uc in ue_coefs if uc.ue_id == ue.id] if coefs: row_coefs[f"moy_ue_{ue.id}"] = coefs[0].coef row_coefs[f"_moy_ue_{ue.id}_td_attrs"] = f' class="{coef_class}" '