# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 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 """ from collections import defaultdict import numpy as np import pandas as pd import flask from flask import url_for, g, request from app.comp import res_sem from app.comp import moy_mod from app.comp.moy_mod import ModuleImplResults from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, Module from app.models.etudiants import Identite from app.models.evaluations import Evaluation from app.models.moduleimpls import ModuleImpl from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_etud import etud_sort_key from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_users import app.scodoc.sco_utils as scu from app.scodoc.gen_tables import GenTable from app.scodoc.htmlutils import histogram_notes import sco_version def do_evaluation_listenotes( evaluation_id=None, moduleimpl_id=None, fmt="html" ) -> tuple[str | flask.Response, str]: """ Affichage des notes d'une évaluation (si evaluation_id) ou de toutes les évaluations d'un module (si moduleimpl_id) """ mode = None evaluations: list[Evaluation] = [] if moduleimpl_id is not None: mode = "module" modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) evaluations = modimpl.evaluations.all() elif evaluation_id is not None: mode = "eval" evaluations = Evaluation.query.filter_by(id=evaluation_id).all() else: raise ValueError("missing argument: evaluation or module") if not evaluations: return "

Aucune évaluation !

", "ScoDoc" evaluation = evaluations[0] modimpl = evaluation.moduleimpl # il y a au moins une evaluation # description de l'evaluation if evaluation_id is not None: H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)] page_title = f"Notes {evaluation.description or modimpl.module.code}" else: H = [] page_title = f"Notes {modimpl.module.code}" # groupes groups = sco_groups.do_evaluation_listegroupes(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(evaluations) > 1: descr = [ ( "moduleimpl_id", {"default": modimpl.id, "input_type": "hidden"}, ) ] else: descr = [ ( "evaluation_id", {"default": 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", # consultation 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( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.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"] group_ids = [x for x in tf[2]["group_ids"] if x != ""] return ( _make_table_notes( tf[1], evaluations, fmt=fmt, note_sur_20=note_sur_20, anonymous_listing=anonymous_listing, group_ids=group_ids, hide_groups=hide_groups, with_emails=with_emails, mode=mode, ), page_title, ) def _make_table_notes( html_form, evaluations: list[Evaluation], fmt: str = "", note_sur_20=False, anonymous_listing=False, hide_groups=False, with_emails=False, group_ids: list[int] | None = None, mode="module", # "eval" or "module" ) -> str: """Table liste notes (une seule évaluation ou toutes celles d'un module)""" group_ids = group_ids or [] if not evaluations: return "

Aucune évaluation !

