diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index e488fe06..40f2c45c 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -35,41 +35,56 @@ Created on 17/01/2024 @author: barasc """ +import pandas as pd + from app.models import FormSemestre, Identite import app.pe.pe_affichage as pe_affichage import app.pe.pe_comp as pe_comp +from app.pe import pe_comp, pe_affichage class EtudiantsJuryPE: - """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" def __init__(self, annee_diplome: int): """ + Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE Args: annee_diplome: L'année de diplomation """ - self.annee_diplome = annee_diplome + """L'année du diplôme""" - "Les identités des étudiants traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT - "Les cursus (semestres suivis, abandons) des étudiants" + "Les identités des étudiants traités pour le jury" + self.cursus = {} - """Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants""" + "Les cursus (semestres suivis, abandons) des étudiants" + self.trajectoires = {} + """Les trajectoires/chemins de semestres suivis par les étudiants + pour atteindre un aggrégat donné + (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)""" - "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" self.etudiants_diplomes = {} - self.diplomes_ids = {} + """Les identités des étudiants à considérer au jury (ceux qui seront effectivement diplômés)""" + + self.diplomes_ids = {} + """Les etudids des étudiants diplômés""" - "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" self.etudiants_ids = {} - """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" + """Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons). + Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant + été réorientés ou ayant abandonnés)""" self.cosemestres: dict[int, FormSemestre] = None "Les cosemestres donnant lieu à même année de diplome" + self.abandons = {} + """Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)""" + self.abandons_ids = {} + """Les etudids des étudiants redoublants/réorientés""" + def find_etudiants(self, formation_id: int): """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant de manière automatique par rapport à leur année de diplomation ``annee_diplome`` @@ -85,7 +100,9 @@ class EtudiantsJuryPE: cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None) self.cosemestres = cosemestres - pe_affichage.pe_print(f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés") + pe_affichage.pe_print( + f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés" + ) pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres") self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) @@ -109,23 +126,30 @@ class EtudiantsJuryPE: # Analyse son parcours pour atteindre chaque semestre de la formation self.structure_cursus_etudiant(etudid) - # Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris self.etudiants_diplomes = self.get_etudiants_diplomes() self.diplomes_ids = set(self.etudiants_diplomes.keys()) - self.etudiants_ids = set(self.identites) + self.etudiants_ids = set(self.identites.keys()) """Les étudiants dont il faut calculer les moyennes""" self.formsemestres_jury_ids = self.get_formsemestres() """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" + # Les abandons (pour debug) + self.abandons = self.get_etudiants_redoublants_ou_reorientes() + self.abandons_ids = set(self.abandons) + # Synthèse pe_affichage.pe_print( f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" ) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) - pe_affichage.pe_print(f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon") + assert nbre_abandons == len(self.abandons_ids) + + pe_affichage.pe_print( + f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon" + ) pe_affichage.pe_print( f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" ) @@ -137,14 +161,8 @@ class EtudiantsJuryPE: # " => semestres dont il faut calculer les moyennes : " # + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) # ) - # Les abandons (pour debug) - self.abandons = sorted( - [ - cursus["nom"] - for etudid, cursus in self.cursus.items() - if etudid not in self.diplomes_ids - ] - ) + + def get_etudiants_diplomes(self) -> dict[int, Identite]: """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` @@ -164,6 +182,23 @@ class EtudiantsJuryPE: etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants + def get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]: + """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` + dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront + pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire. + + Returns: + Un dictionnaire `{etudid: Identite(etudid)}` + """ + etudids = [ + etudid + for etudid in self.cursus + if self.cursus[etudid]["diplome"] != self.annee_diplome + or self.cursus[etudid]["abandon"] is True + ] + etudiants = {etudid: self.identites[etudid] for etudid in etudids} + return etudiants + def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): """Analyse le cursus d'un étudiant pouvant être : @@ -254,7 +289,9 @@ class EtudiantsJuryPE: } # les semestres de n°i de l'étudiant self.cursus[etudid][nom_sem] = semestres_i - def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str): + def get_trajectoire( + self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str + ): """Ensemble des semestres parcourus par un étudiant pour l'amener à un semestre terminal. @@ -279,13 +316,15 @@ class EtudiantsJuryPE: numero_semestre_terminal = formsemestre_final.semestre_id semestres_significatifs = self.get_semestres_significatifs(etudid) - if nom_aggregat.startswith("S"): # les semestres - numero_semestres_possibles =[numero_semestre_terminal] - elif nom_aggregat.endswith("A"): # les années - numero_semestres_possibles = [int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"]] + if nom_aggregat.startswith("S"): # les semestres + numero_semestres_possibles = [numero_semestre_terminal] + elif nom_aggregat.endswith("A"): # les années + numero_semestres_possibles = [ + int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"] + ] assert numero_semestre_terminal in numero_semestres_possibles - else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal) - numero_semestres_possibles = list(range(1, numero_semestre_terminal+1)) + else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal) + numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1)) semestres_aggreges = {} for fid, semestre in semestres_significatifs.items(): @@ -382,14 +421,65 @@ class EtudiantsJuryPE: else: raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") - def nbre_etapes_max_diplomes(self): - """Connaissant les étudiants diplomes du jury PE, + def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int: + """Partant d'un ensemble d'étudiants, nombre de semestres (étapes) maximum suivis par les étudiants du jury. + + Args: + etudids: Liste d'étudid d'étudiants """ nbres_semestres = [] - for etudid in self.diplomes_ids: + for etudid in etudids: nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) - return max(nbres_semestres) + if not nbres_semestres: + return 0 + else: + return max(nbres_semestres) + + def df_administratif(self, etudids: list[int]) -> pd.DataFrame: + """Synthétise toutes les données administratives d'un groupe + d'étudiants fournis par les etudid dans un dataFrame + + Args: + etudids: La liste des étudiants à prendre en compte + """ + + etudids = list(etudids) + + # Récupération des données des étudiants + administratif = {} + nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids) + + for etudid in etudids: + etudiant = self.identites[etudid] + cursus = self.cursus[etudid] + formsemestres = cursus["formsemestres"] + + if cursus["diplome"]: + diplome = cursus["diplome"] + else: + diplome = "indéterminé" + + administratif[etudid] = { + "Nom": etudiant.nom, + "Prenom": etudiant.prenom, + "Civilite": etudiant.civilite_str, + "Age": pe_comp.calcul_age(etudiant.date_naissance), + "Date d'entree": cursus["entree"], + "Date de diplome": diplome, + "Nbre de semestres": len(formsemestres), + } + + # Ajout des noms de semestres parcourus + etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max) + administratif[etudid] |= etapes + + # Construction du dataframe + df = pd.DataFrame.from_dict(administratif, orient="index") + + # Tri par nom/prénom + df.sort_values(by=["Nom", "Prenom"], inplace=True) + return df def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 645d0b1d..882590e5 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,7 +50,6 @@ from app.scodoc.gen_tables import SeqGenTable from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire import app.pe.pe_comp as pe_comp -import app.pe.pe_affichage as pe_affichage from app.pe.pe_semtag import SemestreTag from app.pe.pe_interclasstag import AggregatInterclasseTag from app.pe.pe_trajectoiretag import TrajectoireTag @@ -100,6 +99,7 @@ class JuryPE(object): "Nom du zip où ranger les fichiers générés" self.zipdata = io.BytesIO() + with ZipFile(self.zipdata, "w") as zipfile: # Chargement des étudiants à prendre en compte dans le jury pe_affichage.pe_print( @@ -112,100 +112,125 @@ class JuryPE(object): self.etudiants.find_etudiants(self.formation_id) self.diplomes_ids = self.etudiants.diplomes_ids - # Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE - pe_affichage.pe_print("*** Génère les semestres taggués") - self.semestres_taggues = compute_semestres_tag(self.etudiants) - # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter(output, engine="openpyxl") as writer: - for formsemestretag in self.semestres_taggues.values(): - onglet = formsemestretag.nom - df = formsemestretag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + if self.diplomes_ids: + onglet = "diplômés" + df_diplome = self.etudiants.df_administratif(self.diplomes_ids) + df_diplome.to_excel(writer, onglet, index=True, header=True) + if self.etudiants.abandons_ids: + onglet = "redoublants-réorientés" + df_abandon = self.etudiants.df_administratif( + self.etudiants.abandons_ids + ) + df_abandon.to_excel(writer, onglet, index=True, header=True) output.seek(0) self.add_file_to_zip( zipfile, - f"semestres_taggues_{self.diplome}.xlsx", + f"etudiants_{self.diplome}.xlsx", output.read(), path="details", ) - # Génère les trajectoires (combinaison de semestres suivis - # par un étudiant pour atteindre le semestre final d'un aggrégat) - pe_affichage.pe_print( - "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" - ) - self.trajectoires = TrajectoiresJuryPE(self.diplome) - self.trajectoires.cree_trajectoires(self.etudiants) + if not self.diplomes_ids: + pe_affichage.pe_tools("*** Aucun étudiant diplômé") + else: + # Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE + pe_affichage.pe_print("*** Génère les semestres taggués") + self.semestres_taggues = compute_semestres_tag(self.etudiants) - # Génère les moyennes par tags des trajectoires - pe_affichage.pe_print( - "*** Calcule les moyennes par tag des trajectoires possibles" - ) - self.trajectoires_tagguees = compute_trajectoires_tag( - self.trajectoires, self.etudiants, self.semestres_taggues - ) - - # Intègre le bilan des trajectoires tagguées au zip final - output = io.BytesIO() - with pd.ExcelWriter(output, engine="openpyxl") as writer: - for trajectoire_tagguee in self.trajectoires_tagguees.values(): - onglet = trajectoire_tagguee.get_repr() - df = trajectoire_tagguee.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) - output.seek(0) - - self.add_file_to_zip( - zipfile, - f"trajectoires_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) - - # Génère les interclassements (par promo et) par (nom d') aggrégat - pe_affichage.pe_print("*** Génère les interclassements par aggrégat") - self.interclassements_taggues = compute_interclassements( - self.etudiants, self.trajectoires, self.trajectoires_tagguees - ) - - # Intègre le bilan des aggrégats (interclassé par promo) au zip final - output = io.BytesIO() - with pd.ExcelWriter(output, engine="openpyxl") as writer: - for interclass_tag in self.interclassements_taggues.values(): - if interclass_tag.significatif: # Avec des notes - onglet = interclass_tag.get_repr() - df = interclass_tag.df_moyennes_et_classements() + # Intègre le bilan des semestres taggués au zip final + output = io.BytesIO() + with pd.ExcelWriter(output, engine="openpyxl") as writer: + for formsemestretag in self.semestres_taggues.values(): + onglet = formsemestretag.nom + df = formsemestretag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) - output.seek(0) + output.seek(0) - self.add_file_to_zip( - zipfile, - f"interclassements_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) + self.add_file_to_zip( + zipfile, + f"semestres_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) - # Synthèse des éléments du jury PE - self.synthese = self.synthetise_juryPE() + # Génère les trajectoires (combinaison de semestres suivis + # par un étudiant pour atteindre le semestre final d'un aggrégat) + pe_affichage.pe_print( + "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" + ) + self.trajectoires = TrajectoiresJuryPE(self.diplome) + self.trajectoires.cree_trajectoires(self.etudiants) - # Export des données => mode 1 seule feuille -> supprimé - pe_affichage.pe_print("*** Export du jury de synthese") - output = io.BytesIO() + # Génère les moyennes par tags des trajectoires + pe_affichage.pe_print( + "*** Calcule les moyennes par tag des trajectoires possibles" + ) + self.trajectoires_tagguees = compute_trajectoires_tag( + self.trajectoires, self.etudiants, self.semestres_taggues + ) - with pd.ExcelWriter(output, engine="openpyxl") as writer: - for onglet, df in self.synthese.items(): - # écriture dans l'onglet: - df.to_excel(writer, onglet, index=True, header=True) - output.seek(0) + # Intègre le bilan des trajectoires tagguées au zip final + output = io.BytesIO() + with pd.ExcelWriter(output, engine="openpyxl") as writer: + for trajectoire_tagguee in self.trajectoires_tagguees.values(): + onglet = trajectoire_tagguee.get_repr() + df = trajectoire_tagguee.df_moyennes_et_classements() + # é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}.xlsx", output.read() - ) + self.add_file_to_zip( + zipfile, + f"trajectoires_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) + + # Génère les interclassements (par promo et) par (nom d') aggrégat + pe_affichage.pe_print("*** Génère les interclassements par aggrégat") + self.interclassements_taggues = compute_interclassements( + self.etudiants, self.trajectoires, self.trajectoires_tagguees + ) + + # Intègre le bilan des aggrégats (interclassé par promo) au zip final + output = io.BytesIO() + with pd.ExcelWriter(output, engine="openpyxl") as writer: + for interclass_tag in self.interclassements_taggues.values(): + if interclass_tag.significatif: # Avec des notes + onglet = interclass_tag.get_repr() + df = interclass_tag.df_moyennes_et_classements() + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) + output.seek(0) + + self.add_file_to_zip( + zipfile, + f"interclassements_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) + + # Synthèse des éléments du jury PE + self.synthese = self.synthetise_juryPE() + + # Export des données => mode 1 seule feuille -> supprimé + pe_affichage.pe_print("*** Export du jury de synthese") + output = io.BytesIO() + + with pd.ExcelWriter(output, engine="openpyxl") as writer: + for onglet, df in self.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}.xlsx", output.read() + ) # Fin !!!! Tada :) @@ -251,7 +276,7 @@ class JuryPE(object): synthese = {} pe_affichage.pe_print(" -> Synthèse des données administratives") - synthese["administratif"] = self.df_administratif() + synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) tags = self.do_tags_list(self.interclassements_taggues) for tag in tags: @@ -259,46 +284,6 @@ class JuryPE(object): synthese[tag] = self.df_tag(tag) return synthese - def df_administratif(self): - """Synthétise toutes les données administratives des étudiants""" - - etudids = list(self.diplomes_ids) - - # Récupération des données des étudiants - administratif = {} - nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes() - - for etudid in etudids: - etudiant = self.etudiants.identites[etudid] - cursus = self.etudiants.cursus[etudid] - formsemestres = cursus["formsemestres"] - - if cursus["diplome"]: - diplome = cursus["diplome"] - else: - diplome = "indéterminé" - - administratif[etudid] = { - "Nom": etudiant.nom, - "Prenom": etudiant.prenom, - "Civilite": etudiant.civilite_str, - "Age": pe_comp.calcul_age(etudiant.date_naissance), - "Date d'entree": cursus["entree"], - "Date de diplome": diplome, - "Nbre de semestres": len(formsemestres), - } - - # Ajout des noms de semestres parcourus - etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max) - administratif[etudid] |= etapes - - # Construction du dataframe - df = pd.DataFrame.from_dict(administratif, orient="index") - - # Tri par nom/prénom - df.sort_values(by=["Nom", "Prenom"], inplace=True) - return df - def df_tag(self, tag): """Génère le DataFrame synthétisant les moyennes/classements (groupe, interclassement promo) pour tous les aggrégats prévus, @@ -368,17 +353,6 @@ class JuryPE(object): df.sort_values(by=["Nom", "Prenom"], inplace=True) return df - def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury - """Table(s) du jury - mode: singlesheet ou multiplesheet pour export excel - """ - sT = SeqGenTable() # le fichier excel à générer - - if mode == "singlesheet": - return sT.get_genTable("singlesheet") - else: - return sT - def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.