From 3e55391f7eb66fe7ae5b020cb479ad1639bea2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 20 Jan 2024 16:34:38 +0100 Subject: [PATCH] Recode SemestreTag --- app/pe/pe_etudiant.py | 177 ++++++-- app/pe/pe_jurype.py | 836 ++++++----------------------------- app/pe/pe_semestretag.py | 373 ++++++++-------- app/pe/pe_tagtable.py | 104 ++--- app/scodoc/sco_tag_module.py | 31 +- 5 files changed, 533 insertions(+), 988 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 7c5581846..45c5bc238 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -98,7 +98,7 @@ class EtudiantsJuryPE: pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") no_etud = 0 - for (no_etud, etudid) in enumerate(self.etudiants_ids): + for no_etud, etudid in enumerate(self.etudiants_ids): self.add_etudid(etudid, cosemestres) if (no_etud + 1) % 10 == 0: pe_tools.pe_print((no_etud + 1), " ", end="") @@ -115,15 +115,27 @@ class EtudiantsJuryPE: self.formsemestres_jury_ids = self.get_formsemestres_jury() # Synthèse - pe_tools.pe_print(f" => {len(self.etudiants_jury_ids)} étudiants à diplômer en {annee_diplome}") + pe_tools.pe_print( + f" => {len(self.etudiants_jury_ids)} étudiants à diplômer en {annee_diplome}" + ) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_ids) pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") - pe_tools.pe_print(f" => quelques étudiants futurs diplômés : " + ", ".join([str(etudid) for etudid in list(self.etudiants_jury_ids)[:10]])) + pe_tools.pe_print( + f" => quelques étudiants futurs diplômés : " + + ", ".join([str(etudid) for etudid in list(self.etudiants_jury_ids)[:10]]) + ) + pe_tools.pe_print( + f" => semestres dont il faut calculer les moyennes : " + + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) + ) - def get_etudids(self, annee_diplome: int, ordre="aucun") -> list: + def get_etudids(self, annee_diplome: int = None, ordre="aucun") -> list: """Liste des etudid des étudiants qui vont être à traiter au jury PE pour l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné. + Si l'année de diplômation n'est pas précisée (None), inclus les étudiants réorientés + ou ayant abandonné. + Si l'``ordre`` est précisé, trie la liste par ordre alphabétique de etat_civil Args: @@ -135,11 +147,17 @@ class EtudiantsJuryPE: Note: ex JuryPE.get_etudids_du_jury() """ - etudids = [ - etudid - for (etudid, donnees) in self.cursus.items() - if donnees["diplome"] == annee_diplome and not donnees["abandon"] - ] + if annee_diplome: + etudids = [ + etudid + for (etudid, donnees) in self.cursus.items() + if donnees["diplome"] == annee_diplome and not donnees["abandon"] + ] + else: + etudids = [ + etudid + for (etudid, donnees) in self.cursus.items() + ] if ordre == "alphabetique": # Tri alphabétique etudidsAvecNom = [ (etudid, etud["etat_civil"]) @@ -150,6 +168,24 @@ class EtudiantsJuryPE: etudids = [etud[0] for etud in etudidsAvecNomTrie] return etudids + def get_etudiants(self, annee_diplome: int = None) -> dict[Identite]: + """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` + qui vont être à traiter au jury PE pour + l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné. + + Si l'année de diplômation n'est pas précisée (None), inclus les étudiants réorientés + ou ayant abandonné. + + Args: + annee_diplome: Année de diplomation visée pour le jury + + Returns: + Un dictionnaire `{etudid: Identite(etudid)}` + """ + etudids = self.get_etudids(annee_diplome=annee_diplome) + etudiants = {etudid: self.identites[etudids] for etudid in etudids} + return etudiants + def add_etudid(self, etudid: int, cosemestres): """Ajoute un étudiant à ceux qui devront être traités pendant le jury pouvant être : @@ -173,35 +209,37 @@ class EtudiantsJuryPE: Note: ex JuryPE.add_etudid_to_jury() """ - """L'identité de l'étudiant""" identite = Identite.get_etud(etudid) self.identites[etudid] = identite - """Le cursus global de l'étudiant""" + """Le cursus global de l'étudiant (restreint aux semestres APC)""" semestres_etudiant = { - frmsem.formsemestre_id: frmsem for frmsem in identite.get_formsemestres() - } + frmsem.formsemestre_id: frmsem + for frmsem in identite.get_formsemestres() + if frmsem.formation.is_apc() + } self.cursus[etudid] = { "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury "diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme - "formsemestres": semestres_etudiant # les semestres de l'étudiant + "formsemestres": semestres_etudiant, # les semestres de l'étudiant } """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" - self.cursus[etudid]["abandon"] = arret_de_formation( - identite, cosemestres - ) + self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) """Tri des semestres par n° de semestre""" for nom_sem in pe_tools.TOUS_LES_SEMESTRES: - numero_sem = int(nom_sem[1]) + 1 - self.cursus[etudid][nom_sem] = {fid: semestres_etudiant[fid] - for fid in semestres_etudiant - if semestres_etudiant[fid].semestre_id == numero_sem} - + i = int(nom_sem[1]) + 1 # le n° du semestre + semestres_i = { + fid: semestres_etudiant[fid] + for fid in semestres_etudiant + if semestres_etudiant[fid].semestre_id == i + } # les semestres de n°i de l'étudiant + dernier_semestre_i = get_dernier_semestre(semestres_i) + self.cursus[etudid][nom_sem] = dernier_semestre_i """Tri des semestres par aggrégat""" for parcours in pe_tools.TOUS_LES_AGGREGATS: @@ -210,7 +248,9 @@ class EtudiantsJuryPE: self.cursus[etudid][parcours] = {} for nom_sem in noms_semestre_de_aggregat: - self.cursus[etudid][parcours] = self.cursus[etudid][parcours] | self.cursus[etudid][nom_sem] + self.cursus[etudid][parcours] = ( + self.cursus[etudid][parcours] | self.cursus[etudid][nom_sem] + ) if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: pe_tools.pe_print( @@ -218,20 +258,64 @@ class EtudiantsJuryPE: end="", ) - - def get_formsemestres_jury(self): + def get_formsemestres_jury(self, semestres_recherches=None): """Ayant connaissance des étudiants dont il faut calculer les moyennes pour - le jury PE (attribut `self.etudiant_ids), renvoie l'ensemble des formsemestres - de leur cursus, dont il faudra calculer la moyenne. + le jury PE (attribut `self.etudiant_ids) et de leur cursus, + renvoie un dictionnaire `{fid: FormSemestre(fid)}` + contenant l'ensemble des formsemestres de leur cursus, dont il faudra calculer + la moyenne. Les formsemestres sont limités à ceux indiqués dans ``semestres_recherches``. + + Args: + semestres_recherches: Une liste ou une chaine de caractères parmi : + + * None : pour obtenir tous les formsemestres du jury + * 'Si' : pour obtenir les semestres de n° i (par ex. 'S1') + * 'iA' : pour obtenir les semestres de l'année i (par ex. '1A' donne ['S1, 'S2']) + * '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats Returns: - Un ensemble de formsemestres + Un dictionnaire de la forme {fid: FormSemestre(fid)} + + Remarque: + Une liste de la forme `[ 'Si', 'iA' , ... ]` (combinant les formats précédents) est possible. """ - formsemestres = {} - for etudid in self.etudiants_ids: - formsem_etudid = set(self.cursus[etudid].keys()) - formsemestres = formsemestres | formsem_etudid - return formsemestres + if semestres_recherches is None: + """Appel récursif pour obtenir tous les semestres (validants)""" + semestres = self.get_formsemestres_jury(pe_tools.AGGREGAT_DIPLOMANT) + return semestres + elif isinstance(semestres_recherches, list): + """Appel récursif sur tous les éléments de la liste""" + semestres = {} + for elmt in semestres_recherches: + semestres_elmt = self.get_formsemestres_jury(elmt) + semestres = semestres | semestres_elmt + return semestres + elif ( + isinstance(semestres_recherches, str) + and semestres_recherches in pe_tools.TOUS_LES_AGGREGATS + ): + """Cas d'un aggrégat avec appel récursif sur toutes les entrées de l'aggrégat""" + semestres = self.get_formsemestres_jury( + pe_tools.PARCOURS[semestres_recherches]["aggregat"] + ) + return semestres + elif ( + isinstance(semestres_recherches, str) + and semestres_recherches in pe_tools.TOUS_LES_SEMESTRES + ): + """semestres_recherches est un nom de semestre de type S1, + pour une recherche parmi les étudiants à prendre en compte + dans le jury (diplômé et redoublants non diplômé) + """ + nom_sem = semestres_recherches + semestres = {} + for etudid in self.etudiants_ids: + semestres = semestres | self.cursus[etudid][nom_sem] + return semestres + else: + raise ValueError( + "Probleme de paramètres d'appel dans get_formsemestreids_du_jury" + ) def get_etudiants_dans_semestres(semestres: dict[FormSemestre]) -> set: @@ -249,7 +333,7 @@ def get_etudiants_dans_semestres(semestres: dict[FormSemestre]) -> set: """ etudiants_ids = set() - for (fid, sem) in semestres.items(): # pour chacun des semestres de la liste + for fid, sem in semestres.items(): # pour chacun des semestres de la liste etudiants_du_sem = {ins.etudid for ins in sem.inscriptions} pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants") @@ -361,3 +445,28 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b return True return False + + +def get_dernier_semestre(semestres: dict[FormSemestre]): + """Renvoie le dernier semestre en date d'un dictionnaire + de semestres de la forme {fid: FormSemestre(fid) + + Args: + semestres: Un dictionnaire de semestres + + Return: + Un dictionnaire {fid: FormSemestre(fid)} contenant le semestre le plus récent + """ + if semestres: + fid_dernier_semestre = list(semestres.keys())[0] + dernier_semestre = {fid_dernier_semestre: semestres[fid_dernier_semestre]} + for fid in semestres: + if ( + semestres[fid].date_fin + > dernier_semestre[fid_dernier_semestre].date_fin + ): + dernier_semestre = {fid: semestres[fid]} + fid_dernier_semestre = fid + return dernier_semestre + else: + return {} diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index d79392886..dad54978b 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -35,6 +35,7 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ +import datetime # ---------------------------------------------------------- # Ensemble des fonctions et des classes @@ -46,19 +47,27 @@ import io import os from zipfile import ZipFile +import app.pe.pe_etudiant from app.comp import res_sem from app.comp.res_compat import NotesTableCompat +from app.comp.res_sem import load_formsemestre_results from app.models import Formation, FormSemestre +from app.models.etudiants import Identite from app.scodoc.gen_tables import GenTable, SeqGenTable import app.scodoc.sco_utils as scu -from app.scodoc import codes_cursus # codes_cursus.NEXT -> sem suivant +from app.scodoc import ( + codes_cursus, + sco_formsemestre_inscriptions, +) # codes_cursus.NEXT -> sem suivant from app.scodoc import sco_etud +from app.scodoc import sco_report from app.scodoc import sco_formsemestre from app.pe import pe_tagtable from app.pe import pe_tools from app.pe import pe_semestretag from app.pe import pe_settag +from app.pe.pe_etudiant import EtudiantsJuryPE # ---------------------------------------------------------------------------------------- @@ -83,7 +92,7 @@ class JuryPE(object): Attributs : - * diplome : l'annee d'obtention du diplome BUT et du jury de PE (generalement fevrier XXXX) + * diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) * juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives + celles des semestres valides à prendre en compte permettant le calcul des moyennes ... ``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}`` @@ -94,98 +103,9 @@ class JuryPE(object): # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et # leur affichage dans les avis latex - NBRE_SEMESTRES_PARCOURS = 6 - - PARCOURS = { - "S1": { - "aggregat": ["S1"], - "ordre": 1, - "affichage_court": "S1", - "affichage_long": "Semestre 1", - }, - "S2": { - "aggregat": ["S2"], - "ordre": 2, - "affichage_court": "S2", - "affichage_long": "Semestre 2", - }, - "1A": { - "aggregat": ["S1", "S2"], - "ordre": 3, - "affichage_court": "1A", - "affichage_long": "1ère année", - }, - "S3": { - "aggregat": ["S3"], - "ordre": 4, - "affichage_court": "S3", - "affichage_long": "Semestre 3", - }, - "S4": { - "aggregat": ["S4"], - "ordre": 5, - "affichage_court": "S4", - "affichage_long": "Semestre 4", - }, - "2A": { - "aggregat": ["S3", "S4"], - "ordre": 6, - "affichage_court": "2A", - "affichage_long": "2ème année", - }, - "3S": { - "aggregat": ["S1", "S2", "S3"], - "ordre": 7, - "affichage_court": "S1+S2+S3", - "affichage_long": "BUT du semestre 1 au semestre 3", - }, - "4S": { - "aggregat": ["S1", "S2", "S3", "S4"], - "ordre": 8, - "affichage_court": "BUT", - "affichage_long": "BUT du semestre 1 au semestre 4", - }, - "S5": { - "aggregat": ["S5"], - "ordre": 9, - "affichage_court": "S5", - "affichage_long": "Semestre 5", - }, - "S6": { - "aggregat": ["S6"], - "ordre": 10, - "affichage_court": "S6", - "affichage_long": "Semestre 6", - }, - "3A": { - "aggregat": ["S5", "S6"], - "ordre": 11, - "affichage_court": "3A", - "affichage_long": "3ème année", - }, - "5S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5"], - "ordre": 12, - "affichage_court": "S1+S2+S3+S4+S5", - "affichage_long": "BUT du semestre 1 au semestre 5", - }, - "6S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], - "ordre": 13, - "affichage_court": "BUT", - "affichage_long": "BUT (tout semestre inclus)", - }, - } - - AGGREGAT_DIPLOMANT = ( - "6S" # aggrégat correspondant à la totalité des notes pour le diplôme - ) - TOUS_LES_SEMESTRES = PARCOURS["6S"]["aggregat"] - TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] - TOUS_LES_PARCOURS = list(PARCOURS.keys()) # ------------------------------------------------------------------------------------------------------------------ - def __init__(self, sem_base: FormSemestre, semBase): # CB: à supprimer à long terme + def __init__(self, diplome, formation_id): """ Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit : 1. l'année d'obtention du DUT, @@ -197,6 +117,7 @@ class JuryPE(object): meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, si False, permet des programmes differents """ + self.semTagDict = ( {} ) # Les semestres taggués à la base des calculs de moyenne par tag @@ -205,24 +126,33 @@ class JuryPE(object): ) # dictionnaire récapitulant les semTag impliqués dans le jury de la forme { 'formsemestre_id' : object Semestre_tag self.promoTagDict = {} - # L'année du diplome - self.diplome = get_annee_diplome_semestre(semBase) + "L'année du diplome" + self.diplome = diplome - # Un zip où ranger les fichiers générés: - self.NOM_EXPORT_ZIP = "Jury_PE_%s" % self.diplome + "La formation associée au diplome" + self.formation_id = formation_id + + "Un zip où ranger les fichiers générés" + self.nom_export_zip = "Jury_PE_%s" % self.diplome self.zipdata = io.BytesIO() self.zipfile = ZipFile(self.zipdata, "w") - # - self.ETUDINFO_DICT = {} # Les infos sur les étudiants - self.PARCOURSINFO_DICT = {} # Les parcours des étudiants + "Les informations sur les étudiants édités par le jury PE" + self.etudiants = EtudiantsJuryPE() # Les infos sur les étudiants self.syntheseJury = {} # Le jury de synthèse - self.semestresDeScoDoc = sco_formsemestre.do_formsemestre_list() + """Chargement des étudiants à prendre en compte dans le jury""" + pe_tools.pe_print( + f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}" + ) + self.etudiants.find_etudiants(self.diplome, self.formation_id) - # Calcul du jury PE - self.exe_calculs_juryPE(semBase) - self.synthetise_juryPE() + """Calcul des moyennes pour le jury PE""" + self.exe_calculs_juryPE() + + """Synthèse des éléments du jury PE""" + if False: + self.synthetise_juryPE() # Export des données => mode 1 seule feuille -> supprimé # filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls' @@ -230,10 +160,11 @@ class JuryPE(object): # self.add_file_to_zip(filename, self.xls.excel()) # Fabrique 1 fichier excel résultat avec 1 seule feuille => trop gros - filename = self.NOM_EXPORT_ZIP + "_jurySyntheseDict" + scu.XLSX_SUFFIX - self.xlsV2 = self.table_syntheseJury(mode="multiplesheet") - if self.xlsV2: - self.add_file_to_zip(filename, self.xlsV2.excel()) + if False: + filename = self.nom_export_zip + "_jurySyntheseDict" + scu.XLSX_SUFFIX + self.xlsV2 = self.table_syntheseJury(mode="multiplesheet") + if self.xlsV2: + self.add_file_to_zip(filename, self.xlsV2.excel()) # Pour debug # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug @@ -244,7 +175,7 @@ class JuryPE(object): All files under NOM_EXPORT_ZIP/ path may specify a subdirectory """ - path_in_zip = os.path.join(self.NOM_EXPORT_ZIP, path, filename) + path_in_zip = os.path.join(self.nom_export_zip, path, filename) self.zipfile.writestr(path_in_zip, data) # ------------------------------------------------------------------------------------------------------------------ @@ -261,27 +192,31 @@ class JuryPE(object): # **************************************************************************************************************** # # Lancement des différentes actions permettant le calcul du jury PE # **************************************************************************************************************** # - def exe_calculs_juryPE(self, semBase): - # Liste des étudiants à traiter pour identifier ceux qui seront diplômés - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Recherche et chargement des étudiants diplômés en %d" - % (self.diplome) - ) - self.get_etudiants_in_jury( - semBase, avec_meme_formation=False - ) # calcul des coSemestres + def exe_calculs_juryPE(self): + """Centralise les élements de calcul des moyennes de poursuites + d'études + """ - # Les semestres impliqués (ceux valides pour les étudiants à traiter) - # ------------------------------------------------------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print("*** Création des semestres taggués") - self.get_semtags_in_jury() - if pe_tools.PE_DEBUG: - for semtag in self.semTagDict.values(): # Export - filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv" - self.zipfile.writestr(filename, semtag.str_tagtable()) - # self.export_juryPEDict() + """Création des semestres taggués, de type 'S1', 'S2', ...""" + pe_tools.pe_print("*** Création des semestres taggués") + + formsemestres = self.etudiants.get_formsemestres_jury( + semestres_recherches=pe_tools.TOUS_LES_SEMESTRES + ) + for frmsem_id, formsemestre in formsemestres.items(): + """Choix d'un nom pour le semestretag""" + nom = "S%d %d %d-%d" % ( + formsemestre.semestre_id, + formsemestre.formsemestre_id, + formsemestre.date_debut.year, + formsemestre.date_fin.year, + ) + + pe_tools.pe_print( + f" --> Semestre taggué {nom} sur la base de {formsemestre}" + ) + + self.add_semestretag_in_jury(nom, frmsem_id) # Les moyennes sur toute la scolarité # ----------------------------------- @@ -289,13 +224,16 @@ class JuryPE(object): pe_tools.pe_print( "*** Création des moyennes sur différentes combinaisons de semestres et différents groupes d'étudiant" ) - self.get_settags_in_jury() - if pe_tools.PE_DEBUG: - for settagdict in self.setTagDict.values(): # Export - for settag in settagdict.values(): - filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv" - self.zipfile.writestr(filename, semtag.str_tagtable()) - # self.export_juryPEDict() + if False: + self.get_settags_in_jury() + if pe_tools.PE_DEBUG: + for settagdict in self.setTagDict.values(): # Export + for settag in settagdict.values(): + filename = self.nom_export_zip + semtag.nom + ".csv" + self.add_file_to_zip( + filename, semtag.str_tagtable(), path="details_semestres" + ) + # self.export_juryPEDict() # Les interclassements # -------------------- @@ -303,426 +241,34 @@ class JuryPE(object): pe_tools.pe_print( "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" ) - self.get_promotags_in_jury() - - # **************************************************************************************************************** # - # Fonctions relatives à la liste des étudiants à prendre en compte dans le jury - # **************************************************************************************************************** # - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_in_jury(self, semBase, avec_meme_formation=False): - """ - Calcule la liste des étudiants à prendre en compte dans le jury et la renvoie sous la forme - """ - # Les cosemestres donnant lieu à meme année de diplome - coSems = get_cosemestres_diplomants( - semBase, avec_meme_formation=avec_meme_formation - ) # calcul des coSemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "1) Recherche des coSemestres -> %d trouvés" % len(coSems) - ) - - # Les étudiants inscrits dans les cosemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") - listEtudId = self.get_etudiants_dans_semestres( - coSems - ) # étudiants faisant parti des cosemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" => %d étudiants trouvés" % len(listEtudId)) - - # L'analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, .... - if pe_tools.PE_DEBUG: - pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") - - for no_etud, etudid in enumerate(listEtudId): - self.add_etudiants(etudid) - if pe_tools.PE_DEBUG: - if (no_etud + 1) % 10 == 0: - pe_tools.pe_print((no_etud + 1), " ", end="") - pe_tools.pe_print() - - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " => %d étudiants à diplômer en %d" - % (len(self.get_etudids_du_jury()), self.diplome) - ) - pe_tools.pe_print( - " => %d étudiants éliminer pour abandon" - % (len(listEtudId) - len(self.get_etudids_du_jury())) - ) - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_dans_semestres(self, semsListe): - """Renvoie la liste des etudid des etudiants inscrits à l'un des semestres de la liste fournie en paramètre - en supprimant les doublons (i.e. un même étudiant qui apparaîtra 2 fois)""" - - etudiants = [] - for sem in semsListe: # pour chacun des semestres de la liste - nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"]) - etudiantsDuSemestre = [ - ins.etudid for ins in nt.formsemestre.inscriptions - ] # nt.get_etudids() # identification des etudiants du semestre - - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " --> chargement du semestre %s : %d etudiants " - % (sem["formsemestre_id"], len(etudiantsDuSemestre)) - ) - etudiants.extend(etudiantsDuSemestre) - - return list(set(etudiants)) # suppression des doublons - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudids_du_jury(self, ordre="aucun"): - """Renvoie la liste de tous les étudiants (concrètement leur etudid) - participant au jury c'est-à-dire, ceux dont la date du 'jury' est self.diplome - et n'ayant pas abandonné. - Si l'ordre est précisé, donne une liste etudid dont le nom, prenom trié par ordre alphabétique - """ - etudids = [ - etudid - for (etudid, donnees) in self.PARCOURSINFO_DICT.items() - if donnees["diplome"] == self.diplome and donnees["abandon"] == False - ] - if ordre == "alphabetique": # Tri alphabétique - etudidsAvecNom = [ - (etudid, etud["nom"] + "/" + etud["prenom"]) - for (etudid, etud) in self.PARCOURSINFO_DICT.items() - if etudid in etudids - ] - etudidsAvecNomTrie = sorted(etudidsAvecNom, key=lambda col: col[1]) - etudids = [etud[0] for etud in etudidsAvecNomTrie] - return etudids - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def add_etudiants(self, etudid): - """Ajoute un étudiant connaissant son etudid au dictionnaire de synthèse jurydict. - L'ajout consiste à : - * insérer une entrée pour l'étudiant en mémorisant ses infos (get_etudInfo), - avec son nom, prénom, etc... - * à analyser son parcours, pour vérifier s'il n'a pas abandonné l'IUT en cours de - route (cf. clé abandon) - * à chercher ses semestres valides (formsemestre_id) et ses années valides (formannee_id), - c'est-à-dire ceux pour lesquels il faudra prendre en compte ses notes dans les calculs de - moyenne (type 1A=S1+S2/2) - - Args: - etudid: L'etudid d'un étudiant, à ajouter au jury s'il respecte les critères précédents - """ - - if etudid not in self.PARCOURSINFO_DICT: - etud = self.get_cache_etudInfo_d_un_etudiant( - etudid - ) # On charge les données de l'étudiant - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(etud["nom"] + " " + etud["prenom"], end="") - - self.PARCOURSINFO_DICT[etudid] = { - "etudid": etudid, # les infos sur l'étudiant - "nom": etud["nom"], # Ajout à la table jury - } - - # Analyse du parcours de l'étudiant - - # Sa date prévisionnelle de diplome - self.PARCOURSINFO_DICT[etudid][ - "diplome" - ] = self.calcul_anneePromoBUT_d_un_etudiant(etudid) - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print( - "promo=" + str(self.PARCOURSINFO_DICT[etudid]["diplome"]), end="" - ) - - # Est-il réorienté ou démissionnaire ? - self.PARCOURSINFO_DICT[etudid][ - "abandon" - ] = self.est_un_etudiant_reoriente_ou_demissionnaire(etudid) - - # A-t-il arrêté de lui-même sa formation avant la fin ? - etatD = self.est_un_etudiant_disparu(etudid) - if etatD == True: - self.PARCOURSINFO_DICT[etudid]["abandon"] = True - # dans le jury ne seront traités que les étudiants ayant la date attendue de diplome et n'ayant pas abandonné - - # Quels sont ses semestres validant (i.e ceux dont les notes doivent être prises en compte pour le jury) - # et s'ils existent quelles sont ses notes utiles ? - sesFormsemestre_idValidants = [ - self.get_Fid_d_un_Si_valide_d_un_etudiant(etudid, nom_sem) - for nom_sem in JuryPE.TOUS_LES_SEMESTRES - # Recherche du formsemestre_id de son Si valide (ou a défaut en cours) - ] - for i, nom_sem in enumerate(JuryPE.TOUS_LES_SEMESTRES): - fid = sesFormsemestre_idValidants[i] - self.PARCOURSINFO_DICT[etudid][nom_sem] = fid # ['formsemestre_id'] - if fid != None and pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(nom_sem + "=" + str(fid), end="") - # self.get_moyennesEtClassements_par_semestre_d_un_etudiant( etudid, fid ) - - # Quelles sont ses années validantes ('1A', '2A') et ses parcours (3S, 4S) validants ? - for parcours in JuryPE.TOUS_LES_AGGREGATS: - lesSemsDuParcours = JuryPE.PARCOURS[parcours][ - "aggregat" - ] # les semestres du parcours : par ex. ['S1', 'S2', 'S3'] - lesFidsValidantDuParcours = [ - sesFormsemestre_idValidants[ - JuryPE.TOUS_LES_SEMESTRES.index(nom_sem) - ] - for nom_sem in lesSemsDuParcours # par ex. ['SEM4532', 'SEM567', ...] - ] - parcours_incomplet = ( - sum([fid == None for fid in lesFidsValidantDuParcours]) > 0 - ) - - if not parcours_incomplet: - self.PARCOURSINFO_DICT[etudid][ - parcours - ] = lesFidsValidantDuParcours[-1] - else: - self.PARCOURSINFO_DICT[etudid][parcours] = None - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print( - parcours + "=" + str(self.PARCOURSINFO_DICT[etudid][parcours]), - end="", - ) - - # if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - # print - - # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_reoriente_ou_demissionnaire(self, etudid): - """Renvoie True si l'étudiant est réorienté (NAR) ou démissionnaire (DEM)""" - from app.scodoc import sco_report - - reponse = False - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - (_, parcours) = sco_report.get_code_cursus_etud(etud) - if ( - len(codes_cursus.CODES_SEM_REO & set(parcours.values())) > 0 - ): # Eliminé car NAR apparait dans le parcours - reponse = True - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car réorienté (NAR)") - if "DEM" in list(parcours.values()): # Eliminé car DEM - reponse = True - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car DEM") - return reponse - - # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_disparu(self, etudid): - """Renvoie True si l'étudiant n'a pas achevé la formation à l'IUT et a disparu des listes, sans - pour autant avoir été indiqué NAR ou DEM ; recherche son dernier semestre validé et regarde s'il - n'existe pas parmi les semestres existants dans scodoc un semestre postérieur (en terme de date de - début) de n° au moins égal à celui de son dernier semestre valide dans lequel il aurait pu - s'inscrire mais ne l'a pas fait.""" - sessems = self.get_semestresBUT_d_un_etudiant( - etudid - ) # les semestres de l'étudiant - sonDernierSidValide = self.get_dernier_semestre_id_valide_d_un_etudiant(etudid) - - sesdates = [ - pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems - ] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant - if sesdates: - lastdate = max(sesdates) # date de fin de l'inscription la plus récente - else: - return False - - # if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem) - - if sonDernierSidValide is None: - # si l'étudiant n'a validé aucun semestre, les prend tous ? (à vérifier) - semestresSuperieurs = self.semestresDeScoDoc - else: - semestresSuperieurs = [ - sem - for sem in self.semestresDeScoDoc - if sem["semestre_id"] > sonDernierSidValide - ] # Semestre de rang plus élevé que son dernier sem valide - datesDesSemestresSuperieurs = [ - pe_tagtable.conversionDate_StrToDate(sem["date_debut"]) - for sem in semestresSuperieurs - ] - datesDesSemestresPossibles = [ - date_deb for date_deb in datesDesSemestresSuperieurs if date_deb >= lastdate - ] # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant et de niveau plus élevé que le dernier semestre valide de l'étudiant - if ( - len(datesDesSemestresPossibles) > 0 - ): # etudiant ayant disparu de la circulation - # if PETable.AFFICHAGE_DEBUG_PE == True : - # pe_tools.pe_print(" -> à éliminer car des semestres où il aurait pu s'inscrire existent ") - # pe_tools.pe_print(pe_tools.print_semestres_description( datesDesSemestresPossibles.values() )) - return True - else: - return False - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_dernier_semestre_id_valide_d_un_etudiant(self, etudid): - """Renvoie le n° (semestre_id) du dernier semestre validé par un étudiant fourni par son etudid - et None si aucun semestre n'a été validé - """ - from app.scodoc import sco_report - - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - (code, parcours) = sco_report.get_code_cursus_etud( - etud - ) # description = '1234:A', parcours = {1:ADM, 2:NAR, ...} - sonDernierSemestreValide = max( - [ - int(cle) - for (cle, code) in parcours.items() - if code in codes_cursus.CODES_SEM_VALIDES - ] - + [0] - ) # n° du dernier semestre valide, 0 sinon - return sonDernierSemestreValide if sonDernierSemestreValide > 0 else None - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_Fid_d_un_Si_valide_d_un_etudiant(self, etudid, nom_semestre): - """Récupère le formsemestre_id valide d'un étudiant fourni son etudid à un semestre DUT de n° semestre_id - donné. Si le semestre est en cours (pas encore de jury), renvoie le formsemestre_id actuel. - """ - semestre_id = JuryPE.TOUS_LES_SEMESTRES.index(nom_semestre) + 1 - sesSi = self.get_semestresBUT_d_un_etudiant( - etudid, semestre_id - ) # extrait uniquement les Si par ordre temporel décroissant - - if len(sesSi) > 0: # S'il a obtenu au moins une note - # mT = sesMoyennes[0] - leFid = sesSi[0]["formsemestre_id"] - for i, sem in enumerate( - sesSi - ): # Parcours des éventuels semestres précédents - nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"]) - dec = nt.get_etud_decision_sem( - etudid - ) # quelle est la décision du jury ? - if dec and (dec["code"] in codes_cursus.CODES_SEM_VALIDES): - # isinstance( sesMoyennes[i+1], float) and - # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" - leFid = sem["formsemestre_id"] - else: - leFid = None - return leFid + if False: + self.get_promotags_in_jury() # **************************************************************************************************************** # # Traitements des semestres impliqués dans le jury # **************************************************************************************************************** # # ------------------------------------------------------------------------------------------------------------------ - def get_semtags_in_jury(self): + def add_semestretag_in_jury(self, nom: str, formsemestre_id: int): + """Ajoute (après création si nécessaire) un semtag dans `self.semTag` et + charge également les données des étudiants (découverts avec ce semestre). + + Args: + nom: Le nom à donner au SemestrreTag + formsemestre_id: L'identifiant d'un FormSemestre """ - Créé les semestres tagués relatifs aux résultats des étudiants à prendre en compte dans le jury. - Calcule les moyennes et les classements de chaque semestre par tag et les statistiques de ces semestres. - """ - lesFids = self.get_formsemestreids_du_jury( - self.get_etudids_du_jury(), liste_semestres=JuryPE.TOUS_LES_SEMESTRES - ) - for i, fid in enumerate(lesFids): - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "%d) Semestre taggué %s (avec classement dans groupe)" - % (i + 1, fid) - ) - self.add_semtags_in_jury(fid) + if formsemestre_id in self.semTagDict: + return - # ------------------------------------------------------------------------------------------------------------------ - def add_semtags_in_jury(self, fid): - """Crée si nécessaire un semtag et le mémorise dans self.semTag ; - charge également les données des nouveaux étudiants qui en font partis. - """ - # Semestre taggué avec classement dans le groupe - if fid not in self.semTagDict: - nt = self.get_cache_notes_d_un_semestre(fid) + """Créé le SemestreTag et exécute les calculs de moyennes""" + formsemestretag = pe_semestretag.SemestreTag(nom, formsemestre_id) - # Création du semestres - self.semTagDict[fid] = pe_semestretag.SemestreTag( - nt, nt.sem - ) # Création du pesemestre associé - self.semTagDict[fid].comp_data_semtag() - lesEtudids = self.semTagDict[fid].get_etudids() + self.semTagDict[formsemestre_id] = formsemestretag - lesEtudidsManquants = [] - for etudid in lesEtudids: - if ( - etudid not in self.PARCOURSINFO_DICT - ): # Si l'étudiant n'a pas été pris en compte dans le jury car déjà diplômé ou redoublant - lesEtudidsManquants.append(etudid) - # self.get_cache_etudInfo_d_un_etudiant(etudid) - self.add_etudiants( - etudid - ) # Ajoute les élements de parcours de l'étudiant - - nbinscrit = self.semTagDict[fid].get_nbinscrits() - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " - %d étudiants classés " % (nbinscrit) - + ": " - + ",".join( - [str(etudid) for etudid in self.semTagDict[fid].get_etudids()] - ) - ) - if lesEtudidsManquants: - pe_tools.pe_print( - " - dont %d étudiants manquants ajoutés aux données du jury" - % (len(lesEtudidsManquants)) - + ": " - + ", ".join([str(etudid) for etudid in lesEtudidsManquants]) - ) - pe_tools.pe_print(" - Export csv") - filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv" - self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) - - # ---------------------------------------------------------------------------------------------------------------- - def get_formsemestreids_du_jury(self, etudids, liste_semestres="6S"): - """Renvoie la liste des formsemestre_id validants des étudiants en parcourant les semestres valides des étudiants mémorisés dans - self.PARCOURSINFO_DICT. - Les étudiants sont identifiés par leur etudic donnés dans la liste etudids (généralement self.get_etudids_in_jury() ). - La liste_semestres peut être une liste ou une chaine de caractères parmi : - * None => tous les Fids validant - * 'Si' => le ième 1 semestre - * 'iA' => l'année i = ['S1, 'S2'] ou ['S3', 'S4'] - * '3S', '4S' => fusion des semestres - * [ 'Si', 'iA' , ... ] => une liste combinant les formats précédents - """ - champs_possibles = list(JuryPE.PARCOURS.keys()) - if ( - not isinstance(liste_semestres, list) - and not isinstance(liste_semestres, str) - and liste_semestres not in champs_possibles - ): - raise ValueError( - "Probleme de paramètres d'appel dans pe_jurype.JuryPE.get_formsemestreids_du_jury" - ) - - if isinstance(liste_semestres, list): - res = [] - for elmt in liste_semestres: - res.extend(self.get_formsemestreids_du_jury(etudids, elmt)) - return list(set(res)) - - # si liste_sem est un nom de parcours - nom_sem = liste_semestres - # if nom_sem in ['1A', '2A', '3S', '4S'] : - # return self.get_formsemestreids_du_jury(etudids, JuryPE.PARCOURS[nom_sem] ) - # else : - fids = { - self.PARCOURSINFO_DICT[etudid][nom_sem] - for etudid in etudids - if self.PARCOURSINFO_DICT[etudid][nom_sem] != None - } - - return list(fids) + if pe_tools.PE_DEBUG: + filename = nom.replace(" ", "_") + ".csv" + pe_tools.pe_print(f" - Export csv de {filename} ") + self.zipfile.writestr(filename, formsemestretag.str_tagtable()) # **************************************************************************************************************** # # Traitements des parcours impliquées dans le jury @@ -770,26 +316,24 @@ class JuryPE(object): # ---> sur 2 parcours DUT (cas S3 fini, cas S4 fini) - for i, nom in enumerate(JuryPE.TOUS_LES_AGGREGATS): - parcours = JuryPE.PARCOURS[nom][ + for i, nom in enumerate(pe_tools.TOUS_LES_AGGREGATS): + parcours = pe_tools.PARCOURS[nom][ "aggregat" ] # La liste des noms de semestres (S1, S2, ...) impliqués dans l'aggrégat # Recherche des parcours possibles par le biais de leur Fid final fids_finaux = self.get_formsemestreids_du_jury( - self.get_etudids_du_jury(), nom + self.etudiants.get_etudids(self.diplome), nom ) # les formsemestre_ids validant finaux des étudiants du jury if len(fids_finaux) > 0: # S'il existe des parcours validant - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print("%d) Fusion %s avec" % (i + 1, nom)) + pe_tools.pe_print("%d) Fusion %s avec" % (i + 1, nom)) if nom not in self.setTagDict: self.setTagDict[nom] = {} for fid in fids_finaux: - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print(" - semestre final %s" % (fid)) + pe_tools.pe_print(" - semestre final %s" % (fid)) settag = pe_settag.SetTag( nom, parcours=parcours ) # Le set tag fusionnant les données @@ -800,8 +344,8 @@ class JuryPE(object): # ajoute les étudiants au semestre settag.set_Etudiants( etudiants, - self.PARCOURSINFO_DICT, - self.ETUDINFO_DICT, + self.etudiants.cursus, + self.etudiants.identites, nom_sem_final=self.semTagDict[fid].nom, ) @@ -811,7 +355,7 @@ class JuryPE(object): pe_tools.pe_print( " -> ajout du semestre tagué %s" % (ffid) ) - self.add_semtags_in_jury(ffid) + self.add_semestretag_in_jury(ffid) settag.set_SemTagDict( self.semTagDict ) # ajoute les semestres au settag @@ -821,36 +365,34 @@ class JuryPE(object): self.setTagDict[nom][fid] = settag # Mémorise le résultat else: - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print("%d) Pas de fusion %s possible" % (i + 1, nom)) + pe_tools.pe_print("%d) Pas de fusion %s possible" % (i + 1, nom)) def get_promotags_in_jury(self): """Calcule les aggrégats en interclassant les étudiants du jury (les moyennes ont déjà été calculées en amont)""" - lesEtudids = self.get_etudids_du_jury() + lesEtudids = self.etudiants.get_etudids(self.diplome) - for i, nom in enumerate(JuryPE.PARCOURS.keys()): + for i, nom in enumerate(pe_tools.PARCOURS.keys()): settag = pe_settag.SetTagInterClasse(nom, diplome=self.diplome) nbreEtudInscrits = settag.set_Etudiants( - lesEtudids, self.PARCOURSINFO_DICT, self.ETUDINFO_DICT + lesEtudids, self.etudiants.cursus, self.etudiants.identites ) if nbreEtudInscrits > 0: if pe_tools.PE_DEBUG: pe_tools.pe_print( "%d) %s avec interclassement sur la promo" % (i + 1, nom) ) - if nom in JuryPE.TOUS_LES_SEMESTRES: + if nom in pe_tools.TOUS_LES_SEMESTRES: settag.set_SetTagDict(self.semTagDict) else: # cas des aggrégats settag.set_SetTagDict(self.setTagDict[nom]) settag.comp_data_settag() self.promoTagDict[nom] = settag else: - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "%d) Pas d'interclassement %s sur la promo faute de notes" - % (i + 1, nom) - ) + pe_tools.pe_print( + "%d) Pas d'interclassement %s sur la promo faute de notes" + % (i + 1, nom) + ) # **************************************************************************************************************** # # Méthodes pour la synthèse du juryPE @@ -858,7 +400,7 @@ class JuryPE(object): def synthetise_juryPE(self): """Synthétise tous les résultats du jury PE dans un dictionnaire""" self.syntheseJury = {} - for etudid in self.get_etudids_du_jury(): + for etudid in self.etudiants.get_etudids(self.diplome): etudinfo = self.ETUDINFO_DICT[etudid] self.syntheseJury[etudid] = { "nom": etudinfo["nom"], @@ -886,17 +428,17 @@ class JuryPE(object): ) # nombre de semestres # Ses résultats - for nom in JuryPE.PARCOURS: # S1, puis S2, puis 1A + for nom in pe_tools.PARCOURS: # S1, puis S2, puis 1A # dans le groupe : la table tagguée dans les semtag ou les settag si aggrégat self.syntheseJury[etudid][nom] = {"groupe": {}, "promo": {}} if ( - self.PARCOURSINFO_DICT[etudid][nom] != None + self.etudiants.cursus[etudid][nom] != None ): # Un parcours valide existe - if nom in JuryPE.TOUS_LES_SEMESTRES: - tagtable = self.semTagDict[self.PARCOURSINFO_DICT[etudid][nom]] + if nom in pe_tools.TOUS_LES_SEMESTRES: + tagtable = self.semTagDict[self.etudiants.cursus[etudid][nom]] else: tagtable = self.setTagDict[nom][ - self.PARCOURSINFO_DICT[etudid][nom] + self.etudiants.cursus[etudid][nom] ] for tag in tagtable.get_all_tags(): self.syntheseJury[etudid][nom]["groupe"][ @@ -925,7 +467,7 @@ class JuryPE(object): def get_parcoursIUT(self, etudid): """Renvoie une liste d'infos sur les semestres du parcours d'un étudiant""" # etudinfo = self.ETUDINFO_DICT[etudid] - sems = self.get_semestresBUT_d_un_etudiant(etudid) + sems = pe_etudiants.semestres_etudiant(etudid) infos = [] for sem in sems: @@ -945,12 +487,12 @@ class JuryPE(object): def str_etudiants_in_jury(self, delim=";"): # En tete: entete = ["Id", "Nom", "Abandon", "Diplome"] - for nom_sem in JuryPE.TOUS_LES_PARCOURS: + for nom_sem in pe_tools.TOUS_LES_PARCOURS: entete += [nom_sem, "descr"] chaine = delim.join(entete) + "\n" - for etudid in self.PARCOURSINFO_DICT: - donnees = self.PARCOURSINFO_DICT[etudid] + for etudid in self.etudiants.cursus: + donnees = self.etudiants.cursus[etudid] # pe_tools.pe_print(etudid, donnees) # les infos générales descr = [ @@ -961,7 +503,7 @@ class JuryPE(object): ] # les semestres et les aggrégats - for nom_sem in JuryPE.TOUS_LES_PARCOURS: + for nom_sem in pe_tools.TOUS_LES_PARCOURS: table = ( self.semTagDict[donnees[nom_sem]].nom if donnees[nom_sem] in self.semTagDict @@ -980,7 +522,7 @@ class JuryPE(object): """Export csv de self.PARCOURSINFO_DICT""" fichier = "juryParcoursDict_" + str(self.diplome) pe_tools.pe_print(" -> Export de " + fichier) - filename = self.NOM_EXPORT_ZIP + fichier + ".csv" + filename = self.nom_export_zip + fichier + ".csv" self.zipfile.writestr(filename, self.str_etudiants_in_jury()) def get_allTagForAggregat(self, nom_aggregat): @@ -988,7 +530,7 @@ class JuryPE(object): d'un aggrégat donné par son nom (S1, S2, S3 ou S4, 1A, ...). Renvoie [] si aucun tag. """ taglist = set() - for etudid in self.get_etudids_du_jury(): + for etudid in self.etudiants.get_etudids(): taglist = taglist.union( set(self.syntheseJury[etudid][nom_aggregat]["groupe"].keys()) ) @@ -1001,7 +543,7 @@ class JuryPE(object): """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag""" allTags = set() - for nom in JuryPE.TOUS_LES_PARCOURS: + for nom in pe_tools.TOUS_LES_PARCOURS: allTags = allTags.union(set(self.get_allTagForAggregat(nom))) return sorted(list(allTags)) if len(allTags) > 0 else [] @@ -1054,9 +596,9 @@ class JuryPE(object): ] # Les aggrégats à afficher par ordre tel que indiqué dans le dictionnaire parcours - aggregats = list(JuryPE.PARCOURS.keys()) # ['S1', 'S2', ..., '1A', '4S'] + aggregats = list(pe_tools.PARCOURS.keys()) # ['S1', 'S2', ..., '1A', '4S'] # aggregats = sorted( - # aggregats, key=lambda t: JuryPE.PARCOURS[t]["ordre"] + # aggregats, key=lambda t: pe_tools.PARCOURS[t]["ordre"] # ) # Tri des aggrégats if mode == "multiplesheet": @@ -1064,15 +606,15 @@ class JuryPE(object): self.get_allTagInSyntheseJury() ) # tous les tags de syntheseJuryDict allSheets = sorted(allSheets) # Tri des tags par ordre alphabétique - for sem in JuryPE.TOUS_LES_PARCOURS: + for sem in pe_tools.TOUS_LES_PARCOURS: entete.extend(["%s %s" % (sem, champ) for champ in champs]) else: # "singlesheet" allSheets = ["singlesheet"] for ( sem ) in ( - JuryPE.TOUS_LES_PARCOURS - ): # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] + pe_tools.TOUS_LES_PARCOURS + ): # pe_tools.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] tags = self.get_allTagForAggregat(sem) entete.extend( ["%s %s %s" % (sem, tag, champ) for tag in tags for champ in champs] @@ -1105,7 +647,7 @@ class JuryPE(object): n += 1 # if self.syntheseJury[etudid]['nbSemestres'] < maxParcours: # descr += delim.join( ['']*( maxParcours -self.syntheseJury[etudid]['nbSemestres']) ) + delim - for sem in aggregats: # JuryPE.PARCOURS.keys(): + for sem in aggregats: # pe_tools.PARCOURS.keys(): listeTags = ( self.get_allTagForAggregat(sem) if mode == "singlesheet" @@ -1170,11 +712,14 @@ class JuryPE(object): def get_cache_etudInfo_d_un_etudiant(self, etudid): """Renvoie les informations sur le parcours d'un étudiant soit en les relisant depuis ETUDINFO_DICT si mémorisée soit en les chargeant et en les mémorisant + + TODO:: A supprimer à long terme """ if etudid not in self.ETUDINFO_DICT: - self.ETUDINFO_DICT[etudid] = sco_etud.get_etud_info( - etudid=etudid, filled=True - )[0] + self.ETUDINFO_DICT[etudid] = Identite.get_etud(etudid=etudid) + # sco_etud.get_etud_info( + # etudid=etudid, filled=True + # ))[0] return self.ETUDINFO_DICT[etudid] # ------------------------------------------------------------------------------------------------------------------ @@ -1188,38 +733,17 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_semestresBUT_d_un_etudiant(self, etudid, semestre_id=None): - """Renvoie la liste des semestres DUT d'un étudiant - pour un semestre_id (parmi 1,2,3,4) donné - en fonction de ses infos d'etud (cf. sco_etud.get_etud_info( etudid=etudid, filled=True)[0]), - les semestres étant triés par ordre décroissant. - Si semestre_id == None renvoie tous les semestres""" - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - nbre_semestres = int(JuryPE.AGGREGAT_DIPLOMANT[0]) # 6 - if semestre_id == None: - sesSems = [ - sem for sem in etud["sems"] if 1 <= sem["semestre_id"] <= nbre_semestres - ] - else: - sesSems = [sem for sem in etud["sems"] if sem["semestre_id"] == semestre_id] - return sesSems + def get_semestresBUT_d_un_etudiant(self, identite: Identite, semestre_id=None): + """cf. pe_etudiant.semestres_etudiant()""" - # ********************************************** - def calcul_anneePromoBUT_d_un_etudiant(self, etudid) -> int: - """Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid - en fonction de ses semestres de scolarisation""" - semestres = self.get_semestresBUT_d_un_etudiant(etudid) - if semestres: - return max([get_annee_diplome_semestre(sem) for sem in semestres]) - else: - return None + return None # ********************************************* # Fonctions d'affichage pour debug def get_resultat_d_un_etudiant(self, etudid): chaine = "" - for nom_sem in JuryPE.TOUS_LES_SEMESTRES: - semtagid = self.PARCOURSINFO_DICT[etudid][ + for nom_sem in pe_tools.TOUS_LES_SEMESTRES: + semtagid = self.etudiants.cursus[etudid][ nom_sem ] # le formsemestre_id du semestre taggué de l'étudiant semtag = self.semTagDict[semtagid] @@ -1248,85 +772,3 @@ class JuryPE(object): if annees_debut: return str(min(annees_debut)) return "" - - -# ---------------------------------------------------------------------------------------- -# Fonctions - - -# ---------------------------------------------------------------------------------------- -def get_annee_diplome_semestre(sem) -> int: - """Pour un semestre donne, décrit par le biais du dictionnaire sem usuel : - - sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...} - - à condition qu'il soit un semestre de formation BUT, - predit l'annee à laquelle sera remis le diplome BUT des etudiants scolarisés dans le semestre - (en supposant qu'il n'y ait plus de redoublement) et la renvoie sous la forme d'un int. - - Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés) - s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'annee universitaire. - - Par exemple : - * S5 débutant en 2025 finissant en 2026 => diplome en 2026 - * S3 debutant en 2025 et finissant en 2026 => diplome en 2027 - * S5 décalé débutant en 2025 et finissant en 2025 => diplome en 2026 - * S3 decale débutant en 2025 et finissant en 2025 => diplome en 2027 - - La règle de calcul utilise l'``annee_fin`` du semestre sur le principe suivant : - - * nbreSemRestant = nombre de semestres restant avant diplome - * nbreAnneeRestant = nombre d'annees restant avant diplome - * 1 - delta = 0 si semestre de 1ere partie d'annee / 1 sinon - * decalage = active ou désactive un increment à prendre en compte en cas de semestre decale - - Args: - sem: Le semestre - """ - nbre_semestres = int(JuryPE.AGGREGAT_DIPLOMANT[0]) # 6 - if ( - 1 <= sem["semestre_id"] <= nbre_semestres - ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? - nbreSemRestant = nbre_semestres - sem["semestre_id"] - nbreAnRestant = nbreSemRestant // 2 - delta = int(sem["annee_fin"]) - int(sem["annee_debut"]) - decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 - increment = decalage * (1 - delta) - return int(sem["annee_fin"]) + nbreAnRestant + increment - - -# ---------------------------------------------------------------------------------------- - - -# ---------------------------------------------------------------------------------- -def get_cosemestres_diplomants(semBase, avec_meme_formation=False): - """Partant d'un semestre de Base = {'formsemestre_id': ..., 'semestre_id': ..., 'annee_debut': ...}, - renvoie la liste de tous ses co-semestres (lui-meme inclus) - Par co-semestre, s'entend les semestres : - > dont l'annee predite pour la remise du diplome DUT est la meme - > dont la formation est la même (optionnel) - > ne prenant en compte que les etudiants sans redoublement - """ - tousLesSems = ( - sco_formsemestre.do_formsemestre_list() - ) # tous les semestres memorisés dans scodoc - diplome = get_annee_diplome_semestre(semBase) - - if avec_meme_formation: # si une formation est imposee - nom_formation = str(semBase["formation_id"]) - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" - avec formation imposée : ", nom_formation) - coSems = [ - sem - for sem in tousLesSems - if get_annee_diplome_semestre(sem) == diplome - and sem["formation_id"] == semBase["formation_id"] - ] - else: - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" - toutes formations confondues") - coSems = [ - sem for sem in tousLesSems if get_annee_diplome_semestre(sem) == diplome - ] - - return coSems diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index f7a452095..b5fae7530 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -37,110 +37,100 @@ Created on Fri Sep 9 09:15:05 2016 """ from app import db, log -from app.comp import res_sem +from app.comp import res_sem, inscr_mod, moy_ue, moy_sem +from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.comp.res_sem import load_formsemestre_results +from app.models import FormSemestre, Identite, DispenseUE from app.models.moduleimpls import ModuleImpl from app.pe import pe_tagtable +from app.pe import pe_tools -from app.scodoc import codes_cursus +from app.scodoc import codes_cursus, sco_preferences from app.scodoc import sco_tag_module from app.scodoc import sco_utils as scu +from app.scodoc.codes_cursus import UE_SPORT class SemestreTag(pe_tagtable.TableTag): - """Un SemestreTag représente un tableau de notes (basé sur notesTable) - modélisant les résultats des étudiants sous forme de moyennes par tag. - - Attributs récupérés via des NotesTables : - - nt: le tableau de notes du semestre considéré - - nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions) - - nt.identdict: { etudid : ident } - - liste des moduleimpl { ... 'module_id', ...} - - Attributs supplémentaires : - - inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants - - _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés - - - Attributs hérités de TableTag : - - nom : - - resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...} - - rang - - statistiques - - Redéfinition : - - get_etudids() : les etudids des étudiants non défaillants ni démissionnaires + """Un SemestreTag représente les résultats des étudiants à un semestre, en donnant + accès aux moyennes par tag. + Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. """ - DEBUG = True - # ----------------------------------------------------------------------------- # Fonctions d'initialisation # ----------------------------------------------------------------------------- - def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable - """Instantiation d'un objet SemestreTag à partir d'un tableau de note - et des informations sur le semestre pour le dater + def __init__(self, nom: str, formsemestre_id: int): + """ + Args: + nom: Nom à donner au SemestreTag + formsemestre_id: Identifiant du FormSemestre sur lequel il se base """ pe_tagtable.TableTag.__init__( self, - nom="S%d %s %s-%s" - % ( - sem["semestre_id"], - "ENEPS" - if "ENEPS" in sem["titre"] - else "UFA" - if "UFA" in sem["titre"] - else "FI", - sem["annee_debut"], - sem["annee_fin"], - ), + nom=nom ) + """Le semestre""" + self.formsemestre_id = formsemestre_id + self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - # Les attributs spécifiques - self.nt = notetable + """Les résultats du semestre""" + self.nt = load_formsemestre_results(self.formsemestre) - # Les attributs hérités : la liste des étudiants - self.inscrlist = [ - etud - for etud in self.nt.inscrlist - if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT - ] - self.identdict = { - etudid: ident - for (etudid, ident) in self.nt.identdict.items() - if etudid in self.get_etudids() - } # Liste des étudiants non démissionnaires et non défaillants + """Les étudiants""" + self.etuds = self.nt.etuds + self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} - # Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards - self.modimpls = [ - modimpl - for modimpl in self.nt.formsemestre.modimpls_sorted - if modimpl.module.ue.type == codes_cursus.UE_STANDARD - ] # la liste des modules (objet modimpl) - self.somme_coeffs = sum( - [ - modimpl.module.coefficient - for modimpl in self.modimpls - if modimpl.module.coefficient is not None - ] - ) + """Les notes, les modules implémentés triés, les étudiants, les coeffs, + récupérés notamment de py:mod:`res_but` + """ + self.sem_cube = self.nt.sem_cube + self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted + self.modimpl_coefs_df = self.nt.modimpl_coefs_df - # ----------------------------------------------------------------------------- - def comp_data_semtag(self): - """Calcule tous les données numériques associées au semtag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.tagdict = ( - self.do_tagdict() - ) # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés + """Les inscriptions au module et les dispenses d'UE""" + self.modimpl_inscr_df = self.nt.modimpl_inscr_df + self.ues = self.nt.ues + self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() + self.dispense_ues = self.nt.dispense_ues - # Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT" - for tag in self.tagdict: - self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True)) - self.add_moyennesTag("but", self.get_moyennes_DUT()) - self.taglist = sorted( - list(self.tagdict.keys()) + ["but"] - ) # actualise la liste des tags + """Les tags""" + self.tags = get_synthese_tags_semestre(self.nt.formsemestre) + + """Supprime les tags réservés""" + for tag in pe_tagtable.TAGS_RESERVES: + if tag in self.tags: + del self.tags[tag] + + """Calcul des moyennes & les classements de chaque étudiant à chaque tag""" + self.moyennes_tags = {} + + for tag in self.tags: + pe_tools.pe_print(f" -> Traitement du tag {tag}") + moy_gen_tag = self.compute_moyenne_tag(tag) + class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int + self.moyennes_tags[tag] = { + "notes": moy_gen_tag, + "classements": class_gen_tag, + "min": moy_gen_tag.min(), + "max": moy_gen_tag.max(), + "moy": moy_gen_tag.mean(), + "nb_inscrits": len(moy_gen_tag), + } + + """Ajoute les moyennes générales de BUT pour le semestre considéré""" + pe_tools.pe_print(f" -> Traitement du tag but") + moy_gen_but = self.nt.etud_moy_gen + class_gen_but = self.nt.etud_moy_gen_ranks_int + self.moyennes_tags["but"] = { + "notes": moy_gen_but, + "classements": class_gen_but, + "min": moy_gen_but.min(), + "max": moy_gen_but.max(), + "moy": moy_gen_but.mean(), + "nb_inscrits": len(moy_gen_but), + } # ----------------------------------------------------------------------------- def get_etudids(self): @@ -148,89 +138,64 @@ class SemestreTag(pe_tagtable.TableTag): return [etud["etudid"] for etud in self.inscrlist] # ----------------------------------------------------------------------------- - def do_tagdict(self): - """Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la - forme d'un dictionnaire reliant les tags saisis dans le programme aux - données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff, - la pondération fournie avec le tag (par défaut 1 si non indiquée). - { tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...}, - modimpl_id2 : .... - }, - tagname2 : ... - } - Renvoie le dictionnaire ainsi construit. + def compute_moyenne_tag(self, tag: str) -> list: + """Calcule la moyenne des étudiants pour le tag indiqué, + pour ce SemestreTag. - Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car - correspond à la majorité des calculs de moyennes pour les étudiants - (seuls ceux qui ont capitalisé des ue auront un régime de calcul différent). - """ - tagdict = {} + Sont pris en compte les modules implémentés associés au tag, + avec leur éventuel coefficient de **repondération**, en utilisant les notes + chargées pour ce SemestreTag. - for modimpl in []: # CB: désactive la recherche des tags -> self.modimpls: - modimpl_id = modimpl.id - # liste des tags pour le modimpl concerné: - tags = sco_tag_module.module_tag_list(modimpl.module.id) - - for ( - tag - ) in tags: # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2" - [tagname, ponderation] = sco_tag_module.split_tagname_coeff( - tag - ) # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1) - # tagname = tagname - if tagname not in tagdict: # Ajout d'une clé pour le tag - tagdict[tagname] = {} - - # Ajout du modimpl au tagname considéré - tagdict[tagname][modimpl_id] = { - "module_id": modimpl.module.id, # les données sur le module - "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre - "ponderation": ponderation, # la pondération demandée pour le tag sur le module - "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee - "ue_id": modimpl.module.ue.id, # les données sur l'ue - "ue_code": modimpl.module.ue.ue_code, - "ue_acronyme": modimpl.module.ue.acronyme, - } - return tagdict - - # ----------------------------------------------------------------------------- - def comp_MoyennesTag(self, tag, force=False) -> list: - """Calcule et renvoie les "moyennes" de tous les étudiants du SemTag - (non défaillants) à un tag donné, en prenant en compte - tous les modimpl_id concerné par le tag, leur coeff et leur pondération. Force ou non le calcul de la moyenne lorsque des notes sont manquantes. Renvoie les informations sous la forme d'une liste [ (moy, somme_coeff_normalise, etudid), ...] """ - lesMoyennes = [] - for etudid in self.get_etudids(): - ( - notes, - coeffs_norm, - ponderations, - ) = self.get_listesNotesEtCoeffsTagEtudiant( - tag, etudid - ) # les notes associées au tag - coeffs = comp_coeff_pond( - coeffs_norm, ponderations - ) # les coeff pondérés par les tags - (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme( - notes, coeffs, force=force - ) - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes - # ----------------------------------------------------------------------------- - def get_moyennes_DUT(self): - """Lit les moyennes DUT du semestre pour tous les étudiants - et les renvoie au même format que comp_MoyennesTag""" - return [ - (self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids() + """Adaptation du mask de calcul des moyennes au tag visé""" + modimpls_mask = [ + modimpl.module.ue.type != UE_SPORT + for modimpl in self.formsemestre.modimpls_sorted ] + """Désactive tous les modules qui ne sont pas pris en compte pour ce tag""" + for i, modimpl in enumerate(self.formsemestre.modimpls_sorted): + if modimpl.moduleimpl_id not in self.tags[tag]: + modimpls_mask[i] = False + + """Applique la pondération des coefficients""" + modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy() + for modimpl_id in self.tags[tag]: + ponderation = self.tags[tag][modimpl_id]["ponderation"] + modimpl_coefs_ponderes_df[modimpl_id] *= ponderation + + """Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)""" + moyennes_ues_tag = moy_ue.compute_ue_moys_apc( + self.sem_cube, + self.etuds, + self.formsemestre.modimpls_sorted, + self.modimpl_inscr_df, + modimpl_coefs_ponderes_df, + modimpls_mask, + self.dispense_ues, + block=self.formsemestre.block_moyennes, + ) + + """Les ects""" + ects = self.ues_inscr_parcours_df.fillna(0.0) * [ + ue.ects for ue in self.ues if ue.type != UE_SPORT + ] + + """Calcule la moyenne générale dans le semestre (pondérée par le ECTS)""" + moy_gen_tag = moy_sem.compute_sem_moys_apc_using_ects( + moyennes_ues_tag, + ects, + formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + + return moy_gen_tag + # ----------------------------------------------------------------------------- def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2): """Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id. @@ -319,27 +284,6 @@ class SemestreTag(pe_tagtable.TableTag): return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") return [] - # ----------------------------------------------------------------------------- - def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid): - """Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes - donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans - le calcul de la moyenne du tag. - Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée). - Les pondérations sont celles déclarées avec le tag (cf. _tagdict).""" - - notes = [] - coeffs_norm = [] - ponderations = [] - for moduleimpl_id, modimpl in self.tagdict[ - tag - ].items(): # pour chaque module du semestre relatif au tag - (note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid) - if note != None: - notes.append(note) - coeffs_norm.append(coeff_norm) - ponderations.append(modimpl["ponderation"]) - return (notes, coeffs_norm, ponderations) - # ----------------------------------------------------------------------------- # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug # ----------------------------------------------------------------------------- @@ -435,8 +379,9 @@ class SemestreTag(pe_tagtable.TableTag): return chaine def str_tagsModulesEtCoeffs(self): - """Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération. - Plus concrêtement permet d'afficher le contenu de self._tagdict""" + """Renvoie une chaine affichant la liste des tags associés au semestre, + les modules qui les concernent et les coeffs de pondération. + Plus concrètement permet d'afficher le contenu de self._tagdict""" chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n" chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n" taglist = self.get_all_tags() @@ -463,25 +408,6 @@ class SemestreTag(pe_tagtable.TableTag): # ********************************************* -def comp_coeff_pond(coeffs, ponderations): - """ - Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients : - ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None] - Les coeff peuvent éventuellement être None auquel cas None est conservé ; - Les pondérations sont des floattants - """ - if ( - coeffs == None - or ponderations == None - or not isinstance(coeffs, list) - or not isinstance(ponderations, list) - or len(coeffs) != len(ponderations) - ): - raise ValueError("Erreur de paramètres dans comp_coeff_pond") - return [ - (None if coeffs[i] == None else coeffs[i] * ponderations[i]) - for i in range(len(coeffs)) - ] # ----------------------------------------------------------------------------- @@ -509,3 +435,58 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: if ue_status is None: return None return ue_status["moy"] + + +# ----------------------------------------------------------------------------- +def get_synthese_tags_semestre(formsemestre: FormSemestre): + """Etant données les implémentations des modules du semestre (modimpls), + synthétise les tags les concernant (tags saisis dans le programme pédagogique) + en les associant aux modimpls qui les concernent (modimpl_id, module_id, + le code du module, coeff et pondération fournie avec le tag (par défaut 1 si non indiquée)). + + { tagname1: { modimpl_id1: { 'module_id': ..., + 'coeff': ..., + 'coeff_norm': ..., + 'ponderation': ..., + 'module_code': ..., + 'ue_xxx': ...}, + } + } + + Args: + formsemestre: Le formsemestre à la base de la recherche des tags + """ + synthese_tags = {} + + """Instance des modules du semestre""" + modimpls = formsemestre.modimpls_sorted + + for modimpl in modimpls: + modimpl_id = modimpl.id + + """Liste des tags pour le module concerné""" + tags = sco_tag_module.module_tag_list(modimpl.module.id) + + """Traitement des tags recensés, chacun pouvant étant de la forme + "mathématiques", "théorie", "pe:0", "maths:2" + """ + for tag in tags: + """Extraction du nom du tag et du coeff de pondération""" + (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag) + + """Ajout d'une clé pour le tag""" + if tagname not in synthese_tags: + synthese_tags[tagname] = {} + + """Ajout du module (modimpl) au tagname considéré""" + synthese_tags[tagname][modimpl_id] = { + "modimpl": modimpl, # les données sur le module + # "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre + "ponderation": ponderation, # la pondération demandée pour le tag sur le module + # "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee + # "ue_id": modimpl.module.ue.id, # les données sur l'ue + # "ue_code": modimpl.module.ue.ue_code, + # "ue_acronyme": modimpl.module.ue.acronyme, + } + + return synthese_tags diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index 82cf16b52..ad69a0909 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -41,36 +41,52 @@ import datetime import numpy as np from app.scodoc import sco_utils as scu +import pandas as pd + + +TAGS_RESERVES = ["but"] class TableTag(object): """ - Classe mémorisant les moyennes des étudiants à différents tag et permettant de calculer les rangs et les statistiques : - - nom : Nom représentatif des données de la Table - - inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme : + Classe mémorisant les moyennes des étudiants à différents tags et permettant de + calculer des rangs et des statistiques. + + Ses attributs sont: + + * nom : Nom représentatif des données de la Table + * inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme : { etudid : dictionnaire d'info extrait de Scodoc, ...} - - taglist : Liste triée des noms des tags - - resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée + * taglist : Liste triée des noms des tags + * resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme : { tag : { etudid: (note_moy, somme_coeff_norm), ...} - - rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme : + * rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme : { tag : {etudid: rang, ...} } - - nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags) - - statistiques : Dictionnaire donnant les stastitiques (moyenne, min, max) des résultats par tag de la forme : + * nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags) + * statistiques : Dictionnaire donnant les statistiques (moyenne, min, max) des résultats par tag de la forme : { tag : (moy, min, max), ...} """ - def __init__(self, nom=""): + def __init__(self, nom: str): + """Les attributs basiques des TagTable, qui seront initialisés + dans les classes dérivées + """ self.nom = nom - self.inscrlist = [] - self.identdict = {} - self.taglist = [] + """Les étudiants""" + self.etudiants = {} + """Les moyennes par tag""" + self.moyennes_tags = {} + + + # ----------------------------------------------------------------------------------------------------------- + def get_all_tags(self): + """Renvoie la liste des tags du semestre triée par ordre alphabétique""" + # return self.taglist + return sorted(self.moyennes_tags.keys()) - self.resultats = {} - self.rangs = {} - self.statistiques = {} # ***************************************************************************************************************** # Accesseurs @@ -80,8 +96,8 @@ class TableTag(object): def get_moy_from_resultats(self, tag, etudid): """Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats""" return ( - self.resultats[tag][etudid][0] - if tag in self.resultats and etudid in self.resultats[tag] + self.moyennes_tags[tag][etudid][0] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) @@ -90,7 +106,7 @@ class TableTag(object): """Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats""" return ( self.rangs[tag][etudid] - if tag in self.resultats and etudid in self.resultats[tag] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) @@ -100,16 +116,11 @@ class TableTag(object): au regard du format de self.resultats. """ return ( - self.resultats[tag][etudid][1] - if tag in self.resultats and etudid in self.resultats[tag] + self.moyennes_tags[tag][etudid][1] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) - # ----------------------------------------------------------------------------------------------------------- - def get_all_tags(self): - """Renvoie la liste des tags du semestre triée par ordre alphabétique""" - # return self.taglist - return sorted(self.resultats.keys()) # ----------------------------------------------------------------------------------------------------------- def get_nbinscrits(self): @@ -170,10 +181,12 @@ class TableTag(object): avec calcul du rang :param tag: Un tag :param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ] + + TODO:: Inutile maintenant ? """ # ajout des moyennes au dictionnaire résultat if listMoyEtCoeff: - self.resultats[tag] = { + self.moyennes_tags[tag] = { etudid: (moyenne, somme_coeffs) for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff } @@ -204,11 +217,12 @@ class TableTag(object): self.statistiques """ stats = ("-NA-", "-", "-") - if tag not in self.resultats: + if tag not in self.moyennes_tags: return stats notes = [ - self.get_moy_from_resultats(tag, etudid) for etudid in self.resultats[tag] + self.get_moy_from_resultats(tag, etudid) + for etudid in self.moyennes_tags[tag] ] # les notes du tag notes_valides = [ note for note in notes if isinstance(note, float) and note != None @@ -225,7 +239,7 @@ class TableTag(object): """Renvoie une chaine de caractères (valable pour un csv) décrivant la moyenne et le rang d'un étudiant, pour un tag donné ; """ - if tag not in self.get_all_tags() or etudid not in self.resultats[tag]: + if tag not in self.get_all_tags() or etudid not in self.moyennes_tags[tag]: return "" moystr = TableTag.str_moytag( @@ -256,30 +270,18 @@ class TableTag(object): str_moytag = classmethod(str_moytag) # ----------------------------------------------------------------------- - def str_tagtable(self, delim=";", decimal_sep=","): - """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags.""" - entete = ["etudid", "nom", "prenom"] + def str_tagtable(self): + """Renvoie une chaine de caractère listant toutes les moyennes, + les rangs des étudiants pour tous les tags.""" + + etudiants = self.etudiants + df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) + for tag in self.get_all_tags(): - entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]] - chaine = delim.join(entete) + "\n" + df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}")) + df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}")) - for etudid in self.identdict: - descr = delim.join( - [ - str(etudid), - self.identdict[etudid]["nom"], - self.identdict[etudid]["prenom"], - ] - ) - descr += delim + self.str_res_d_un_etudiant(etudid, delim) - chaine += descr + "\n" - - # Ajout des stats ... à faire - - if decimal_sep != ".": - return chaine.replace(".", decimal_sep) - else: - return chaine + return df.to_csv(sep=";") # ************************************************************************ diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index cf535e3b1..2f51e383e 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -292,24 +292,35 @@ def get_etud_tagged_modules(etudid, tagname): return R -def split_tagname_coeff(tag, separateur=":"): - """Découpe un tag saisi par un utilisateur pour en extraire un tagname - (chaine de caractère correspondant au tag) - et un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":"). - Renvoie le résultat sous la forme d'une liste [tagname, pond] où pond est un float +def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: + """Découpage d'un tag, tel que saisi par un utilisateur dans le programme, + pour en extraire : - Auteur: CB + * son _nom de tag_ (tagname) (chaine de caractère correspondant au tag) + * un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":"). + + Args: + tag: La saisie utilisateur du tag dans le programme + separateur: Le séparateur des informations dans la saisie utilisateur + + Return: + Tuple (tagname, coeff_de_ponderation) extrait de la saisie utilisateur + (avec coeff_de_ponderation=1.0 si non mentionné) + + Author: + Cléo Baras """ if separateur in tag: temp = tag.split(":") try: pond = float(temp[1]) - return [temp[0], pond] + return (temp[0], pond) except: - return [tag, 1.0] # renvoie tout le tag si le découpage à échouer + """Renvoie tout le tag si le découpage à échouer""" + return (tag, 1.0) else: - # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag - return [tag, 1.0] + """initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag""" + return (tag, 1.0) """Tests: