From 78eeb9c67f21cafb37db4f080acf661ba9d46f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Fri, 2 Feb 2024 11:49:24 +0100 Subject: [PATCH] =?UTF-8?q?Export=20Excel=20:=20Ajoute=20un=20tableur=20do?= =?UTF-8?q?nnant=20les=20informations=20par=20=C3=A9tudiants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_affichage.py | 68 +--------- app/pe/pe_etudiant.py | 73 ++++++++++- app/pe/pe_jury.py | 283 +++++++++++++++++++++++++++++++++-------- app/pe/pe_semtag.py | 4 +- 4 files changed, 308 insertions(+), 120 deletions(-) diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index 4bb2b33d..48c1e33b 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -1,5 +1,3 @@ -from app.models import Formation, FormSemestre -from app.scodoc import codes_cursus from app import log PE_DEBUG = 0 @@ -9,73 +7,13 @@ if not PE_DEBUG: def pe_print(*a, **kw): # kw is ignored. log always add a newline log(" ".join(a)) + else: pe_print = print # print function # Affichage dans le tableur pe en cas d'absence de notes SANS_NOTE = "-" - -def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: - """Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité - d'un étudiant. - - Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec : - - * 2 le numéro du semestre, - * FI la modalité, - * 2014-2015 les dates - - Args: - semestre: Un ``FormSemestre`` - avec_fid: Ajoute le n° du semestre à la description - - Returns: - La chaine de caractères décrivant succintement le semestre - """ - formation: Formation = semestre.formation - parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) - - description = [ - parcours.SESSION_NAME.capitalize(), - str(semestre.semestre_id), - semestre.modalite, # eg FI ou FC - f"{semestre.date_debut.year}-{semestre.date_fin.year}", - ] - if avec_fid: - description.append(f"({semestre.formsemestre_id})") - - return " ".join(description) +NOM_STAT_GROUPE = "statistiques du groupe" +NOM_STAT_PROMO = "statistiques de la promo" -def etapes_du_cursus(semestres: dict[int, FormSemestre], nbre_etapes_max: int) -> list[str]: - """Partant d'un dictionnaire de semestres (qui retrace - la scolarité d'un étudiant), liste les noms des - semestres (en version abbrégée) - qu'un étudiant a suivi au cours de sa scolarité à l'IUT. - Les noms des semestres sont renvoyés dans un dictionnaire - ``{"etape i": nom_semestre_a_etape_i}`` - avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i, - le nom affiché est vide. - - La fonction suppose la liste des semestres triées par ordre - décroissant de date. - - Args: - semestres: une liste de ``FormSemestre`` - nbre_etapes_max: le nombre d'étapes max prise en compte - - Returns: - Une liste de nom de semestre (dans le même ordre que les ``semestres``) - - See also: - app.pe.pe_affichage.nom_semestre_etape - """ - assert len(semestres) <= nbre_etapes_max - - noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()] - noms = noms[::-1] # trie par ordre croissant - - dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)} - for (i, nom) in enumerate(noms): # Charge les noms de semestres - dico[f"Etape {i+1}"] = nom - return dico diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 811180df..c459752f 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -37,8 +37,9 @@ Created on 17/01/2024 """ import pandas as pd -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, Formation from app.pe import pe_comp, pe_affichage +from app.scodoc import codes_cursus class EtudiantsJuryPE: @@ -467,7 +468,7 @@ class EtudiantsJuryPE: } # Ajout des noms de semestres parcourus - etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max) + etapes = etapes_du_cursus(formsemestres, nbre_semestres_max) administratif[etudid] |= etapes # Construction du dataframe @@ -647,3 +648,71 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSeme dernier_semestre = semestres[fid] return dernier_semestre return None + + +def etapes_du_cursus( + semestres: dict[int, FormSemestre], nbre_etapes_max: int +) -> list[str]: + """Partant d'un dictionnaire de semestres (qui retrace + la scolarité d'un étudiant), liste les noms des + semestres (en version abbrégée) + qu'un étudiant a suivi au cours de sa scolarité à l'IUT. + Les noms des semestres sont renvoyés dans un dictionnaire + ``{"etape i": nom_semestre_a_etape_i}`` + avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i, + le nom affiché est vide. + + La fonction suppose la liste des semestres triées par ordre + décroissant de date. + + Args: + semestres: une liste de ``FormSemestre`` + nbre_etapes_max: le nombre d'étapes max prise en compte + + Returns: + Une liste de nom de semestre (dans le même ordre que les ``semestres``) + + See also: + app.pe.pe_affichage.nom_semestre_etape + """ + assert len(semestres) <= nbre_etapes_max + + noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()] + noms = noms[::-1] # trie par ordre croissant + + dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)} + for i, nom in enumerate(noms): # Charge les noms de semestres + dico[f"Etape {i+1}"] = nom + return dico + + +def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: + """Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité + d'un étudiant. + + Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec : + + * 2 le numéro du semestre, + * FI la modalité, + * 2014-2015 les dates + + Args: + semestre: Un ``FormSemestre`` + avec_fid: Ajoute le n° du semestre à la description + + Returns: + La chaine de caractères décrivant succintement le semestre + """ + formation: Formation = semestre.formation + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) + + description = [ + parcours.SESSION_NAME.capitalize(), + str(semestre.semestre_id), + semestre.modalite, # eg FI ou FC + f"{semestre.date_debut.year}-{semestre.date_fin.year}", + ] + if avec_fid: + description.append(f"({semestre.formsemestre_id})") + + return " ".join(description) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 0d9d2c82..7c45d4c8 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -48,6 +48,9 @@ from zipfile import ZipFile import numpy as np +from app.pe import pe_comp +from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE + from app.pe.pe_tabletags import TableTag from app.scodoc.gen_tables import SeqGenTable from app.pe.pe_etudiant import EtudiantsJuryPE @@ -119,7 +122,9 @@ class JuryPE(object): self._gen_xls_semestre_taggues(zipfile) self._gen_xls_trajectoires(zipfile) self._gen_xls_aggregats(zipfile) - self._gen_xls_synthese(zipfile) + self._gen_xls_synthese_jury_par_tag(zipfile) + self._gen_xls_synthese_par_etudiant(zipfile) + # Fin !!!! Tada :) @@ -237,13 +242,13 @@ class JuryPE(object): path="details", ) - def _gen_xls_synthese(self, zipfile: ZipFile): - """Synthèse des éléments du jury PE""" + def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile): + """Synthèse des éléments du jury PE tag par tag""" # Synthèse des éléments du jury PE - self.synthese = self.synthetise_juryPE() + self.synthese = self.synthetise_jury_par_tags() # Export des données => mode 1 seule feuille -> supprimé - pe_affichage.pe_print("*** Export du jury de synthese") + pe_affichage.pe_print("*** Export du jury de synthese par tags") output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" @@ -254,7 +259,27 @@ class JuryPE(object): output.seek(0) self.add_file_to_zip( - zipfile, f"synthese_jury_{self.diplome}.xlsx", output.read() + zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read() + ) + + def _gen_xls_synthese_par_etudiant(self, zipfile: ZipFile): + """Synthèse des éléments du jury PE, étudiant par étudiant""" + # Synthèse des éléments du jury PE + synthese = self.synthetise_jury_par_etudiants() + + # Export des données => mode 1 seule feuille -> supprimé + pe_affichage.pe_print("*** Export du jury de synthese par étudiants") + output = io.BytesIO() + with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated + output, engine="openpyxl" + ) as writer: + for onglet, df in synthese.items(): + # écriture dans l'onglet: + df.to_excel(writer, onglet, index=True, header=True) + output.seek(0) + + self.add_file_to_zip( + zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read() ) def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""): @@ -292,10 +317,11 @@ class JuryPE(object): # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** - def synthetise_juryPE(self): - """Synthétise tous les résultats du jury PE dans des dataframes""" + def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]: + """Synthétise tous les résultats du jury PE dans des dataframes, + dont les onglets sont les tags""" - pe_affichage.pe_print("*** Synthèse finale des moyennes ***") + pe_affichage.pe_print("*** Synthèse finale des moyennes par tag***") synthese = {} pe_affichage.pe_print(" -> Synthèse des données administratives") @@ -332,65 +358,102 @@ class JuryPE(object): } for aggregat in aggregats: + # Le dictionnaire par défaut des moyennes + donnees[etudid] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome) + # La trajectoire de l'étudiant sur l'aggrégat trajectoire = self.trajectoires.suivi[etudid][aggregat] - - # La note de l'étudiant (chargement à venir) - note = np.nan - - # L'affichage de l'aggrégat dans le tableur excel - descr = pe_comp.PARCOURS[aggregat]["descr"] - # Les moyennes par tag de cette trajectoire - donnees[etudid] |= { - (descr, "", "note"): pe_affichage.SANS_NOTE, - (descr, "statistique du groupe", "class."): pe_affichage.SANS_NOTE, - (descr, "statistique du groupe", "min"): pe_affichage.SANS_NOTE, - (descr, "statistique du groupe", "moy"): pe_affichage.SANS_NOTE, - (descr, "statistique du groupe", "max"): pe_affichage.SANS_NOTE, - } if trajectoire: trajectoire_tagguee = self.trajectoires_tagguees[ trajectoire.trajectoire_id ] - if tag in trajectoire_tagguee.moyennes_tags: - bilan = trajectoire_tagguee.moyennes_tags[tag] - note = TableTag.get_note_for_df(bilan, etudid) - if note != np.nan: - donnees[etudid] |= { - (descr, "", "note"): note, - (descr, "statistique du groupe", "class."): TableTag.get_class_for_df(bilan, etudid), - (descr, "statistique du groupe", "min"): TableTag.get_min_for_df(bilan), - (descr, "statistique du groupe", "moy"): TableTag.get_moy_for_df(bilan), - (descr, "statistique du groupe", "max"): TableTag.get_max_for_df(bilan) - } + else: + trajectoire_tagguee = None - """L'interclassement""" + # L'interclassement interclass = self.interclassements_taggues[aggregat] - donnees[etudid] |= { - (descr, f"statistique de la promotion {self.diplome}", "class."): pe_affichage.SANS_NOTE, - (descr, f"statistique de la promotion {self.diplome}", "min"): pe_affichage.SANS_NOTE, - (descr, f"statistique de la promotion {self.diplome}", "moy"): pe_affichage.SANS_NOTE, - (descr, f"statistique de la promotion {self.diplome}", "max"): pe_affichage.SANS_NOTE, - } - if tag in interclass.moyennes_tags: - bilan = interclass.moyennes_tags[tag] - if note != np.nan: - donnees[etudid] |= { - (descr, f"statistique de la promotion {self.diplome}", "class."): TableTag.get_class_for_df(bilan, etudid), - (descr, f"statistique de la promotion {self.diplome}", "min"): TableTag.get_min_for_df(bilan), - (descr, f"statistique de la promotion {self.diplome}", "moy"): TableTag.get_moy_for_df(bilan), - (descr, f"statistique de la promotion {self.diplome}", "max"): TableTag.get_max_for_df(bilan) - } + # Injection des données dans un dictionnaire + donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome) + + # Fin de l'aggrégat # Construction du dataFrame df = pd.DataFrame.from_dict(donnees, orient="index") # Tri par nom/prénom - df.sort_values(by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True) + df.sort_values( + by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True + ) return df + def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]: + """Synthétise tous les résultats du jury PE dans des dataframes, + dont les onglets sont les étudiants""" + pe_affichage.pe_print("*** Synthèse finale des moyennes par étudiants***") + + synthese = {} + pe_affichage.pe_print(" -> Synthèse des données administratives") + synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) + + etudids = list(self.diplomes_ids) + + for etudid in etudids: + etudiant = self.etudiants.identites[etudid] + nom = etudiant.nom + prenom = etudiant.prenom[0] # initial du prénom + + onglet = f"{nom} {prenom}. ({etudid})" + if len(onglet) > 32: # limite sur la taille des onglets + fin_onglet = f"{prenom}. ({etudid})" + onglet = f"{nom[:32-len(fin_onglet)-2]}." + fin_onglet + + pe_affichage.pe_print(f" -> Synthèse de l'étudiant {etudid}") + synthese[onglet] = self.df_synthese_etudiant(etudid) + return synthese + + def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame: + """Créé un DataFrame pour un étudiant donné par son etudid, retraçant + toutes ses moyennes aux différents tag et aggrégats""" + tags = self.do_tags_list(self.interclassements_taggues) + aggregats = pe_comp.TOUS_LES_PARCOURS + + donnees = {} + + for tag in tags: + # Une ligne pour le tag + donnees[tag] = {("", "", "tag"): tag} + + for aggregat in aggregats: + # Le dictionnaire par défaut des moyennes + donnees[tag] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome) + + # La trajectoire de l'étudiant sur l'aggrégat + trajectoire = self.trajectoires.suivi[etudid][aggregat] + if trajectoire: + trajectoire_tagguee = self.trajectoires_tagguees[ + trajectoire.trajectoire_id + ] + else: + trajectoire_tagguee = None + + # L'interclassement + interclass = self.interclassements_taggues[aggregat] + + # Injection des données dans un dictionnaire + donnees[tag] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome) + + + # Fin de l'aggrégat + # Construction du dataFrame + df = pd.DataFrame.from_dict(donnees, orient="index") + + # Tri par nom/prénom + df.sort_values( + by=[("", "", "tag")], inplace=True + ) + return df def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. @@ -485,3 +548,121 @@ def compute_interclassements( aggregats_interclasses_taggues[nom_aggregat] = interclass return aggregats_interclasses_taggues + +def get_defaut_dict_synthese_aggregat(aggregat: str, diplome: int) -> dict: + """Renvoie le dictionnaire de synthèse (à intégrer dans + un tableur excel) pour décrire les résultats d'un aggrégat""" + # L'affichage de l'aggrégat dans le tableur excel + descr = pe_comp.PARCOURS[aggregat]["descr"] + + nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" + donnees = { + (descr, "", "note"): SANS_NOTE, + # Les stat du groupe + (descr, NOM_STAT_GROUPE, "class."): SANS_NOTE, + (descr, NOM_STAT_GROUPE, "min"): SANS_NOTE, + (descr, NOM_STAT_GROUPE, "moy"): SANS_NOTE, + (descr, NOM_STAT_GROUPE, "max"): SANS_NOTE, + # Les stats de l'interclassement dans la promo + (descr, nom_stat_promo, "class."): SANS_NOTE, + ( + descr, + nom_stat_promo, + "min", + ): SANS_NOTE, + ( + descr, + nom_stat_promo, + "moy", + ): SANS_NOTE, + ( + descr, + nom_stat_promo, + "max", + ): SANS_NOTE, + } + return donnees + + +def get_dict_synthese_aggregat( + aggregat: str, + trajectoire_tagguee: TrajectoireTag, + interclassement_taggue: AggregatInterclasseTag, + etudid: int, + tag: str, + diplome: int +): + """Renvoie le dictionnaire (à intégrer au tableur excel de synthese) + traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée + à l'aggrégat donné et pour un tag donné""" + donnees = {} + # L'affichage de l'aggrégat dans le tableur excel + descr = pe_comp.PARCOURS[aggregat]["descr"] + + # La note de l'étudiant (chargement à venir) + note = np.nan + + # Les données de la trajectoire tagguée pour le tag considéré + if trajectoire_tagguee and tag in trajectoire_tagguee.moyennes_tags: + bilan = trajectoire_tagguee.moyennes_tags[tag] + + # La moyenne de l'étudiant + note = TableTag.get_note_for_df(bilan, etudid) + + # Statistiques sur le groupe + if note != np.nan: + # Les moyennes de cette trajectoire + donnees |= { + (descr, "", "note"): note, + ( + descr, + NOM_STAT_GROUPE, + "class.", + ): TableTag.get_class_for_df(bilan, etudid), + ( + descr, + NOM_STAT_GROUPE, + "min", + ): TableTag.get_min_for_df(bilan), + ( + descr, + NOM_STAT_GROUPE, + "moy", + ): TableTag.get_moy_for_df(bilan), + ( + descr, + NOM_STAT_GROUPE, + "max", + ): TableTag.get_max_for_df(bilan), + } + + # L'interclassement + if tag in interclassement_taggue.moyennes_tags: + bilan = interclassement_taggue.moyennes_tags[tag] + + if note != np.nan: + nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" + + donnees |= { + ( + descr, + nom_stat_promo, + "class.", + ): TableTag.get_class_for_df(bilan, etudid), + ( + descr, + nom_stat_promo, + "min", + ): TableTag.get_min_for_df(bilan), + ( + descr, + nom_stat_promo, + "moy", + ): TableTag.get_moy_for_df(bilan), + ( + descr, + nom_stat_promo, + "max", + ): TableTag.get_max_for_df(bilan), + } + return donnees diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index d77f1caf..0b8a6238 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -35,7 +35,7 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ - +import app.pe.pe_etudiant from app import db, log from app.comp import res_sem, moy_ue, moy_sem from app.comp.res_compat import NotesTableCompat @@ -133,7 +133,7 @@ class SemestreTag(TableTag): def get_repr(self): """Nom affiché pour le semestre taggué""" - return pe_affichage.nom_semestre_etape(self.formsemestre, avec_fid=True) + return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) def compute_moyenne_tag(self, tag: str) -> list: """Calcule la moyenne des étudiants pour le tag indiqué,