# -*- 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 # ############################################################################## ############################################################################## # Module "Avis de poursuite d'étude" # conçu et développé par Cléo Baras (IUT de Grenoble) ############################################################################## """ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ # ---------------------------------------------------------- # Ensemble des fonctions et des classes # permettant les calculs preliminaires (hors affichage) # a l'edition d'un jury de poursuites d'etudes # ---------------------------------------------------------- import io import os import time from zipfile import ZipFile import numpy as np import pandas as pd from app.pe.rcss import pe_rcs from app.pe.moys import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant from app.pe.moys import ( pe_tabletags, pe_ressemtag, pe_sxtag, pe_rcstag, pe_interclasstag, pe_moytag, ) import app.pe.pe_rcss_jury as pe_rcss_jury class JuryPE(object): """ Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base d'une année de diplôme. De ce semestre est déduit : 1. l'année d'obtention du DUT, 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. Args: diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) """ def __init__(self, diplome: int): pe_affichage.pe_start_log() self.diplome = diplome "L'année du diplome" self.nom_export_zip = f"Jury_PE_{self.diplome}" "Nom du zip où ranger les fichiers générés" pe_affichage.pe_print( f"Données de poursuite d'étude générées le {time.strftime('%d/%m/%Y à %H:%M')}\n" ) # Chargement des étudiants à prendre en compte dans le jury pe_affichage.pe_print( f"""*** Recherche des étudiants diplômés 🎓 en {self.diplome}""" ) self.etudiants = pe_etudiant.EtudiantsJuryPE( self.diplome ) # Les infos sur les étudiants self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome) """Les informations sur les regroupements de semestres""" self.zipdata = io.BytesIO() with ZipFile(self.zipdata, "w") as zipfile: if not self.diplomes_ids: pe_affichage.pe_print("*** Aucun étudiant diplômé") else: try: self._gen_xls_diplomes(zipfile) self._gen_xls_ressembuttags(zipfile) self._gen_trajectoires() self._gen_semXs() self._gen_xls_sxtags(zipfile) self._gen_rcsemxs() self._gen_xls_rcstags(zipfile) self._gen_xls_interclasstags(zipfile) self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) except Exception as e: raise e # et le log self._add_log_to_zip(zipfile) # Fin !!!! Tada :) def _gen_xls_diplomes(self, zipfile: ZipFile): "Intègre le bilan des semestres taggués au zip" output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: 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"etudiants_{self.diplome}.xlsx", output.read(), path="details", ) def _gen_xls_ressembuttags(self, zipfile: ZipFile): """Calcule les moyennes par tag des résultats des Semestres BUT""" pe_affichage.pe_print( "*** Génère les ResSemBUTTag (résultats des semestres BUT taggués)" ) formsemestres = get_formsemestres_etudiants(self.etudiants) pe_affichage.pe_print( f"--> {len(formsemestres)} résultats de semestres à considérer" ) self.ressembuttags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes self.ressembuttags[frmsem_id] = pe_ressemtag.ResSemBUTTag(formsemestre) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: onglets = [] for res_sem_tag in self.ressembuttags.values(): onglet = res_sem_tag.get_repr(verbose=True) onglets += [] df = res_sem_tag.to_df() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( zipfile, f"ResSemBUTTags_{self.diplome}.xlsx", output.read(), path="details", ) def _gen_trajectoires(self): """Génère l'ensemble des trajectoires (RCS), qui traduisent les différents chemins au sein des (form)semestres pour atteindre la cible d'un RCS (par ex: 'S2' ou '3S'). """ pe_affichage.pe_print( "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" ) self.rcss_jury.cree_trajectoires(self.etudiants) def _gen_semXs(self): """Génère les SemXs (trajectoires/combinaisons de semestre de même rang x) qui traduisent les différents chemins des étudiants pour valider un semestre Sx. """ # Génère les regroupements de semestres de type Sx pe_affichage.pe_print( "*** Génère les SemXs (RCS de même Sx donnant lieu à validation du semestre)" ) self.rcss_jury.cree_semxs(self.etudiants) self.rcss_jury._aff_semxs_suivis(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCF de type Sx (pour identifier les redoublements impactant les semestres taggués). """ # Génère les moyennes des RCS de type Sx pe_affichage.pe_print( "*** Calcule les moyennes des SxTag (moyennes d'un SemX/RCS de type Sx)" ) # Les SxTag (moyenne de Sx par UE) self.sxtags = {} for rcf_id, rcf in self.rcss_jury.semXs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: onglets = [] for sxtag in self.sxtags.values(): onglet = sxtag.get_repr(verbose=False) if sxtag.is_significatif(): df = sxtag.to_df() onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) if onglets: self.add_file_to_zip( zipfile, f"semestres_taggues_{self.diplome}.xlsx", output.read(), path="details", ) def _gen_rcsemxs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" pe_affichage.pe_print( "*** Génère les RCSemX (regroupements cohérents de données" " extraites des SemX) amenant du S1 à un semestre final***" ) self.rcss_jury.cree_rcsemxs(self.etudiants) self.rcss_jury._aff_rcsemxs_suivis(self.etudiants) def _gen_xls_rcstags(self, zipfile: ZipFile): """Génère les RCS taggués traduisant les moyennes (orientées compétences) de regroupements de semestre de type Sx, xA ou xS. Stocke le résultat dans self.rccs_tag, un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: RCSTag(fid_terminal)} }`` Pour rappel : Chaque RCS est identifié par un nom d'aggrégat et par un formsemestre terminal. Par exemple : * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison. Args: etudiants: Les données des étudiants semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) """ # Génère les moyennes des RCS de type Sx pe_affichage.pe_print("*** Calcule les moyennes des RC de RCFS") self.rcss_tags = {} for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items(): self.rcss_tags[rcs_id] = pe_rcstag.RCSTag(rcsemx, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: onglets = [] for rcs_tag in self.rcss_tags.values(): onglet = rcs_tag.get_repr(verbose=False) if rcs_tag.is_significatif(): df = rcs_tag.to_df() onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) if onglets: self.add_file_to_zip( zipfile, f"RCRCFs_{self.diplome}.xlsx", output.read(), path="details", ) def _gen_xls_interclasstags(self, zipfile: ZipFile): """Génère les interclassements sur la promo de diplômés par (nom d') aggrégat en distinguant les interclassements par accronymes d'UEs (sur les SxTag) et ceux par compétences (sur les RCSTag). """ pe_affichage.pe_print( "*** Génère les interclassements sur chaque type de RCS/agrgégat" ) self.interclasstags = { pe_moytag.CODE_MOY_UE: {}, pe_moytag.CODE_MOY_COMPETENCES: {}, } etudiants_diplomes = self.etudiants.etudiants_diplomes # Les interclassements par UE for Sx in pe_rcs.TOUS_LES_SEMESTRES: pe_affichage.pe_print( f"--> Interclassement par (acronyme d')UEs pour le RCS {Sx}" ) interclass = pe_interclasstag.InterClassTag( Sx, pe_moytag.CODE_MOY_UE, etudiants_diplomes, self.rcss_jury.semXs, self.sxtags, self.rcss_jury.semXs_suivis, ) self.interclasstags[pe_moytag.CODE_MOY_UE][Sx] = interclass # Les interclassements par compétences for nom_rcs in pe_rcs.TOUS_LES_RCS: pe_affichage.pe_print( f"--> Interclassement par compétences pour le RCS {nom_rcs}" ) interclass = pe_interclasstag.InterClassTag( nom_rcs, pe_moytag.CODE_MOY_COMPETENCES, etudiants_diplomes, self.rcss_jury.rcsemxs, self.rcss_tags, self.rcss_jury.rcsemxs_suivis, ) self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][nom_rcs] = interclass # Intègre le bilan des aggrégats (interclassé par promo) au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: onglets = [] for type_interclass in [ pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES, ]: interclasstag = self.interclasstags[type_interclass] for nom_rcs, interclass in interclasstag.items(): onglet = interclass.get_repr() if interclass.is_significatif(): df = interclass.to_df(cohorte="Promo") onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) if onglets: self.add_file_to_zip( zipfile, f"InterClassTags_{self.diplome}.xlsx", output.read(), path="details", ) 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_jury_par_tags() # Export des données => mode 1 seule feuille -> supprimé 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" ) as writer: onglets = [] for (tag, type_moy), df in self.synthese.items(): onglet = f"{tag} {type_moy}" onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( 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: onglets = [] for onglet, df in synthese.items(): onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read() ) def _add_log_to_zip(self, zipfile): """Add a text file with the log messages""" log_data = pe_affichage.pe_get_log() self.add_file_to_zip(zipfile, "pe_log.txt", log_data) def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""): """Add a file to given zip All files under NOM_EXPORT_ZIP/ path may specify a subdirectory Args: zipfile: ZipFile filename: Le nom du fichier à intégrer au zip data: Les données du fichier path: Un dossier dans l'arborescence du zip """ path_in_zip = os.path.join(path, filename) # self.nom_export_zip, zipfile.writestr(path_in_zip, data) def get_zipped_data(self) -> io.BytesIO | None: """returns file-like data with a zip of all generated (CSV) files. Warning: reset stream to the begining. """ self.zipdata.seek(0) return self.zipdata def _do_tags_list(self, interclassements: dict[str, dict]): """La liste des tags extraites des interclassements""" tags = [] # Pour chaque type d'interclassements for type in interclassements: interclassement = interclassements[type] for aggregat in interclassement: interclass = interclassement[aggregat] if interclass.tags_sorted: tags.extend(interclass.tags_sorted) tags = sorted(set(tags)) return tags # **************************************************************************************************************** # # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** 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 et des types de calcul de moyennes (par UEs ou par compétences)""" pe_affichage.pe_print( "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***" ) synthese = {} pe_affichage.pe_print(" -> Synthèse des données administratives") synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) tags = self._do_tags_list(self.interclasstags) for tag in tags: for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy}") synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) return synthese def df_tag_type(self, tag, type_moy, type_colonnes=True): """Génère le DataFrame synthétisant les moyennes/classements (groupe + interclassement promo) pour tous les aggrégats prévus, en fonction du type (UEs ou Compétences) de données souhaitées, tels que fourni dans l'excel final. Si type=UEs => tous les sxtag du tag Si type=Compétences => tous les rcstag du tag Args: tag: Un des tags (a minima `but`) type_moy: Un type de moyenne Returns: """ # Les données des étudiants etuds = [etud for etudid, etud in self.etudiants.etudiants_diplomes.items()] df = pe_tabletags.df_administratif(etuds, aggregat="", cohorte="") if type_moy == pe_moytag.CODE_MOY_UE: aggregats = pe_rcs.TOUS_LES_SEMESTRES else: aggregats = pe_rcs.TOUS_LES_RCS for aggregat in aggregats: print(aggregat) # Descr de l'aggrégat descr = pe_rcs.TYPES_RCS[aggregat]["descr"] # L'interclassement associé interclass = self.interclasstags[type_moy][aggregat] if interclass.is_significatif(): # Le dataframe du classement sur le groupe df_groupe = interclass.compute_df_synthese_moyennes_tag( tag, aggregat=aggregat, type_colonnes=False ) df = df.join(df_groupe) # Le dataframe du classement sur la promo df_promo = interclass.to_df( administratif=False, aggregat=aggregat, tags_cibles=[tag], cohorte="Promo", type_colonnes=False, ) df = df.join(df_promo) # Conversion des colonnes en multiindex if type_colonnes: df.columns = pd.MultiIndex.from_tuples(df.columns) return df # Fin de l'aggrégat 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.interclasstags) donnees = {} for tag in tags: # Une ligne pour le tag donnees[tag] = {("", "", "tag"): tag} for aggregat in pe_rcs.TOUS_LES_RCS: # 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.rcss_jury.trajectoires_suivies[etudid][aggregat] if trajectoire: trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: # L'interclassement interclass = self.interclasstags[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 get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: """Ayant connaissance des étudiants dont il faut calculer les moyennes pour le jury PE (attribut `self.etudiant_ids) et de leurs trajectoires (semestres parcourus), renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer la moyenne. Args: etudiants: Les étudiants du jury PE Returns: Un dictionnaire de la forme `{fid: FormSemestre(fid)}` """ semestres = {} for etudid in etudiants.etudiants_ids: for cle in etudiants.cursus[etudid]: if cle.startswith("S"): semestres = semestres | etudiants.cursus[etudid][cle] return semestres def get_defaut_dict_synthese_aggregat(nom_rcs: 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 Args: nom_rcs : Le nom du RCS visé diplôme : l'année du diplôme """ # L'affichage de l'aggrégat dans le tableur excel descr = pe_rcs.get_descr_rcs(nom_rcs) 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: pe_rcstag.RCSTag, interclassement_taggue: pe_interclasstag.InterClassTag, 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_rcs.get_descr_rcs(aggregat) # La note de l'étudiant (chargement à venir) note = np.nan # Les données de la trajectoire tagguée pour le tag considéré moy_tag = trajectoire_tagguee.moyennes_tags[tag] # Les données de l'étudiant note = moy_tag.get_note_for_df(etudid) classement = moy_tag.get_class_for_df(etudid) nmin = moy_tag.get_min_for_df() nmax = moy_tag.get_max_for_df() nmoy = moy_tag.get_moy_for_df() # Statistiques sur le groupe if not pd.isna(note) and note != np.nan: # Les moyennes de cette trajectoire donnees |= { (descr, "", "note"): note, (descr, NOM_STAT_GROUPE, "class."): classement, (descr, NOM_STAT_GROUPE, "min"): nmin, (descr, NOM_STAT_GROUPE, "moy"): nmoy, (descr, NOM_STAT_GROUPE, "max"): nmax, } # L'interclassement moy_tag = interclassement_taggue.moyennes_tags[tag] classement = moy_tag.get_class_for_df(etudid) nmin = moy_tag.get_min_for_df() nmax = moy_tag.get_max_for_df() nmoy = moy_tag.get_moy_for_df() if not pd.isna(note) and note != np.nan: nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" donnees |= { (descr, nom_stat_promo, "class."): classement, (descr, nom_stat_promo, "min"): nmin, (descr, nom_stat_promo, "moy"): nmoy, (descr, nom_stat_promo, "max"): nmax, } return donnees