" evaluation = evaluations[0] modimpl = evaluation.moduleimpl module: Module = modimpl.module formsemestre: FormSemestre = modimpl.formsemestre is_apc = module.formation.is_apc() if is_apc: res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) is_conforme = modimpl.check_apc_conformity(res) evals_poids, ues = moy_mod.load_evaluations_poids(modimpl.id) if not ues: is_apc = False else: evals_poids, ues = None, None is_conforme = True # (debug) check that all evals are in same module: for e in evaluations: if e.moduleimpl_id != modimpl.id: raise ValueError("invalid evaluations list") if fmt == "xls" or fmt == "json": 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(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 fmt in {"xls", "xml", "json"}: columns_ids = ["etudid", "nom", "prenom"] else: columns_ids = ["nomprenom"] if not hide_groups and fmt not in {"xml", "json"}: # n'indique pas les groupes en xml et json car notation "humaine" ici 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) -> str: "get new key (int)" 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( evaluation.id, groups, include_demdef=True ) for etudid, etat in etudid_etats: css_row_class = None # infos identite etudiant etud: Identite = Identite.query.filter_by( id=etudid, dept_id=g.scodoc_dept_id ).first() if etud is None: continue if etat == scu.INSCRIT: # si inscrit, indique groupe groups = sco_groups.get_etud_groups(etudid, formsemestre.id) grc = sco_groups.listgroups_abbrev(groups) else: if etat == scu.DEMISSION: grc = "DEM" # attention: ce code est re-ecrit plus bas, ne pas le changer (?) css_row_class = "etuddem" else: grc = etat code = getattr(etud, 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": url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etudid, ), "_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{ etud.sort_key}" """, "prenom": etud.prenom.lower().capitalize(), "nom_usuel": etud.nom_usuel, "nomprenom": etud.nomprenom, "group": grc, "_group_td_attrs": 'class="group"', "email": etud.get_first_email(), "emailperso": etud.get_first_email("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 :", "comment": "", } # Ajoute les notes de chaque évaluation: evals_state: dict[int, dict] = {} for e in evaluations: evals_state[e.id] = sco_evaluations.do_evaluation_etat(e.id) notes, nb_abs, nb_att = _add_eval_columns( e, evals_state[e.id], evals_poids, ues, rows, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, key_mgr, note_sur_20, keep_numeric, fmt=fmt, ) columns_ids.append(e.id) # if anonymous_listing: rows.sort(key=lambda x: x["code"] or "") else: # sort by nom, prenom, sans accents rows.sort(key=etud_sort_key) # Si module, ajoute la (les) "moyenne(s) du module: if mode == "module": if len(evaluations) > 1: # Moyenne de l'étudiant dans le module # Affichée même en APC à titre indicatif _add_moymod_column( formsemestre.id, modimpl.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( modimpl, 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(evaluations) > 0 and fmt != "bordereau" and fmt != "json": 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 fmt == "html" and len(evaluations) > 1: rlinks = {"_table_part": "head"} for e in evaluations: rlinks[e.id] = "afficher" rlinks["_" + str(e.id) + "_help"] = ( "afficher seulement les notes de cette évaluation" ) rlinks["_" + str(e.id) + "_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e.id, ) rlinks["_" + str(e.id) + "_td_attrs"] = ' class="tdlink" ' rows.append(rlinks) if len(evaluations) == 1: # colonne "Rem." seulement si une eval if fmt == "html": # pas d'indication d'origine en pdf (pour affichage) columns_ids.append("expl_key") elif fmt == "xls" or fmt == "xml": columns_ids.append("comment") elif fmt == "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(evaluations) == 1: evalname = f"""{module.code}-{ evaluation.date_debut.replace(tzinfo=None).isoformat() if evaluation.date_debut else ""}""" hh = "%s, %s (%d étudiants)" % ( evaluation.description, gr_title, len(etudid_etats), ) filename = scu.make_filename(f"notes_{evalname}_{gr_title_filename}") if fmt == "bordereau": hh = f""" {len(etudid_etats)} étudiants { nb_abs} absent{'s' if nb_abs > 1 else ''}, {nb_att} en attente.""" # Attention: ReportLab supporte seulement '
', pas '
' ! pdf_title = f"""
BORDEREAU DE SIGNATURES

{formsemestre.titre or ''}
({formsemestre.mois_debut()} - {formsemestre.mois_fin()}) semestre {formsemestre.semestre_id} {formsemestre.modalite or ""}
Notes du module {module.code} - {module.titre}
Évaluation : {evaluation.description} """ if evaluation.date_debut: pdf_title += f" ({evaluation.date_debut.strftime('%d/%m/%Y')})" pdf_title += "(noté sur {evaluation.note_max} )

" else: hh = " %s, %s (%d étudiants)" % ( evaluation.description, gr_title, len(etudid_etats), ) if evaluation.date_debut: pdf_title = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" else: pdf_title = evaluation.description or f"évaluation dans {module.code}" caption = hh html_title = "" base_url = ( url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id, ) + gl ) html_next_section = f"""
{nb_abs} absents, {nb_att} en attente.
""" else: # Plusieurs évaluations (module) filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename)) title = f"Notes {module.type_name()} {module.code} {module.titre}" title += f""" semestre {formsemestre.titre_mois()}""" if gr_title and gr_title != "tous": title += " {gr_title}" caption = title html_next_section = "" if fmt == "pdf" or fmt == "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 = ( url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) + gl ) # display tab = GenTable( titles=titles, columns_ids=columns_ids, rows=rows, html_sortable=True, base_url=base_url, filename=filename, origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", caption=caption, html_next_section=html_next_section, page_title="Notes de " + formsemestre.titre_mois(), html_title=html_title, pdf_title=pdf_title, html_class="notes_evaluation", preferences=sco_preferences.SemPreferences(formsemestre.id), # html_generate_cells=False # la derniere ligne (moyennes) est incomplete ) if fmt == "bordereau": fmt = "pdf" t = tab.make_page(fmt=fmt, with_html_headers=False) if fmt != "html": return t if len(evaluations) > 1: all_complete = True for e in evaluations: if not evals_state[e.id]["evalcomplete"]: all_complete = False if all_complete: eval_info = """Évaluations 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.""" if is_apc: eval_info += """ La moyenne indicative est la moyenne des moyennes d'UE, et n'est pas utilisée en BUT. Les moyennes sur le groupe sont estimées sans les absents (sauf pour les moyennes des moyennes d'UE) ni les démissionnaires.""" eval_info += """""" return html_form + eval_info + t + "

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

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(f"""({key}) {comment}
""") if commentkeys: C.append( f"""Gérer les opérations
""" ) eval_info = "xxx" if evals_state[evaluation.id]["evalcomplete"]: eval_info = 'Evaluation prise en compte dans les moyennes' elif evals_state[evaluation.id]["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=evaluation.id) + eval_info + html_form + t + "\n".join(C) ) def _add_eval_columns( evaluation: Evaluation, eval_state, evals_poids: pd.DataFrame | None, ues, rows, titles, row_coefs, row_poids, row_note_max, row_moys, is_apc, K, note_sur_20, keep_numeric, fmt="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 # actifs == inscrit au semestre, non DEM ni DEF: _, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs() notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) if evaluation.date_debut: titles[evaluation.id] = ( f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" ) else: titles[evaluation.id] = f"{evaluation.description} " if eval_state["evalcomplete"]: klass = "eval_complete" elif eval_state["evalattente"]: klass = "eval_attente" else: klass = "eval_incomplete" titles[evaluation.id] += " (non prise en compte)" titles[f"_{evaluation.id}_td_attrs"] = f'class="{klass}"' for row in rows: etudid = row["etudid"] if etudid in notes_db: val = notes_db[etudid]["value"] if val is None: nb_abs += 1 if val == scu.NOTES_ATTENTE: nb_att += 1 # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES if ( (etudid in etudids_actifs) and val is not None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE ): if evaluation.note_max > 0: valsur20 = val * 20.0 / evaluation.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 = notes_db[etudid]["comment"] if comment is None: comment = "" explanation = "%s (%s) %s" % ( notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"), sco_users.user_info(notes_db[etudid]["uid"])["nomcomplet"], comment, ) else: if (etudid in etudids_actifs) and evaluation.publish_incomplete: # Note manquante mais prise en compte immédiate: affiche ATT val = scu.NOTES_ATTENTE val_fmt = "ATT" explanation = "non saisie mais prise en compte immédiate" else: explanation = "" val_fmt = "" val = None cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get( val_fmt, "" ) if val is None: row[f"_{evaluation.id}_td_attrs"] = f'class="etudabs {cell_class}" ' if not row.get("_css_row_class", ""): row["_css_row_class"] = "etudabs" else: row[f"_{evaluation.id}_td_attrs"] = f'class="{cell_class}" ' # 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] = f"coef. {evaluation.coefficient:g}" if is_apc: if fmt == "html": row_poids[evaluation.id] = _mini_table_eval_ue_poids( evaluation.id, evals_poids, ues ) else: row_poids[evaluation.id] = evaluation.get_ue_poids_str() if note_sur_20: nmax = 20.0 else: nmax = evaluation.note_max if keep_numeric: row_note_max[evaluation.id] = nmax else: row_note_max[evaluation.id] = f"/ {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, evaluation.description, ( evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else "" ), ) ) else: row_moys[evaluation.id] = "" return notes, nb_abs, nb_att # pour histogramme def _mini_table_eval_ue_poids( evaluation_id: int, evals_poids: pd.DataFrame, ues ) -> str: "contenu de la cellule: poids" ue_poids = [ (ue.acronyme, evals_poids[ue.id][evaluation_id]) for ue in ues if (evals_poids[ue.id][evaluation_id] or 0) > 0 ] return ( """" + "
""" + "".join([f"{up[0]}" for up in ue_poids]) + "
" + "".join([f"{up[1]}" for up in ue_poids]) + "
" ) 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" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) _, etudids_actifs = formsemestre.etudids_actifs() 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 etudid in etudids_actifs and not isinstance(val, str): notes.append(val) if not np.isnan(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( modimpl, 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 nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre) modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] _, etudids_actifs = modimpl.formsemestre.etudids_actifs() # les UE dans lesquelles ce module a un coef non nul: ues_with_coef = nt.modimpl_coefs_df[modimpl.id][ nt.modimpl_coefs_df[modimpl.id] > 0 ].index ues = [ue for ue in ues if ue.id in ues_with_coef] sum_by_ue = defaultdict(float) nb_notes_by_ue = defaultdict(int) if is_conforme: # valeur des moyennes vers les UEs: for row in rows: for ue in ues: moy_ue = modimpl_results.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" if ( isinstance(moy_ue, float) and not np.isnan(moy_ue) and row["etudid"] in etudids_actifs ): sum_by_ue[ue.id] += moy_ue nb_notes_by_ue[ue.id] += 1 # 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}" ' if nb_notes_by_ue[ue.id] > 0: row_moys[col_id] = "%.3g" % (sum_by_ue[ue.id] / nb_notes_by_ue[ue.id]) row_moys["_" + col_id + "_help"] = "moyenne des moyennes" else: row_moys[col_id] = ""