From 86e8803c8795991bab994ad4a027fcf2f80bfe35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Mon, 15 Jan 2024 19:45:38 +0100 Subject: [PATCH 01/23] =?UTF-8?q?Cr=C3=A9ation=20branche=20pe-DUT-to-BUT?= =?UTF-8?q?=20+=20changement=20dut->but=20dans=20excel=20export=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jurype.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 01687f7c..a9899340 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -984,9 +984,9 @@ class JuryPE(object): rows=[], titles={"pas d'étudiants": "pas d'étudiants"}, html_sortable=True, - xls_sheet_name="dut", + xls_sheet_name="but", ) - sT.add_genTable("dut", T) + sT.add_genTable("but", T) return sT # Si des étudiants From e28bfa34be7efa63738a6f461e3a33bc8ba41880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 05:36:27 +0100 Subject: [PATCH 02/23] =?UTF-8?q?Ent=C3=AAte=20des=20m=C3=A9thodes=20&=20f?= =?UTF-8?q?onctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_avislatex.py | 37 ++++++++++++++++------ app/pe/pe_jurype.py | 68 +++++++++++++++++++++++++++------------- app/pe/pe_semestretag.py | 32 ++++++++++++++----- app/pe/pe_settag.py | 46 +++++++++++++++++++-------- app/pe/pe_tagtable.py | 55 +++++++++++++++++++++++--------- app/pe/pe_tools.py | 15 ++++++--- 6 files changed, 181 insertions(+), 72 deletions(-) diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py index 3c498f47..b5e6fb07 100644 --- a/app/pe/pe_avislatex.py +++ b/app/pe/pe_avislatex.py @@ -67,7 +67,8 @@ def get_code_latex_from_modele(fichier): # ---------------------------------------------------------------------------------------- -def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"): +def get_code_latex_from_scodoc_preference(formsemestre_id, + champ="pe_avis_latex_tmpl"): """ Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX et s'assure qu'il est renvoyé au format unicode @@ -94,7 +95,9 @@ def get_tags_latex(code_latex): return [] -def comp_latex_parcourstimeline(etudiant, promo, taille=17): +def comp_latex_parcourstimeline(etudiant, + promo, + taille=17): """Interprète un tag dans un avis latex **parcourstimeline** et génère le code latex permettant de retracer le parcours d'un étudiant sous la forme d'une frise temporelle. @@ -156,9 +159,11 @@ def interprete_tag_latex(tag): # ---------------------------------------------------------------------------------------- -def get_code_latex_avis_etudiant( - donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs -): +def get_code_latex_avis_etudiant(donnees_etudiant, + un_avis_latex, + annotationPE, + footer_latex, + prefs): """ Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un @@ -220,7 +225,8 @@ def get_code_latex_avis_etudiant( # ---------------------------------------------------------------------------------------- -def get_annotation_PE(etudid, tag_annotation_pe): +def get_annotation_PE(etudid, + tag_annotation_pe): """Renvoie l'annotation PE dans la liste de ces annotations ; Cette annotation est reconnue par la présence d'un tag **PE** (cf. .get_preferences -> pe_tag_annotation_avis_latex). @@ -261,7 +267,11 @@ def get_annotation_PE(etudid, tag_annotation_pe): # ---------------------------------------------------------------------------------------- -def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ): +def str_from_syntheseJury(donnees_etudiant, + aggregat, + groupe, + tag_scodoc, + champ): """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, une valeur indiquée par un champ ; si champ est une liste, renvoie la liste des valeurs extraites. @@ -312,7 +322,8 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ) # ---------------------------------------------------------------------------------------- -def get_bilanParTag(donnees_etudiant, groupe="groupe"): +def get_bilanParTag(donnees_etudiant, + groupe="groupe"): """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans les données étudiants, ses résultats. result: chaine unicode @@ -383,7 +394,12 @@ def get_bilanParTag(donnees_etudiant, groupe="groupe"): # ---------------------------------------------------------------------------------------- def get_avis_poursuite_par_etudiant( - jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs + jury, + etudid, + template_latex, + tag_annotation_pe, + footer_latex, + prefs ): """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni. result: [ chaine unicode, chaine unicode ] @@ -444,7 +460,8 @@ def get_templates_from_distrib(template="avis"): # ---------------------------------------------------------------------------------------- -def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe): +def table_syntheseAnnotationPE(syntheseJury, + tag_annotation_pe): """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant""" sT = SeqGenTable() # le fichier excel à générer diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index a9899340..33dd376a 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -268,7 +268,9 @@ class JuryPE(object): # **************************************************************************************************************** # # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_in_jury(self, semBase, avec_meme_formation=False): + 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 """ @@ -314,7 +316,8 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_dans_semestres(self, semsListe): + 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)""" @@ -335,7 +338,8 @@ class JuryPE(object): return list(set(etudiants)) # suppression des doublons # ------------------------------------------------------------------------------------------------------------------ - def get_etudids_du_jury(self, ordre="aucun"): + 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é. @@ -359,7 +363,8 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def add_etudiants(self, etudid): + def add_etudiants(self, + etudid): """Ajoute un étudiant (via 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), @@ -449,7 +454,8 @@ class JuryPE(object): # print # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_reoriente_ou_demissionnaire(self, etudid): + 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 @@ -469,7 +475,8 @@ class JuryPE(object): return reponse # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_disparu(self, etudid): + 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 @@ -519,7 +526,8 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_dernier_semestre_id_valide_d_un_etudiant(self, etudid): + 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é """ @@ -542,7 +550,9 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_Fid_d_un_Si_valide_d_un_etudiant(self, etudid, nom_semestre): + 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. """ @@ -591,7 +601,8 @@ class JuryPE(object): self.add_semtags_in_jury(fid) # ------------------------------------------------------------------------------------------------------------------ - def add_semtags_in_jury(self, fid): + 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. """ @@ -638,7 +649,9 @@ class JuryPE(object): self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) # ---------------------------------------------------------------------------------------------------------------- - def get_formsemestreids_du_jury(self, etudids, liste_semestres="4S"): + def get_formsemestreids_du_jury(self, + etudids, + liste_semestres="4S"): """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() ). @@ -866,7 +879,8 @@ class JuryPE(object): tag ] = tagtable.get_resultatsEtud(tag, etudid) - def get_dateEntree(self, etudid): + def get_dateEntree(self, + etudid): """Renvoie l'année d'entrée de l'étudiant à l'IUT""" # etudinfo = self.ETUDINFO_DICT[etudid] semestres = self.get_semestresDUT_d_un_etudiant(etudid) @@ -876,7 +890,8 @@ class JuryPE(object): else: return "" - def get_parcoursIUT(self, etudid): + 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_semestresDUT_d_un_etudiant(etudid) @@ -896,7 +911,8 @@ class JuryPE(object): # **************************************************************************************************************** # # Méthodes d'affichage pour debug # **************************************************************************************************************** # - def str_etudiants_in_jury(self, delim=";"): + def str_etudiants_in_jury(self, + delim=";"): # En tete: entete = ["Id", "Nom", "Abandon", "Diplome"] for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]: @@ -958,7 +974,8 @@ class JuryPE(object): allTags = allTags.union(set(self.get_allTagForAggregat(nom))) return sorted(list(allTags)) if len(allTags) > 0 else [] - def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury + def table_syntheseJury(self, + mode="singlesheet"): # was str_syntheseJury """Table(s) du jury mode: singlesheet ou multiplesheet pour export excel """ @@ -984,9 +1001,9 @@ class JuryPE(object): rows=[], titles={"pas d'étudiants": "pas d'étudiants"}, html_sortable=True, - xls_sheet_name="but", + xls_sheet_name="dut", ) - sT.add_genTable("but", T) + sT.add_genTable("dut", T) return sT # Si des étudiants @@ -1120,7 +1137,8 @@ class JuryPE(object): # **************************************************************************************************************** # # ------------------------------------------------------------------------------------------------------------------ - def get_cache_etudInfo_d_un_etudiant(self, etudid): + 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 """ @@ -1133,7 +1151,8 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat: + def get_cache_notes_d_un_semestre(self, + formsemestre_id: int) -> NotesTableCompat: """Charge la table des notes d'un formsemestre""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) return res_sem.load_formsemestre_results(formsemestre) @@ -1141,7 +1160,9 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_semestresDUT_d_un_etudiant(self, etudid, semestre_id=None): + def get_semestresDUT_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]), @@ -1155,7 +1176,8 @@ class JuryPE(object): return sesSems # ********************************************** - def calcul_anneePromoDUT_d_un_etudiant(self, etudid) -> int: + def calcul_anneePromoDUT_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_semestresDUT_d_un_etudiant(etudid) @@ -1166,7 +1188,8 @@ class JuryPE(object): # ********************************************* # Fonctions d'affichage pour debug - def get_resultat_d_un_etudiant(self, etudid): + def get_resultat_d_un_etudiant(self, + etudid): chaine = "" for nom_sem in ["S1", "S2", "S3", "S4"]: semtagid = self.PARCOURSINFO_DICT[etudid][ @@ -1239,7 +1262,8 @@ def get_annee_diplome_semestre(sem) -> int: # ---------------------------------------------------------------------------------- -def get_cosemestres_diplomants(semBase, avec_meme_formation=False): +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 : diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index 8384551c..440855b2 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -78,7 +78,9 @@ class SemestreTag(pe_tagtable.TableTag): # ----------------------------------------------------------------------------- # Fonctions d'initialisation # ----------------------------------------------------------------------------- - def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable + 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 """ @@ -194,7 +196,9 @@ class SemestreTag(pe_tagtable.TableTag): return tagdict # ----------------------------------------------------------------------------- - def comp_MoyennesTag(self, tag, force=False) -> list: + 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. @@ -232,7 +236,10 @@ class SemestreTag(pe_tagtable.TableTag): ] # ----------------------------------------------------------------------------- - def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2): + 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. La note et le coeff sont extraits : 1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre, @@ -313,14 +320,17 @@ class SemestreTag(pe_tagtable.TableTag): return (note, coeff_norm) # ----------------------------------------------------------------------------- - def get_ue_capitalisees(self, etudid) -> list[dict]: + def get_ue_capitalisees(self, + etudid) -> list[dict]: """Renvoie la liste des capitalisation effectivement capitalisées par un étudiant""" if etudid in self.nt.validations.ue_capitalisees.index: return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") return [] # ----------------------------------------------------------------------------- - def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid): + 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. @@ -343,7 +353,10 @@ class SemestreTag(pe_tagtable.TableTag): # ----------------------------------------------------------------------------- # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug # ----------------------------------------------------------------------------- - def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"): + def str_detail_resultat_d_un_tag(self, + tag, + etudid=None, + delim=";"): """Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag : rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés. Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés. @@ -463,7 +476,8 @@ class SemestreTag(pe_tagtable.TableTag): # ********************************************* -def comp_coeff_pond(coeffs, ponderations): +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] @@ -499,7 +513,9 @@ def get_moduleimpl(modimpl_id) -> dict: # ********************************************** -def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: +def get_moy_ue_from_nt(nt, + etudid, + modimpl_id) -> float: """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id """ diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index f8c35bd4..a0ded4f5 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -51,15 +51,19 @@ class SetTag(pe_tagtable.TableTag): """ # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, parcours): + def __init__(self, + nom_combinaison, + parcours): pe_tagtable.TableTag.__init__(self, nom=nom_combinaison) self.combinaison = nom_combinaison self.parcours = parcours # Le groupe de semestres/parcours à aggréger # ------------------------------------------------------------------------------------------- - def set_Etudiants( - self, etudiants: list[dict], juryPEDict, etudInfoDict, nom_sem_final=None - ): + def set_Etudiants(self, + etudiants: list[dict], + juryPEDict, + etudInfoDict, + nom_sem_final=None): """Détermine la liste des étudiants à prendre en compte, en partant de la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide.""" if nom_sem_final: @@ -94,7 +98,8 @@ class SetTag(pe_tagtable.TableTag): ) # --------------------------------------------------------------------------------------------- - def set_SemTagDict(self, SemTagDict): + def set_SemTagDict(self, + SemTagDict): """Mémorise les semtag nécessaires au jury.""" self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} if PE_DEBUG >= 1: @@ -152,7 +157,9 @@ class SetTag(pe_tagtable.TableTag): self.tagdict[tag][mod] = semtag.tagdict[tag][mod] # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): + def get_NotesEtCoeffsSetTagEtudiant(self, + tag, + etudid): """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) avec notes et coeffs deux listes""" lesSemsDeLEtudiant = [ @@ -172,7 +179,9 @@ class SetTag(pe_tagtable.TableTag): return (notes, coeffs) # ------------------------------------------------------------------------------------------------------------------- - def comp_MoyennesSetTag(self, tag, force=False): + def comp_MoyennesSetTag(self, + tag, + force=False): """Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération appliqué dans cette moyenne. @@ -203,19 +212,25 @@ class SetTag(pe_tagtable.TableTag): class SetTagInterClasse(pe_tagtable.TableTag): - """Récupère les moyennes de SetTag aggrégant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2 + """Récupère les moyennes de SetTag aggrégeant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2 pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo nom_combinaison = 'S1' ou '1A' """ # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, diplome): + def __init__(self, + nom_combinaison, + diplome): pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") self.combinaison = nom_combinaison self.parcoursDict = {} # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): + def set_Etudiants(self, + etudiants, + juryPEDict, + etudInfoDict, + nom_sem_final=None): """Détermine la liste des étudiants à prendre en compte, en partant de la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. Renvoie le nombre d'étudiants effectivement inscrits.""" @@ -237,7 +252,8 @@ class SetTagInterClasse(pe_tagtable.TableTag): ) # --------------------------------------------------------------------------------------------- - def set_SetTagDict(self, SetTagDict): + def set_SetTagDict(self, + SetTagDict): """Mémorise les settag nécessaires au jury.""" self.SetTagDict = { fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None @@ -285,7 +301,9 @@ class SetTagInterClasse(pe_tagtable.TableTag): return sorted(list(set(ensemble))) # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): + def get_NotesEtCoeffsSetTagEtudiant(self, + tag, + etudid): """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) avec notes et coeffs deux listes""" leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] @@ -297,7 +315,9 @@ class SetTagInterClasse(pe_tagtable.TableTag): return (note, coeff) # ------------------------------------------------------------------------------------------------------------------- - def get_MoyennesSetTag(self, tag, force=False): + def get_MoyennesSetTag(self, + tag, + force=False): """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération appliqué dans cette moyenne. diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index 49bf22cc..d974b857 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -77,7 +77,9 @@ class TableTag(object): # ***************************************************************************************************************** # ----------------------------------------------------------------------------------------------------------- - def get_moy_from_resultats(self, tag, etudid): + 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] @@ -86,7 +88,9 @@ class TableTag(object): ) # ----------------------------------------------------------------------------------------------------------- - def get_rang_from_resultats(self, tag, etudid): + def get_rang_from_resultats(self, + tag, + etudid): """Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats""" return ( self.rangs[tag][etudid] @@ -95,7 +99,9 @@ class TableTag(object): ) # ----------------------------------------------------------------------------------------------------------- - def get_coeff_from_resultats(self, tag, etudid): + def get_coeff_from_resultats(self, + tag, + etudid): """Renvoie la somme des coeffs de pondération normalisée utilisés dans le calcul de la moyenne à un tag d'un étudiant au regard du format de self.resultats. """ @@ -117,15 +123,18 @@ class TableTag(object): return len(self.inscrlist) # ----------------------------------------------------------------------------------------------------------- - def get_moy_from_stats(self, tag): + def get_moy_from_stats(self, + tag): """Renvoie la moyenne des notes calculées pour d'un tag donné""" return self.statistiques[tag][0] if tag in self.statistiques else None - def get_min_from_stats(self, tag): + def get_min_from_stats(self, + tag): """Renvoie la plus basse des notes calculées pour d'un tag donné""" return self.statistiques[tag][1] if tag in self.statistiques else None - def get_max_from_stats(self, tag): + def get_max_from_stats(self, + tag): """Renvoie la plus haute des notes calculées pour d'un tag donné""" return self.statistiques[tag][2] if tag in self.statistiques else None @@ -142,7 +151,9 @@ class TableTag(object): "min", ) - def get_resultatsEtud(self, tag, etudid): + def get_resultatsEtud(self, + tag, + etudid): """Renvoie un tuple (note, coeff, rang, nb_inscrit, moy, min, max) synthétisant les résultats d'un étudiant à un tag donné. None sinon""" return ( @@ -164,7 +175,9 @@ class TableTag(object): # ***************************************************************************************************************** # ----------------------------------------------------------------------------------------------------------- - def add_moyennesTag(self, tag, listMoyEtCoeff) -> bool: + def add_moyennesTag(self, + tag, + listMoyEtCoeff) -> bool: """ Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats avec calcul du rang @@ -197,7 +210,8 @@ class TableTag(object): # Méthodes dévolues aux calculs de statistiques (min, max, moy) sur chaque moyenne taguée # ***************************************************************************************************************** - def comp_stats_d_un_tag(self, tag): + def comp_stats_d_un_tag(self, + tag): """ Calcule la moyenne generale, le min, le max pour un tag donné, en ne prenant en compte que les moyennes significatives. Mémorise le resultat dans @@ -221,7 +235,10 @@ class TableTag(object): # ************************************************************************ # Méthodes dévolues aux affichages -> a revoir # ************************************************************************ - def str_resTag_d_un_etudiant(self, tag, etudid, delim=";"): + def str_resTag_d_un_etudiant(self, + tag, + etudid, + delim=";"): """Renvoie une chaine de caractères (valable pour un csv) décrivant la moyenne et le rang d'un étudiant, pour un tag donné ; """ @@ -236,14 +253,20 @@ class TableTag(object): ) return moystr - def str_res_d_un_etudiant(self, etudid, delim=";"): + def str_res_d_un_etudiant(self, + etudid, + delim=";"): """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique).""" return delim.join( [self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()] ) # ----------------------------------------------------------------------- - def str_moytag(cls, moyenne, rang, nbinscrit, delim=";"): + def str_moytag(cls, + moyenne, + rang, + nbinscrit, + delim=";"): """Renvoie une chaine de caractères représentant une moyenne (float ou string) et un rang pour différents formats d'affichage : HTML, debug ligne de commande, csv""" moystr = ( @@ -256,7 +279,9 @@ class TableTag(object): str_moytag = classmethod(str_moytag) # ----------------------------------------------------------------------- - def str_tagtable(self, delim=";", decimal_sep=","): + 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"] for tag in self.get_all_tags(): @@ -288,7 +313,9 @@ class TableTag(object): # ********************************************* -def moyenne_ponderee_terme_a_terme(notes, coefs=None, force=False): +def moyenne_ponderee_terme_a_terme(notes, + coefs=None, + force=False): """ Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération. Renvoie le résultat sous forme d'un tuple (moy, somme_coeff) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index ead3a2d1..133a0788 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -76,7 +76,8 @@ PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" # ---------------------------------------------------------------------------------------- -def print_semestres_description(sems, avec_affichage_debug=False): +def print_semestres_description(sems, + avec_affichage_debug=False): """Dediee a l'affichage d'un semestre pour debug du module""" def chaine_semestre(sem): @@ -119,7 +120,6 @@ def calcul_age(born): ) -# ---------------------------------------------------------------------------------------- def remove_accents(input_unicode_str): """Supprime les accents d'une chaine unicode""" nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) @@ -166,7 +166,10 @@ def list_directory_filenames(path): return R -def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): +def add_local_file_to_zip(zipfile, + ziproot, + pathname, + path_in_zip): """Read pathname server file and add content to zip under path_in_zip""" rooted_path_in_zip = os.path.join(ziproot, path_in_zip) zipfile.write(filename=pathname, arcname=rooted_path_in_zip) @@ -174,7 +177,8 @@ def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): # zipfile.writestr(rooted_path_in_zip, data) -def add_refs_to_register(register, directory): +def add_refs_to_register(register, + directory): """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme filename => pathname """ @@ -184,7 +188,8 @@ def add_refs_to_register(register, directory): register[filename] = pathname -def add_pe_stuff_to_zip(zipfile, ziproot): +def add_pe_stuff_to_zip(zipfile, + ziproot): """Add auxiliary files to (already opened) zip Put all local files found under config/doc_poursuites_etudes/local and config/doc_poursuites_etudes/distrib From 898270d2f03abfe8f88f0e48a380485a76b08394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 06:19:49 +0100 Subject: [PATCH 03/23] =?UTF-8?q?R=C3=A9active=20la=20d=C3=A9tection=20des?= =?UTF-8?q?=20=C3=A9tudiants=20=C3=A0=20prendre=20en=20compte=20dans=20un?= =?UTF-8?q?=20jury=20BUT=20+=20D=C3=A9sactive=20les=20avis=20LaTeX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jurype.py | 205 ++++++++++++++++++++++++++---------------- app/pe/pe_tagtable.py | 2 +- app/pe/pe_view.py | 101 +++++++++++---------- 3 files changed, 183 insertions(+), 125 deletions(-) diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 33dd376a..aaaa61b8 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -78,20 +78,24 @@ def comp_nom_semestre_dans_parcours(sem): # ---------------------------------------------------------------------------------------- class JuryPE(object): - """Classe memorisant toutes les informations necessaires pour etablir un jury de PE. Modele - base sur NotesTable + """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. + Modèle basé sur NotesTable. - Attributs : - diplome : l'annee d'obtention du diplome DUT et du jury de PE (generalement fevrier 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', '', }} - Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue - et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif + Attributs : + * diplome : l'annee d'obtention du diplome BUT et du jury de PE (generalement fevrier 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', '', }}`` + + Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue + et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif """ # 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"], @@ -105,6 +109,12 @@ class JuryPE(object): "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, @@ -117,12 +127,6 @@ class JuryPE(object): "affichage_court": "S4", "affichage_long": "Semestre 4", }, - "1A": { - "aggregat": ["S1", "S2"], - "ordre": 3, - "affichage_court": "1A", - "affichage_long": "1ère année", - }, "2A": { "aggregat": ["S3", "S4"], "ordre": 6, @@ -133,16 +137,52 @@ class JuryPE(object): "aggregat": ["S1", "S2", "S3"], "ordre": 7, "affichage_court": "S1+S2+S3", - "affichage_long": "DUT du semestre 1 au semestre 3", + "affichage_long": "BUT du semestre 1 au semestre 3", }, "4S": { "aggregat": ["S1", "S2", "S3", "S4"], "ordre": 8, - "affichage_court": "DUT", - "affichage_long": "DUT (tout semestre inclus)", + "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, semBase): """ @@ -325,7 +365,7 @@ class JuryPE(object): for sem in semsListe: # pour chacun des semestres de la liste nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"]) etudiantsDuSemestre = ( - nt.get_etudids() + [ins.etudid for ins in nt.formsemestre.inscriptions] # nt.get_etudids() ) # identification des etudiants du semestre if pe_tools.PE_DEBUG: @@ -341,7 +381,7 @@ class JuryPE(object): 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 + 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 """ @@ -365,13 +405,18 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ def add_etudiants(self, etudid): - """Ajoute un étudiant (via son etudid) au dictionnaire de synthèse jurydict. + """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 => 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) + * 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: @@ -391,7 +436,7 @@ class JuryPE(object): # Sa date prévisionnelle de diplome self.PARCOURSINFO_DICT[etudid][ "diplome" - ] = self.calcul_anneePromoDUT_d_un_etudiant(etudid) + ] = 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="" @@ -412,11 +457,10 @@ class JuryPE(object): # 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.PARCOURS["4S"][ - "aggregat" - ] # Recherche du formsemestre_id de son Si valide (ou a défaut en cours) + 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.PARCOURS["4S"]["aggregat"]): + 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: @@ -424,13 +468,11 @@ class JuryPE(object): # 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 ["1A", "2A", "3S", "4S"]: - lesSemsDuParcours = JuryPE.PARCOURS[parcours][ - "aggregat" - ] # les semestres du parcours : par ex. ['S1', 'S2', 'S3'] + 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.PARCOURS["4S"]["aggregat"].index(nom_sem) + JuryPE.TOUS_LES_SEMESTRES.index(nom_sem) ] for nom_sem in lesSemsDuParcours # par ex. ['SEM4532', 'SEM567', ...] ] @@ -482,7 +524,7 @@ class JuryPE(object): 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_semestresDUT_d_un_etudiant( + sessems = self.get_semestresBUT_d_un_etudiant( etudid ) # les semestres de l'étudiant sonDernierSidValide = self.get_dernier_semestre_id_valide_d_un_etudiant(etudid) @@ -556,8 +598,8 @@ class JuryPE(object): """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.PARCOURS["4S"]["aggregat"].index(nom_semestre) + 1 - sesSi = self.get_semestresDUT_d_un_etudiant( + 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 @@ -590,7 +632,8 @@ class JuryPE(object): 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=["S1", "S2", "S3", "S4"] + self.get_etudids_du_jury(), + liste_semestres=JuryPE.TOUS_LES_SEMESTRES ) for i, fid in enumerate(lesFids): if pe_tools.PE_DEBUG: @@ -634,7 +677,7 @@ class JuryPE(object): " - %d étudiants classés " % (nbinscrit) + ": " + ",".join( - [etudid for etudid in self.semTagDict[fid].get_etudids()] + [str(etudid) for etudid in self.semTagDict[fid].get_etudids()] ) ) if lesEtudidsManquants: @@ -642,7 +685,7 @@ class JuryPE(object): " - dont %d étudiants manquants ajoutés aux données du jury" % (len(lesEtudidsManquants)) + ": " - + ", ".join(lesEtudidsManquants) + + ", ".join([str(etudid) for etudid in lesEtudidsManquants]) ) pe_tools.pe_print(" - Export csv") filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv" @@ -651,7 +694,7 @@ class JuryPE(object): # ---------------------------------------------------------------------------------------------------------------- def get_formsemestreids_du_jury(self, etudids, - liste_semestres="4S"): + 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() ). @@ -736,8 +779,8 @@ class JuryPE(object): # '3S' : ['S1', 'S2', 'S3'], '4S' : ['S1', 'S2', 'S3', 'S4'] } # ---> sur 2 parcours DUT (cas S3 fini, cas S4 fini) - combinaisons = ["1A", "2A", "3S", "4S"] - for i, nom in enumerate(combinaisons): + + for i, nom in enumerate(JuryPE.TOUS_LES_AGGREGATS): parcours = JuryPE.PARCOURS[nom][ "aggregat" ] # La liste des noms de semestres (S1, S2, ...) impliqués dans l'aggrégat @@ -806,7 +849,7 @@ class JuryPE(object): pe_tools.pe_print( "%d) %s avec interclassement sur la promo" % (i + 1, nom) ) - if nom in ["S1", "S2", "S3", "S4"]: + if nom in JuryPE.TOUS_LES_SEMESTRES: settag.set_SetTagDict(self.semTagDict) else: # cas des aggrégats settag.set_SetTagDict(self.setTagDict[nom]) @@ -859,7 +902,7 @@ class JuryPE(object): if ( self.PARCOURSINFO_DICT[etudid][nom] != None ): # Un parcours valide existe - if nom in ["S1", "S2", "S3", "S4"]: + if nom in JuryPE.TOUS_LES_SEMESTRES: tagtable = self.semTagDict[self.PARCOURSINFO_DICT[etudid][nom]] else: tagtable = self.setTagDict[nom][ @@ -883,7 +926,7 @@ class JuryPE(object): etudid): """Renvoie l'année d'entrée de l'étudiant à l'IUT""" # etudinfo = self.ETUDINFO_DICT[etudid] - semestres = self.get_semestresDUT_d_un_etudiant(etudid) + semestres = self.get_semestresBUT_d_un_etudiant(etudid) if semestres: # le 1er sem à l'IUT return semestres[0]["annee_debut"] @@ -894,7 +937,7 @@ class JuryPE(object): etudid): """Renvoie une liste d'infos sur les semestres du parcours d'un étudiant""" # etudinfo = self.ETUDINFO_DICT[etudid] - sems = self.get_semestresDUT_d_un_etudiant(etudid) + sems = self.get_semestresBUT_d_un_etudiant(etudid) infos = [] for sem in sems: @@ -915,7 +958,7 @@ class JuryPE(object): delim=";"): # En tete: entete = ["Id", "Nom", "Abandon", "Diplome"] - for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]: + for nom_sem in JuryPE.TOUS_LES_PARCOURS: entete += [nom_sem, "descr"] chaine = delim.join(entete) + "\n" @@ -930,8 +973,8 @@ class JuryPE(object): str(donnees["diplome"]), ] - # les semestres - for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]: + # les semestres et les aggrégats + for nom_sem in JuryPE.TOUS_LES_PARCOURS: table = ( self.semTagDict[donnees[nom_sem]].nom if donnees[nom_sem] in self.semTagDict @@ -968,9 +1011,10 @@ class JuryPE(object): return list(taglist) def get_allTagInSyntheseJury(self): - """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag""" + """Extrait tous les tags du dictionnaire syntheseJury trié par + ordre alphabétique. [] si aucun tag""" allTags = set() - for nom in JuryPE.PARCOURS.keys(): + for nom in JuryPE.TOUS_LES_PARCOURS: allTags = allTags.union(set(self.get_allTagForAggregat(nom))) return sorted(list(allTags)) if len(allTags) > 0 else [] @@ -1160,7 +1204,7 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_semestresDUT_d_un_etudiant(self, + def get_semestresBUT_d_un_etudiant(self, etudid, semestre_id=None): """Renvoie la liste des semestres DUT d'un étudiant @@ -1169,18 +1213,19 @@ class JuryPE(object): 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"] <= 4] + 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 calcul_anneePromoDUT_d_un_etudiant(self, + 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_semestresDUT_d_un_etudiant(etudid) + semestres = self.get_semestresBUT_d_un_etudiant(etudid) if semestres: return max([get_annee_diplome_semestre(sem) for sem in semestres]) else: @@ -1191,7 +1236,7 @@ class JuryPE(object): def get_resultat_d_un_etudiant(self, etudid): chaine = "" - for nom_sem in ["S1", "S2", "S3", "S4"]: + for nom_sem in JuryPE.TOUS_LES_SEMESTRES: semtagid = self.PARCOURSINFO_DICT[etudid][ nom_sem ] # le formsemestre_id du semestre taggué de l'étudiant @@ -1230,27 +1275,37 @@ class JuryPE(object): # ---------------------------------------------------------------------------------------- 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 DUT, - predit l'annee à laquelle sera remis le diplome DUT des etudiants scolarisés dans le semestre + + 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. - Hypothese : les semestres de 1ere partie d'annee universitaire (comme des S1 ou des S3) s'etalent - sur deux annees civiles - contrairement au semestre de seconde partie d'annee universitaire (comme - des S2 ou des S4). + + 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 : - > S4 debutant en 2016 finissant en 2016 => diplome en 2016 - > S3 debutant en 2015 et finissant en 2016 => diplome en 2016 - > S3 (decale) debutant en 2015 et finissant en 2015 => diplome en 2016 - La regle 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 desactive un increment a prendre en compte en cas de semestre decale + * 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"] <= 4 - ): # Si le semestre est un semestre DUT => problème si formation DUT en 1 an ?? - nbreSemRestant = 4 - sem["semestre_id"] + 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 diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index d974b857..994d3850 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -291,7 +291,7 @@ class TableTag(object): for etudid in self.identdict: descr = delim.join( [ - etudid, + str(etudid), self.identdict[etudid]["nom"], self.identdict[etudid]["prenom"], ] diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 7a49e721..ded41a91 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -106,39 +106,40 @@ def pe_view_sem_recap( # (chaines unicodes, html non quoté) template_latex = "" # template fourni via le formulaire Web - if avis_tmpl_file: - try: - template_latex = avis_tmpl_file.read().decode("utf-8") - except UnicodeDecodeError as e: - raise ScoValueError( - "Données (template) invalides (caractères non UTF8 ?)" - ) from e - else: - # template indiqué dans préférences ScoDoc ? - template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( - formsemestre_id, champ="pe_avis_latex_tmpl" - ) + if False: + if avis_tmpl_file: + try: + template_latex = avis_tmpl_file.read().decode("utf-8") + except UnicodeDecodeError as e: + raise ScoValueError( + "Données (template) invalides (caractères non UTF8 ?)" + ) from e + else: + # template indiqué dans préférences ScoDoc ? + template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( + formsemestre_id, champ="pe_avis_latex_tmpl" + ) - template_latex = template_latex.strip() - if not template_latex: - # pas de preference pour le template: utilise fichier du serveur - template_latex = pe_avislatex.get_templates_from_distrib("avis") + template_latex = template_latex.strip() + if not template_latex: + # pas de preference pour le template: utilise fichier du serveur + template_latex = pe_avislatex.get_templates_from_distrib("avis") - # Footer: - footer_latex = "" - # template fourni via le formulaire Web - if footer_tmpl_file: - footer_latex = footer_tmpl_file.read().decode("utf-8") - else: - footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( - formsemestre_id, champ="pe_avis_latex_footer" - ) - footer_latex = footer_latex.strip() - if not footer_latex: - # pas de preference pour le footer: utilise fichier du serveur - footer_latex = pe_avislatex.get_templates_from_distrib( - "footer" - ) # fallback: footer vides + # Footer: + footer_latex = "" + # template fourni via le formulaire Web + if footer_tmpl_file: + footer_latex = footer_tmpl_file.read().decode("utf-8") + else: + footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( + formsemestre_id, champ="pe_avis_latex_footer" + ) + footer_latex = footer_latex.strip() + if not footer_latex: + # pas de preference pour le footer: utilise fichier du serveur + footer_latex = pe_avislatex.get_templates_from_distrib( + "footer" + ) # fallback: footer vides tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference( formsemestre_id, champ="pe_tag_annotation_avis_latex" @@ -151,27 +152,29 @@ def pe_view_sem_recap( jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel() ) - latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex - for etudid in etudids: - [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant( - jury, - etudid, - template_latex, - tag_annotation_pe, - footer_latex, - prefs, + if False: + latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex + for etudid in etudids: + [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant( + jury, + etudid, + template_latex, + tag_annotation_pe, + footer_latex, + prefs, + ) + jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex) + latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico + + # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous + doc_latex = "\n% -----\n".join( + ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())] ) - jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex) - latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico + jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex) - # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous - doc_latex = "\n% -----\n".join( - ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())] - ) - jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex) + # Ajoute image, LaTeX class file(s) and modeles + pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP) - # Ajoute image, LaTeX class file(s) and modeles - pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP) data = jury.get_zipped_data() return send_file( From 2f81ce8ac2049d88807954c80a913dcbad76296f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 06:35:27 +0100 Subject: [PATCH 04/23] =?UTF-8?q?Am=C3=A9liore=20rendu=20visuel=20min/moy/?= =?UTF-8?q?max=20dans=20excel=20:=20None/None/None=20->=20-/-/-?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jurype.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index aaaa61b8..cc3a799d 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -1045,9 +1045,9 @@ class JuryPE(object): rows=[], titles={"pas d'étudiants": "pas d'étudiants"}, html_sortable=True, - xls_sheet_name="dut", + xls_sheet_name="but", ) - sT.add_genTable("dut", T) + sT.add_genTable("but", T) return sT # Si des étudiants @@ -1069,24 +1069,20 @@ 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 = sorted( - aggregats, key=lambda t: JuryPE.PARCOURS[t]["ordre"] - ) # Tri des aggrégats + # aggregats = sorted( + # aggregats, key=lambda t: JuryPE.PARCOURS[t]["ordre"] + # ) # Tri des aggrégats if mode == "multiplesheet": allSheets = ( self.get_allTagInSyntheseJury() ) # tous les tags de syntheseJuryDict allSheets = sorted(allSheets) # Tri des tags par ordre alphabétique - for ( - sem - ) in aggregats: # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] + for sem in JuryPE.TOUS_LES_PARCOURS: entete.extend(["%s %s" % (sem, champ) for champ in champs]) else: # "singlesheet" allSheets = ["singlesheet"] - for ( - sem - ) in aggregats: # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] + for sem in JuryPE.TOUS_LES_PARCOURS: # JuryPE.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] @@ -1145,19 +1141,21 @@ class JuryPE(object): ) row[champ + "note"] = scu.fmt_note(resgroupe[0]) row[champ + "class groupe"] = "%s / %s" % ( - resgroupe[2], - resgroupe[3], + resgroupe[2] if resgroupe[2] else "-", + resgroupe[3] if resgroupe[3] else "-" ) row[champ + "class promo"] = "%s / %s" % ( - respromo[2], - respromo[3], + respromo[2] if respromo[2] else "-", + respromo[3] if respromo[3] else "-" ) row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple( - scu.fmt_note(x) - for x in (resgroupe[6], resgroupe[4], resgroupe[5]) + (scu.fmt_note(x) if x is not None else "-") + for x in (resgroupe[6], + resgroupe[4], + resgroupe[5]) ) row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple( - scu.fmt_note(x) + (scu.fmt_note(x) if x is not None else "-") for x in (respromo[6], respromo[4], respromo[5]) ) rows.append(row) From 6d16927db91a826f4d4da2a3d1499d03c2e17063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 08:54:20 +0100 Subject: [PATCH 05/23] =?UTF-8?q?D=C3=A9sactive=20les=20moyennes=20par=20t?= =?UTF-8?q?ag,=20autres=20que=20le=20tag=20but?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_semestretag.py | 6 +++--- app/pe/pe_settag.py | 2 +- app/pe/pe_view.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index 440855b2..977bc477 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -139,9 +139,9 @@ class SemestreTag(pe_tagtable.TableTag): # 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("dut", self.get_moyennes_DUT()) + self.add_moyennesTag("but", self.get_moyennes_DUT()) self.taglist = sorted( - list(self.tagdict.keys()) + ["dut"] + list(self.tagdict.keys()) + ["but"] ) # actualise la liste des tags # ----------------------------------------------------------------------------- @@ -168,7 +168,7 @@ class SemestreTag(pe_tagtable.TableTag): """ tagdict = {} - for modimpl in self.modimpls: + 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) diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index a0ded4f5..4431bcb8 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -150,7 +150,7 @@ class SetTag(pe_tagtable.TableTag): self.tagdict = {} for semtag in self.SemTagDict.values(): for tag in semtag.get_all_tags(): - if tag != "dut": + if tag != "but": if tag not in self.tagdict: self.tagdict[tag] = {} for mod in semtag.tagdict[tag]: diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index ded41a91..a538b7df 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -96,7 +96,7 @@ def pe_view_sem_recap( return _pe_view_sem_recap_form(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) - semBase = sco_formsemestre.get_formsemestre(formsemestre_id) + semBase = sco_formsemestre.get_formsemestre(formsemestre_id) # FormSemestre.get_formsemestre() jury = pe_jurype.JuryPE(semBase) # Ajout avis LaTeX au même zip: From 01d0f7d6517822e46e81a6ba58ebeba580cea4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 09:21:02 +0100 Subject: [PATCH 06/23] =?UTF-8?q?Erreur=20si=20le=20jury=20demand=C3=A9=20?= =?UTF-8?q?correspond=20=C3=A0=20un=20DUT=20ou=20tout=20formation=20non=20?= =?UTF-8?q?APC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jurype.py | 8 ++++++-- app/pe/pe_view.py | 9 ++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index cc3a799d..9cae9efe 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -184,14 +184,18 @@ class JuryPE(object): TOUS_LES_PARCOURS = list(PARCOURS.keys()) # ------------------------------------------------------------------------------------------------------------------ - def __init__(self, semBase): + def __init__(self, + sem_base: FormSemestre, + semBase # CB: à supprimer à long terme + ): """ 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, 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. Args: - semBase: le dictionnaire sem donnant la base du jury + sem_base: le FormSemestre donnant le semestre à la base du jury PE + semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term) meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, si False, permet des programmes differents """ diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index a538b7df..39b810c9 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -36,6 +36,8 @@ """ from flask import send_file, request + +from app.models import FormSemestre from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu @@ -96,9 +98,14 @@ def pe_view_sem_recap( return _pe_view_sem_recap_form(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) + sem_base = FormSemestre.get_formsemestre(formsemestre_id) + if not sem_base.formation.is_apc(): + raise ScoValueError("Le module de Poursuites d'Etudes avec Scodoc 9 n'est disponible que pour des formations BUT") + + # TODO: CB: A supprimer à long terme semBase = sco_formsemestre.get_formsemestre(formsemestre_id) # FormSemestre.get_formsemestre() - jury = pe_jurype.JuryPE(semBase) + jury = pe_jurype.JuryPE(sem_base, semBase) # Ajout avis LaTeX au même zip: etudids = list(jury.syntheseJury.keys()) From df20372abb6a0fa23e552bb4ebe057b717429305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 16 Jan 2024 15:51:22 +0100 Subject: [PATCH 07/23] Reformattage code avec Black --- app/pe/pe_avislatex.py | 37 ++++--------- app/pe/pe_jurype.py | 109 ++++++++++++++++----------------------- app/pe/pe_semestretag.py | 34 ++++-------- app/pe/pe_settag.py | 44 +++++----------- app/pe/pe_tagtable.py | 55 +++++--------------- app/pe/pe_view.py | 8 ++- 6 files changed, 95 insertions(+), 192 deletions(-) diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py index b5e6fb07..3c498f47 100644 --- a/app/pe/pe_avislatex.py +++ b/app/pe/pe_avislatex.py @@ -67,8 +67,7 @@ def get_code_latex_from_modele(fichier): # ---------------------------------------------------------------------------------------- -def get_code_latex_from_scodoc_preference(formsemestre_id, - champ="pe_avis_latex_tmpl"): +def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"): """ Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX et s'assure qu'il est renvoyé au format unicode @@ -95,9 +94,7 @@ def get_tags_latex(code_latex): return [] -def comp_latex_parcourstimeline(etudiant, - promo, - taille=17): +def comp_latex_parcourstimeline(etudiant, promo, taille=17): """Interprète un tag dans un avis latex **parcourstimeline** et génère le code latex permettant de retracer le parcours d'un étudiant sous la forme d'une frise temporelle. @@ -159,11 +156,9 @@ def interprete_tag_latex(tag): # ---------------------------------------------------------------------------------------- -def get_code_latex_avis_etudiant(donnees_etudiant, - un_avis_latex, - annotationPE, - footer_latex, - prefs): +def get_code_latex_avis_etudiant( + donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs +): """ Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un @@ -225,8 +220,7 @@ def get_code_latex_avis_etudiant(donnees_etudiant, # ---------------------------------------------------------------------------------------- -def get_annotation_PE(etudid, - tag_annotation_pe): +def get_annotation_PE(etudid, tag_annotation_pe): """Renvoie l'annotation PE dans la liste de ces annotations ; Cette annotation est reconnue par la présence d'un tag **PE** (cf. .get_preferences -> pe_tag_annotation_avis_latex). @@ -267,11 +261,7 @@ def get_annotation_PE(etudid, # ---------------------------------------------------------------------------------------- -def str_from_syntheseJury(donnees_etudiant, - aggregat, - groupe, - tag_scodoc, - champ): +def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ): """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, une valeur indiquée par un champ ; si champ est une liste, renvoie la liste des valeurs extraites. @@ -322,8 +312,7 @@ def str_from_syntheseJury(donnees_etudiant, # ---------------------------------------------------------------------------------------- -def get_bilanParTag(donnees_etudiant, - groupe="groupe"): +def get_bilanParTag(donnees_etudiant, groupe="groupe"): """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans les données étudiants, ses résultats. result: chaine unicode @@ -394,12 +383,7 @@ def get_bilanParTag(donnees_etudiant, # ---------------------------------------------------------------------------------------- def get_avis_poursuite_par_etudiant( - jury, - etudid, - template_latex, - tag_annotation_pe, - footer_latex, - prefs + jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs ): """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni. result: [ chaine unicode, chaine unicode ] @@ -460,8 +444,7 @@ def get_templates_from_distrib(template="avis"): # ---------------------------------------------------------------------------------------- -def table_syntheseAnnotationPE(syntheseJury, - tag_annotation_pe): +def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe): """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant""" sT = SeqGenTable() # le fichier excel à générer diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 9cae9efe..d7939288 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -175,19 +175,17 @@ class JuryPE(object): "affichage_court": "BUT", "affichage_long": "BUT (tout semestre inclus)", }, - } - AGGREGAT_DIPLOMANT = "6S" # aggrégat correspondant à la totalité des notes pour le diplôme + 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, sem_base: FormSemestre, semBase): # CB: à supprimer à long terme """ 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, @@ -312,9 +310,7 @@ class JuryPE(object): # **************************************************************************************************************** # # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_in_jury(self, - semBase, - avec_meme_formation=False): + 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 """ @@ -360,17 +356,16 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_dans_semestres(self, - semsListe): + 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 + 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( @@ -382,8 +377,7 @@ class JuryPE(object): return list(set(etudiants)) # suppression des doublons # ------------------------------------------------------------------------------------------------------------------ - def get_etudids_du_jury(self, - ordre="aucun"): + 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é. @@ -407,8 +401,7 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def add_etudiants(self, - etudid): + 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), @@ -473,7 +466,9 @@ class JuryPE(object): # 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'] + lesSemsDuParcours = JuryPE.PARCOURS[parcours][ + "aggregat" + ] # les semestres du parcours : par ex. ['S1', 'S2', 'S3'] lesFidsValidantDuParcours = [ sesFormsemestre_idValidants[ JuryPE.TOUS_LES_SEMESTRES.index(nom_sem) @@ -500,8 +495,7 @@ class JuryPE(object): # print # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_reoriente_ou_demissionnaire(self, - etudid): + 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 @@ -521,8 +515,7 @@ class JuryPE(object): return reponse # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_disparu(self, - etudid): + 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 @@ -572,8 +565,7 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_dernier_semestre_id_valide_d_un_etudiant(self, - etudid): + 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é """ @@ -596,9 +588,7 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_Fid_d_un_Si_valide_d_un_etudiant(self, - etudid, - nom_semestre): + 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. """ @@ -636,8 +626,7 @@ class JuryPE(object): 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 + self.get_etudids_du_jury(), liste_semestres=JuryPE.TOUS_LES_SEMESTRES ) for i, fid in enumerate(lesFids): if pe_tools.PE_DEBUG: @@ -648,8 +637,7 @@ class JuryPE(object): self.add_semtags_in_jury(fid) # ------------------------------------------------------------------------------------------------------------------ - def add_semtags_in_jury(self, - fid): + 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. """ @@ -696,9 +684,7 @@ class JuryPE(object): self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) # ---------------------------------------------------------------------------------------------------------------- - def get_formsemestreids_du_jury(self, - etudids, - liste_semestres="6S"): + 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() ). @@ -926,8 +912,7 @@ class JuryPE(object): tag ] = tagtable.get_resultatsEtud(tag, etudid) - def get_dateEntree(self, - etudid): + def get_dateEntree(self, etudid): """Renvoie l'année d'entrée de l'étudiant à l'IUT""" # etudinfo = self.ETUDINFO_DICT[etudid] semestres = self.get_semestresBUT_d_un_etudiant(etudid) @@ -937,8 +922,7 @@ class JuryPE(object): else: return "" - def get_parcoursIUT(self, - etudid): + 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) @@ -958,8 +942,7 @@ class JuryPE(object): # **************************************************************************************************************** # # Méthodes d'affichage pour debug # **************************************************************************************************************** # - def str_etudiants_in_jury(self, - delim=";"): + def str_etudiants_in_jury(self, delim=";"): # En tete: entete = ["Id", "Nom", "Abandon", "Diplome"] for nom_sem in JuryPE.TOUS_LES_PARCOURS: @@ -1022,8 +1005,7 @@ class JuryPE(object): allTags = allTags.union(set(self.get_allTagForAggregat(nom))) return sorted(list(allTags)) if len(allTags) > 0 else [] - def table_syntheseJury(self, - mode="singlesheet"): # was str_syntheseJury + def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury """Table(s) du jury mode: singlesheet ou multiplesheet pour export excel """ @@ -1086,7 +1068,11 @@ class JuryPE(object): 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'] + for ( + sem + ) in ( + JuryPE.TOUS_LES_PARCOURS + ): # JuryPE.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] @@ -1146,17 +1132,15 @@ class JuryPE(object): row[champ + "note"] = scu.fmt_note(resgroupe[0]) row[champ + "class groupe"] = "%s / %s" % ( resgroupe[2] if resgroupe[2] else "-", - resgroupe[3] if resgroupe[3] else "-" + resgroupe[3] if resgroupe[3] else "-", ) row[champ + "class promo"] = "%s / %s" % ( respromo[2] if respromo[2] else "-", - respromo[3] if respromo[3] else "-" + respromo[3] if respromo[3] else "-", ) row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple( (scu.fmt_note(x) if x is not None else "-") - for x in (resgroupe[6], - resgroupe[4], - resgroupe[5]) + for x in (resgroupe[6], resgroupe[4], resgroupe[5]) ) row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple( (scu.fmt_note(x) if x is not None else "-") @@ -1183,8 +1167,7 @@ class JuryPE(object): # **************************************************************************************************************** # # ------------------------------------------------------------------------------------------------------------------ - def get_cache_etudInfo_d_un_etudiant(self, - etudid): + 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 """ @@ -1197,8 +1180,7 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_cache_notes_d_un_semestre(self, - formsemestre_id: int) -> NotesTableCompat: + def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat: """Charge la table des notes d'un formsemestre""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) return res_sem.load_formsemestre_results(formsemestre) @@ -1206,25 +1188,24 @@ class JuryPE(object): # ------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------ - def get_semestresBUT_d_un_etudiant(self, - etudid, - semestre_id=None): + 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 + 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] + 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 calcul_anneePromoBUT_d_un_etudiant(self, - etudid) -> int: + 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) @@ -1235,8 +1216,7 @@ class JuryPE(object): # ********************************************* # Fonctions d'affichage pour debug - def get_resultat_d_un_etudiant(self, - etudid): + def get_resultat_d_un_etudiant(self, etudid): chaine = "" for nom_sem in JuryPE.TOUS_LES_SEMESTRES: semtagid = self.PARCOURSINFO_DICT[etudid][ @@ -1303,7 +1283,7 @@ def get_annee_diplome_semestre(sem) -> int: Args: sem: Le semestre """ - nbre_semestres = int(JuryPE.AGGREGAT_DIPLOMANT[0]) # 6 + 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 ?? @@ -1319,8 +1299,7 @@ def get_annee_diplome_semestre(sem) -> int: # ---------------------------------------------------------------------------------- -def get_cosemestres_diplomants(semBase, - avec_meme_formation=False): +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 : diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index 977bc477..f7a45209 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -78,9 +78,7 @@ class SemestreTag(pe_tagtable.TableTag): # ----------------------------------------------------------------------------- # Fonctions d'initialisation # ----------------------------------------------------------------------------- - def __init__(self, - notetable, - sem): # Initialisation sur la base d'une notetable + 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 """ @@ -168,7 +166,7 @@ class SemestreTag(pe_tagtable.TableTag): """ tagdict = {} - for modimpl in []: # CB: désactive la recherche des tags -> self.modimpls: + 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) @@ -196,9 +194,7 @@ class SemestreTag(pe_tagtable.TableTag): return tagdict # ----------------------------------------------------------------------------- - def comp_MoyennesTag(self, - tag, - force=False) -> list: + 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. @@ -236,10 +232,7 @@ class SemestreTag(pe_tagtable.TableTag): ] # ----------------------------------------------------------------------------- - def get_noteEtCoeff_modimpl(self, - modimpl_id, - etudid, - profondeur=2): + 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. La note et le coeff sont extraits : 1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre, @@ -320,17 +313,14 @@ class SemestreTag(pe_tagtable.TableTag): return (note, coeff_norm) # ----------------------------------------------------------------------------- - def get_ue_capitalisees(self, - etudid) -> list[dict]: + def get_ue_capitalisees(self, etudid) -> list[dict]: """Renvoie la liste des capitalisation effectivement capitalisées par un étudiant""" if etudid in self.nt.validations.ue_capitalisees.index: return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") return [] # ----------------------------------------------------------------------------- - def get_listesNotesEtCoeffsTagEtudiant(self, - tag, - etudid): + 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. @@ -353,10 +343,7 @@ class SemestreTag(pe_tagtable.TableTag): # ----------------------------------------------------------------------------- # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug # ----------------------------------------------------------------------------- - def str_detail_resultat_d_un_tag(self, - tag, - etudid=None, - delim=";"): + def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"): """Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag : rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés. Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés. @@ -476,8 +463,7 @@ class SemestreTag(pe_tagtable.TableTag): # ********************************************* -def comp_coeff_pond(coeffs, - ponderations): +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] @@ -513,9 +499,7 @@ def get_moduleimpl(modimpl_id) -> dict: # ********************************************** -def get_moy_ue_from_nt(nt, - etudid, - modimpl_id) -> float: +def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id """ diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index 4431bcb8..e2f0a4f8 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -51,19 +51,15 @@ class SetTag(pe_tagtable.TableTag): """ # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, - nom_combinaison, - parcours): + def __init__(self, nom_combinaison, parcours): pe_tagtable.TableTag.__init__(self, nom=nom_combinaison) self.combinaison = nom_combinaison self.parcours = parcours # Le groupe de semestres/parcours à aggréger # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, - etudiants: list[dict], - juryPEDict, - etudInfoDict, - nom_sem_final=None): + def set_Etudiants( + self, etudiants: list[dict], juryPEDict, etudInfoDict, nom_sem_final=None + ): """Détermine la liste des étudiants à prendre en compte, en partant de la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide.""" if nom_sem_final: @@ -98,8 +94,7 @@ class SetTag(pe_tagtable.TableTag): ) # --------------------------------------------------------------------------------------------- - def set_SemTagDict(self, - SemTagDict): + def set_SemTagDict(self, SemTagDict): """Mémorise les semtag nécessaires au jury.""" self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} if PE_DEBUG >= 1: @@ -157,9 +152,7 @@ class SetTag(pe_tagtable.TableTag): self.tagdict[tag][mod] = semtag.tagdict[tag][mod] # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, - tag, - etudid): + def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) avec notes et coeffs deux listes""" lesSemsDeLEtudiant = [ @@ -179,9 +172,7 @@ class SetTag(pe_tagtable.TableTag): return (notes, coeffs) # ------------------------------------------------------------------------------------------------------------------- - def comp_MoyennesSetTag(self, - tag, - force=False): + def comp_MoyennesSetTag(self, tag, force=False): """Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération appliqué dans cette moyenne. @@ -218,19 +209,13 @@ class SetTagInterClasse(pe_tagtable.TableTag): """ # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, - nom_combinaison, - diplome): + def __init__(self, nom_combinaison, diplome): pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") self.combinaison = nom_combinaison self.parcoursDict = {} # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, - etudiants, - juryPEDict, - etudInfoDict, - nom_sem_final=None): + def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): """Détermine la liste des étudiants à prendre en compte, en partant de la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. Renvoie le nombre d'étudiants effectivement inscrits.""" @@ -252,8 +237,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): ) # --------------------------------------------------------------------------------------------- - def set_SetTagDict(self, - SetTagDict): + def set_SetTagDict(self, SetTagDict): """Mémorise les settag nécessaires au jury.""" self.SetTagDict = { fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None @@ -301,9 +285,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): return sorted(list(set(ensemble))) # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, - tag, - etudid): + def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) avec notes et coeffs deux listes""" leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] @@ -315,9 +297,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): return (note, coeff) # ------------------------------------------------------------------------------------------------------------------- - def get_MoyennesSetTag(self, - tag, - force=False): + def get_MoyennesSetTag(self, tag, force=False): """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération appliqué dans cette moyenne. diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index 994d3850..82cf16b5 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -77,9 +77,7 @@ class TableTag(object): # ***************************************************************************************************************** # ----------------------------------------------------------------------------------------------------------- - def get_moy_from_resultats(self, - tag, - etudid): + 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] @@ -88,9 +86,7 @@ class TableTag(object): ) # ----------------------------------------------------------------------------------------------------------- - def get_rang_from_resultats(self, - tag, - etudid): + def get_rang_from_resultats(self, tag, etudid): """Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats""" return ( self.rangs[tag][etudid] @@ -99,9 +95,7 @@ class TableTag(object): ) # ----------------------------------------------------------------------------------------------------------- - def get_coeff_from_resultats(self, - tag, - etudid): + def get_coeff_from_resultats(self, tag, etudid): """Renvoie la somme des coeffs de pondération normalisée utilisés dans le calcul de la moyenne à un tag d'un étudiant au regard du format de self.resultats. """ @@ -123,18 +117,15 @@ class TableTag(object): return len(self.inscrlist) # ----------------------------------------------------------------------------------------------------------- - def get_moy_from_stats(self, - tag): + def get_moy_from_stats(self, tag): """Renvoie la moyenne des notes calculées pour d'un tag donné""" return self.statistiques[tag][0] if tag in self.statistiques else None - def get_min_from_stats(self, - tag): + def get_min_from_stats(self, tag): """Renvoie la plus basse des notes calculées pour d'un tag donné""" return self.statistiques[tag][1] if tag in self.statistiques else None - def get_max_from_stats(self, - tag): + def get_max_from_stats(self, tag): """Renvoie la plus haute des notes calculées pour d'un tag donné""" return self.statistiques[tag][2] if tag in self.statistiques else None @@ -151,9 +142,7 @@ class TableTag(object): "min", ) - def get_resultatsEtud(self, - tag, - etudid): + def get_resultatsEtud(self, tag, etudid): """Renvoie un tuple (note, coeff, rang, nb_inscrit, moy, min, max) synthétisant les résultats d'un étudiant à un tag donné. None sinon""" return ( @@ -175,9 +164,7 @@ class TableTag(object): # ***************************************************************************************************************** # ----------------------------------------------------------------------------------------------------------- - def add_moyennesTag(self, - tag, - listMoyEtCoeff) -> bool: + def add_moyennesTag(self, tag, listMoyEtCoeff) -> bool: """ Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats avec calcul du rang @@ -210,8 +197,7 @@ class TableTag(object): # Méthodes dévolues aux calculs de statistiques (min, max, moy) sur chaque moyenne taguée # ***************************************************************************************************************** - def comp_stats_d_un_tag(self, - tag): + def comp_stats_d_un_tag(self, tag): """ Calcule la moyenne generale, le min, le max pour un tag donné, en ne prenant en compte que les moyennes significatives. Mémorise le resultat dans @@ -235,10 +221,7 @@ class TableTag(object): # ************************************************************************ # Méthodes dévolues aux affichages -> a revoir # ************************************************************************ - def str_resTag_d_un_etudiant(self, - tag, - etudid, - delim=";"): + def str_resTag_d_un_etudiant(self, tag, etudid, delim=";"): """Renvoie une chaine de caractères (valable pour un csv) décrivant la moyenne et le rang d'un étudiant, pour un tag donné ; """ @@ -253,20 +236,14 @@ class TableTag(object): ) return moystr - def str_res_d_un_etudiant(self, - etudid, - delim=";"): + def str_res_d_un_etudiant(self, etudid, delim=";"): """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique).""" return delim.join( [self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()] ) # ----------------------------------------------------------------------- - def str_moytag(cls, - moyenne, - rang, - nbinscrit, - delim=";"): + def str_moytag(cls, moyenne, rang, nbinscrit, delim=";"): """Renvoie une chaine de caractères représentant une moyenne (float ou string) et un rang pour différents formats d'affichage : HTML, debug ligne de commande, csv""" moystr = ( @@ -279,9 +256,7 @@ class TableTag(object): str_moytag = classmethod(str_moytag) # ----------------------------------------------------------------------- - def str_tagtable(self, - delim=";", - decimal_sep=","): + 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"] for tag in self.get_all_tags(): @@ -313,9 +288,7 @@ class TableTag(object): # ********************************************* -def moyenne_ponderee_terme_a_terme(notes, - coefs=None, - force=False): +def moyenne_ponderee_terme_a_terme(notes, coefs=None, force=False): """ Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération. Renvoie le résultat sous forme d'un tuple (moy, somme_coeff) diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 39b810c9..71c50760 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -100,10 +100,14 @@ def pe_view_sem_recap( sem_base = FormSemestre.get_formsemestre(formsemestre_id) if not sem_base.formation.is_apc(): - raise ScoValueError("Le module de Poursuites d'Etudes avec Scodoc 9 n'est disponible que pour des formations BUT") + raise ScoValueError( + "Le module de Poursuites d'Etudes avec Scodoc 9 n'est disponible que pour des formations BUT" + ) # TODO: CB: A supprimer à long terme - semBase = sco_formsemestre.get_formsemestre(formsemestre_id) # FormSemestre.get_formsemestre() + semBase = sco_formsemestre.get_formsemestre( + formsemestre_id + ) # FormSemestre.get_formsemestre() jury = pe_jurype.JuryPE(sem_base, semBase) # Ajout avis LaTeX au même zip: From c9336dd01c7ff3a5a03d02436eff6aaad95185ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 20 Jan 2024 09:31:02 +0100 Subject: [PATCH 08/23] =?UTF-8?q?Refonte=20de=20la=20recherche=20des=20?= =?UTF-8?q?=C3=A9tudiants=20=C3=A0=20prendre=20en=20compte=20dans=20le=20J?= =?UTF-8?q?uryPE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 363 ++++++++++++++++++++++++++++++++++++++++++ app/pe/pe_tools.py | 203 +++++++++++++++++++++-- app/pe/pe_view.py | 48 ++++-- 3 files changed, 594 insertions(+), 20 deletions(-) create mode 100644 app/pe/pe_etudiant.py diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py new file mode 100644 index 00000000..7c558184 --- /dev/null +++ b/app/pe/pe_etudiant.py @@ -0,0 +1,363 @@ +# -*- 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 17/01/2024 + +@author: barasc +""" + +import app.pe.pe_tools as pe_tools +from app.models import FormSemestre, Identite +from app.pe.pe_tools import pe_print +from app.scodoc import ( + sco_etud, + codes_cursus, + sco_formsemestre, + sco_formsemestre_inscriptions, + sco_report, +) +import datetime + + +class EtudiantsJuryPE: + """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" + + def __init__(self): + """ """ + + "Les identités des étudiants du jury" + self.identites = {} # ex. ETUDINFO_DICT + "Les cursus (semestres suivis, abandons, ...)" + self.cursus = {} + "Les etudids des étudiants à considérer au jury" + self.etudiants_jury_ids = {} + "Les etudids des étudiants dont il faut calculer les moyennes/classements" + self.etudiants_ids = {} + "Les formsemestres dont il faut calculer les moyennes" + self.formsemestres_jury_ids = {} + + def find_etudiants(self, annee_diplome: int, formation_id: int): + """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant + de manière automatique par rapport à leur année de diplomation ``annee_diplome`` + dans la formation ``formation_id``. + + Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE. + + Args: + annee_diplome: L'année de diplomation + formation_id: L'identifiant de la formation + + *Remarque* : ex: JuryPE.get_etudiants_in_jury() + """ + "Les cosemestres donnant lieu à même année de diplome" + cosemestres = pe_tools.get_cosemestres_diplomants( + annee_diplome, None # formation_id, + ) + pe_tools.pe_print( + "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) + ) + + """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" + pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") + self.etudiants_ids = get_etudiants_dans_semestres( + cosemestres + ) # étudiants faisant partie de tous les cosemestres + pe_tools.pe_print(" => %d étudiants trouvés" % len(self.etudiants_ids)) + + # L'analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, .... + pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") + + no_etud = 0 + 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="") + no_etud += 1 + pe_tools.pe_print() + + """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" + self.etudiants_jury_ids = self.get_etudids(annee_diplome) + + """Les étudiants dont il faut calculer les moyennes""" + self.etudiants_ids = {etudid for etudid in self.cursus} + + """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" + 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}") + 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]])) + + def get_etudids(self, annee_diplome: int, 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'``ordre`` est précisé, trie la liste par ordre alphabétique de etat_civil + + Args: + annee_diplome: Année de diplomation visée pour le jury + ordre: Un ordre de tri + + Returns: + Une liste contenant des ``etudids`` + + 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 ordre == "alphabetique": # Tri alphabétique + etudidsAvecNom = [ + (etudid, etud["etat_civil"]) + for (etudid, etud) in self.cursus.items() + if etudid in etudids + ] + etudidsAvecNomTrie = sorted(etudidsAvecNom, key=lambda col: col[1]) + etudids = [etud[0] for etud in etudidsAvecNomTrie] + return etudids + + def add_etudid(self, etudid: int, cosemestres): + """Ajoute un étudiant à ceux qui devront être traités pendant le jury pouvant être : + + * des étudiants sur lesquels le jury va statuer (année de diplômation du jury considéré) + * des étudiants qui ne seront pas considérés dans le jury mais ont participé dans leur scolarité + à un (ou plusieurs) semestres communs aux étudiants du jury (et impacteront les classements) + + L'ajout consiste : + + * à insérer une entrée pour l'étudiant en mémorisant son identité, + avec son nom, prénom, etc... + * à analyser son parcours, pour déterminer s'il n'a (ou non) 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 à ceux traiter par le jury + cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres de même année de diplomation + 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""" + semestres_etudiant = { + frmsem.formsemestre_id: frmsem for frmsem in identite.get_formsemestres() + } + + 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 + } + + """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" + 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} + + + """Tri des semestres par aggrégat""" + for parcours in pe_tools.TOUS_LES_AGGREGATS: + """L'aggrégat considéré""" + noms_semestre_de_aggregat = pe_tools.PARCOURS[parcours]["aggregat"] + + 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] + + if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: + pe_tools.pe_print( + parcours + "=" + str(self.cursus[etudid][parcours]), + end="", + ) + + + def get_formsemestres_jury(self): + """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. + + Returns: + Un ensemble de formsemestres + """ + formsemestres = {} + for etudid in self.etudiants_ids: + formsem_etudid = set(self.cursus[etudid].keys()) + formsemestres = formsemestres | formsem_etudid + return formsemestres + + +def get_etudiants_dans_semestres(semestres: dict[FormSemestre]) -> set: + """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) + inscrits à l'un des semestres de la liste de ``semestres``. + + Remarque : Les ``cosemestres`` sont généralement obtenus avec ``sco_formsemestre.do_formsemestre_list()`` + + Args: + semestres: Un dictionnaire {fid: Formsemestre(fid)} donnant un + ensemble d'identifiant de semestres + + Returns: + Un ensemble d``etudid`` + """ + + etudiants_ids = set() + 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") + etudiants_ids = ( + etudiants_ids | etudiants_du_sem + ) # incluant la suppression des doublons + + return etudiants_ids + + +def annee_diplome(identite: Identite) -> int: + """L'année de diplôme prévue d'un étudiant en fonction de ses semestres + d'inscription (pour un BUT). + + Args: + identite: L'identité d'un étudiant + + Returns: + L'année prévue de sa diplômation + + NOTE: Pourrait être déplacé dans app.models.etudiants.Identite + """ + formsemestres = identite.get_formsemestres() + if formsemestres: + return max( + [ + pe_tools.get_annee_diplome_semestre(sem_base) + for sem_base in formsemestres + ] + ) + else: + return None + + +def semestres_etudiant(etudid: int, semestre_id=None): + """La liste des semestres BUT d'un étudiant + pour un semestre_id (parmi 1, 2, 3, 4, 5, 6) 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 + + NOTE:: ex:: JuryPE.get_semestresBUT_d_un_etudiant() + TODO:: A revoir""" + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + nbre_semestres = int(pe_tools.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 arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool: + """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : + + * d'une réorientation à l'initiative du jury de semestre ou d'une démission ; dans ce cas, utilise les + décisions prises dans les jury de semestres (code NAR pour réorienté & DEM pour démissionnaire) + + * d'un arrêt volontaire : l'étudiant disparait des listes d'inscrit, sans + pour autant avoir été indiqué NAR ou DEM. Dans ce cas, 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. + + Args: + identite: L'identité d'un étudiant + cosemestres: Les semestres donnant lieu à diplômation (sans redoublement) en date du jury + + Returns: + Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? + + TODO:: A reprendre avec l'accès aux résultats des jury prévu par Scodoc9 + """ + etudid = identite.etudid + reponse = False + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + (code, parcours) = sco_report.get_code_cursus_etud(etud) + + # Est-il réorienté ou démissionnaire ? + if ( + len(codes_cursus.CODES_SEM_REO & set(parcours.values())) > 0 + ): # Eliminé car NAR apparait dans le parcours + if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: + pe_tools.pe_print(" -> à éliminer car réorienté (NAR)") + return True + + if "DEM" in list(parcours.values()): # Eliminé car DEM + if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: + pe_tools.pe_print(" -> à éliminer car DEM") + return True + + # A-t-il arrêté volontairement sa formation ? + dernier_formsemestre = identite.get_formsemestres()[0] + + # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? + formsestres_superieurs_possibles = [] + for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits + if ( + sem.formsemestre_id != dernier_formsemestre.formsemestre_id + and sem.date_debut.year >= dernier_formsemestre.date_debut.year + and sem.semestre_id > dernier_formsemestre.semestre_id + ): # 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 + formsestres_superieurs_possibles.append(fid) + + if len(formsestres_superieurs_possibles) > 0: + return True + + return False diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index 133a0788..a2264fc0 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -46,9 +46,11 @@ from flask import g import app.scodoc.sco_utils as scu from app import log +from app.models import FormSemestre +from app.scodoc import sco_formsemestre from app.scodoc.sco_logos import find_logo -PE_DEBUG = 0 +PE_DEBUG = 1 if not PE_DEBUG: # log to notes.log @@ -74,10 +76,96 @@ PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" # ---------------------------------------------------------------------------------------- +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[AGGREGAT_DIPLOMANT]["aggregat"] +TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] +TOUS_LES_PARCOURS = list(PARCOURS.keys()) + # ---------------------------------------------------------------------------------------- -def print_semestres_description(sems, - avec_affichage_debug=False): +def print_semestres_description(sems, avec_affichage_debug=False): """Dediee a l'affichage d'un semestre pour debug du module""" def chaine_semestre(sem): @@ -166,10 +254,7 @@ def list_directory_filenames(path): return R -def add_local_file_to_zip(zipfile, - ziproot, - pathname, - path_in_zip): +def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): """Read pathname server file and add content to zip under path_in_zip""" rooted_path_in_zip = os.path.join(ziproot, path_in_zip) zipfile.write(filename=pathname, arcname=rooted_path_in_zip) @@ -177,8 +262,7 @@ def add_local_file_to_zip(zipfile, # zipfile.writestr(rooted_path_in_zip, data) -def add_refs_to_register(register, - directory): +def add_refs_to_register(register, directory): """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme filename => pathname """ @@ -188,8 +272,7 @@ def add_refs_to_register(register, register[filename] = pathname -def add_pe_stuff_to_zip(zipfile, - ziproot): +def add_pe_stuff_to_zip(zipfile, ziproot): """Add auxiliary files to (already opened) zip Put all local files found under config/doc_poursuites_etudes/local and config/doc_poursuites_etudes/distrib @@ -218,6 +301,104 @@ def add_pe_stuff_to_zip(zipfile, ) +# ---------------------------------------------------------------------------------------- +def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int: + """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres) + et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle + sera remis le diplôme BUT des étudiants qui y sont scolarisés + (en supposant qu'il n'y ait pas de redoublement à venir). + + **Remarque sur le calcul** : 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'année 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 + + La fonction est adaptée au cas des semestres décalés. + + Par exemple : + + * S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026 + * S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027 + + Args: + sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit : + + * un ``FormSemestre`` (Scodoc9) + * un dict (format compatible avec Scodoc7) + + nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT) + """ + + if isinstance(sem_base, FormSemestre): + sem_id = sem_base.semestre_id + annee_fin = sem_base.date_fin.year + annee_debut = sem_base.date_debut.year + else: # sem_base est un dictionnaire (Scodoc 7) + sem_id = sem_base["semestre_id"] + annee_fin = int(sem_base["annee_fin"]) + annee_debut = int(sem_base["annee_debut"]) + if ( + 1 <= sem_id <= nbre_sem_formation + ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? + nbreSemRestant = ( + nbre_sem_formation - sem_id + ) # nombre de semestres restant avant diplome + nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome + # Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé + # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon + delta = annee_fin - annee_debut + decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 + increment = decalage * (1 - delta) + return annee_fin + nbreAnRestant + increment + + +def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list: + """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome`` + et s'intégrant à la formation donnée par son ``formation_id``. + + **Définition** : Un co-semestre est un semestre : + + * dont l'année de diplômation prédite (sans redoublement) est la même + * dont la formation est la même (optionnel) + * qui a des étudiants inscrits + + Si formation_id == None, ne prend pas en compte l'identifiant de formation + TODO:: A raccrocher à un programme + + Args: + annee_diplome: L'année de diplomation + formation_id: L'identifiant de la formation + """ + tousLesSems = ( + sco_formsemestre.do_formsemestre_list() + ) # tous les semestres memorisés dans scodoc + + if formation_id: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + and sem["formation_id"] == formation_id + } + else: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + } + + cosemestres = {} + for fid in cosemestres_fids: + cosem = FormSemestre.get_formsemestre(fid) + if len(cosem.etuds_inscriptions) > 0: + cosemestres[fid] = cosem + + return cosemestres + + # ---------------------------------------------------------------------------------------- # Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long) JURY_SYNTHESE_POUR_DEBUG = { diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 71c50760..779e049d 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -51,12 +51,38 @@ from app.pe import pe_avislatex def _pe_view_sem_recap_form(formsemestre_id): + sem_base = FormSemestre.get_formsemestre(formsemestre_id) + if not sem_base.formation.is_apc() or sem_base.formation.get_cursus().NB_SEM < 6: + H = [ + html_sco_header.sco_header(page_title="Avis de poursuite d'études"), + f"""

Génération des avis de poursuites d'études

+

+ Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de + poursuites d'études. +
+ De nombreux aspects sont paramétrables: + + voir la documentation + . + Cette fonction (en Scodoc9) n'est prévue que pour le BUT. +
+ Rendez-vous donc sur un semestre de BUT. +

+

Génération des avis de poursuites d'études

Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de - poursuites d'études. + poursuites d'études pour les étudiants diplômés en {diplome}.
De nombreux aspects sont paramétrables: Date: Sat, 20 Jan 2024 16:34:38 +0100 Subject: [PATCH 09/23] 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 7c558184..45c5bc23 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 d7939288..dad54978 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 f7a45209..b5fae753 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 82cf16b5..ad69a090 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 cf535e3b..2f51e383 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: From 9e925aa50032a4311596960a3b752cf71392b069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 21 Jan 2024 11:42:46 +0100 Subject: [PATCH 10/23] =?UTF-8?q?D=C3=A9tection=20des=20cursus=20des=20?= =?UTF-8?q?=C3=A9tudiants=20dans=20les=20aggr=C3=A9gats=20(quelles=20combi?= =?UTF-8?q?naisons=20de=20semestre=20pour=20un=20'3S'=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_semestretag.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index b5fae753..a97c5000 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -95,10 +95,8 @@ class SemestreTag(pe_tagtable.TableTag): self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() self.dispense_ues = self.nt.dispense_ues - """Les tags""" + """Les tags (en supprimant les tags réservés)""" 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] @@ -132,6 +130,10 @@ class SemestreTag(pe_tagtable.TableTag): "nb_inscrits": len(moy_gen_but), } + """Synthétise l'ensemble des moyennes dans un dataframe""" + self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique + self.notes = self.df_tagtable() # Le dataframe synthétique des notes (=moyennes par tag) + # ----------------------------------------------------------------------------- def get_etudids(self): """Renvoie la liste des etud_id des étudiants inscrits au semestre""" From 7a0b560d54258ec7690c089c5ffc510457070686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 21 Jan 2024 13:14:04 +0100 Subject: [PATCH 11/23] Celui qui voulait des tenseurs --- app/pe/pe_settag.py | 163 ++++++++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 73 deletions(-) diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index e2f0a4f8..74a0cb22 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -35,9 +35,16 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ - +from app.comp.res_sem import load_formsemestre_results +from app.models import FormSemestre +from app.pe.pe_semestretag import SemestreTag from app.pe.pe_tools import pe_print, PE_DEBUG from app.pe import pe_tagtable +import pandas as pd +import numpy as np + + +from app.pe.pe_etudiant import EtudiantsJuryPE class SetTag(pe_tagtable.TableTag): @@ -50,80 +57,88 @@ class SetTag(pe_tagtable.TableTag): un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte). """ - # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, parcours): - pe_tagtable.TableTag.__init__(self, nom=nom_combinaison) - self.combinaison = nom_combinaison - self.parcours = parcours # Le groupe de semestres/parcours à aggréger - - # ------------------------------------------------------------------------------------------- - def set_Etudiants( - self, etudiants: list[dict], juryPEDict, etudInfoDict, nom_sem_final=None + def __init__( + self, + nom, + formsemestre_terminal: FormSemestre, + semestres_aggreges: dict[int, FormSemestre], + semestres_taggues: dict[int, SemestreTag], + donnees_etudiants: EtudiantsJuryPE, ): - """Détermine la liste des étudiants à prendre en compte, en partant de - la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide.""" - if nom_sem_final: - self.nom += "_" + nom_sem_final - for etudid in etudiants: - parcours_incomplet = ( - sum([juryPEDict[etudid][nom_sem] is None for nom_sem in self.parcours]) - > 0 - ) # manque-t-il des formsemestre_id validant aka l'étudiant n'a pas été inscrit dans tous les semestres de l'aggrégat - if not parcours_incomplet: - self.inscrlist.append(etudInfoDict[etudid]) - self.identdict[etudid] = etudInfoDict[etudid] + pe_tagtable.TableTag.__init__(self, nom) - delta = len(etudiants) - len(self.inscrlist) - if delta > 0: - pe_print(self.nom + " -> " + str(delta) + " étudiants supprimés") + """Le formsemestre terminal et les semestres aggrégés""" + self.formsemestre_terminal = formsemestre_terminal + nt = load_formsemestre_results(formsemestre_terminal) + self.semestres_aggreges = semestres_aggreges - # Le sous-ensemble des parcours - self.parcoursDict = {etudid: juryPEDict[etudid] for etudid in self.identdict} - - # ------------------------------------------------------------------------------------------- - def get_Fids_in_settag(self): - """Renvoie la liste des semestres (leur formsemestre_id) à prendre en compte - pour le calcul des moyennes, en considérant tous les étudiants inscrits et - tous les semestres de leur parcours""" - return list( - { - self.parcoursDict[etudid][nom_sem] - for etudid in self.identdict - for nom_sem in self.parcours + """Les semestres tags associés aux semestres aggrégés""" + try: + self.semestres_tags_aggreges = { + frmsem_id: semestres_taggues[frmsem_id] + for frmsem_id in semestres_taggues } - ) + except: + raise ValueError("Semestres taggués manquants") - # --------------------------------------------------------------------------------------------- - def set_SemTagDict(self, SemTagDict): - """Mémorise les semtag nécessaires au jury.""" - self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} - if PE_DEBUG >= 1: - pe_print(" => %d semestres fusionnés" % len(self.SemTagDict)) + """Les étudiants (état civil + cursus connu)""" + self.etuds = nt.etuds + self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} + self.cursus = { + etudid: donnees_etudiants.cursus[etudid] for etudid in self.etudiants + } + + """Les tags extraits de tous les semestres""" + self.tags_sorted = self.do_taglist() + + """Construit le cube de notes""" + self.notes_cube = {} + + """Les moyennes par tag""" + self.moyennes_tags = {} # ------------------------------------------------------------------------------------------------------------------- - def comp_data_settag(self): - """Calcule tous les données numériques relatives au settag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.taglist = self.do_taglist() # la liste des tags - self.do_tagdict() # le dico descriptif des tags - # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) + def compute_notes_cube(self): + """Construit le cube de notes (etudid x tags x semestre_aggregé) + nécessaire au calcul des moyennes de l'aggrégat + """ + nb_tags = len(self.tags_sorted) + nb_etudiants = len(self.etuds) + nb_semestres = len(self.semestres_tags_aggreges) + + """Index du cube (etudids -> dim 0, tags -> dim 1)""" + etudids = [etud.etudid for etud in self.etuds] + tags = self.tags_sorted + semestres_id = list(self.semestres_tags_aggreges.keys()) + + dfs = {} + + for frmsem_id in semestres_id: + """Partant d'un dataframe vierge""" + df = pd.DataFrame(np.nan, index=etudids, columns=self.tags_sorted) + + """Charge les notes du semestre tag""" + notes = self.semestres_tags_aggreges[frmsem_id].notes + + """Les étudiants & les tags commun au dataframe final et aux notes du semestre)""" + etudids_communs = df.index.intersection(notes.index) + tags_communs = df.columns.intersection(notes.columns) + + """Injecte les notes par tag""" + df.loc[etudids_communs, tags_communs] = notes.loc[ + etudids_communs, tags_communs + ] + + """Stocke le df""" + dfs[frmsem_id] = df + + """Réunit les notes sous forme d'un cube etdids x tags x semestres""" + semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs] + etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1) + + return etudids_x_tags_x_semestres + - # Calcul des moyennes de chaque étudiant par tag - reussiteAjoutTag = {"OK": [], "KO": []} - for tag in self.taglist: - moyennes = self.comp_MoyennesSetTag(tag, force=False) - res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne - reussiteAjoutTag["OK" if res else "KO"].append(tag) - if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: - pe_print( - " => Fusion de %d tags : " % (len(reussiteAjoutTag["OK"])) - + ", ".join(reussiteAjoutTag["OK"]) - ) - if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: - pe_print( - " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) - + ", ".join(reussiteAjoutTag["KO"]) - ) # ------------------------------------------------------------------------------------------------------------------- def get_etudids(self): @@ -131,13 +146,15 @@ class SetTag(pe_tagtable.TableTag): # ------------------------------------------------------------------------------------------------------------------- def do_taglist(self): - """Parcourt les tags des semestres taggués et les synthétise sous la forme - d'une liste en supprimant les doublons + """Synthétise les tags à partir des semestres (taggués) aggrégés + + Returns: + Une liste de tags triés par ordre alphabétique """ - ensemble = [] - for semtag in self.SemTagDict.values(): - ensemble.extend(semtag.get_all_tags()) - return sorted(list(set(ensemble))) + tags = [] + for frmsem_id in self.semestres_tags_aggreges: + tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) + return sorted(set(tags)) # ------------------------------------------------------------------------------------------------------------------- def do_tagdict(self): From 340aa749b231d8b4a53115333efa9230b733ecf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 21 Jan 2024 18:05:00 +0100 Subject: [PATCH 12/23] Calcul des moyennes par tag d'un settag (avec les tenseurs) --- app/pe/pe_settag.py | 130 ++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 70 deletions(-) diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index 74a0cb22..61f4fe90 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -35,6 +35,7 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ +from app.comp import moy_sem from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre from app.pe.pe_semestretag import SemestreTag @@ -92,12 +93,26 @@ class SetTag(pe_tagtable.TableTag): self.tags_sorted = self.do_taglist() """Construit le cube de notes""" - self.notes_cube = {} + self.notes_cube = self.compute_notes_cube() - """Les moyennes par tag""" + """Calcul les moyennes par tag sous forme d'un dataframe""" + etudids = self.get_etudids() + self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) + + """Synthétise les moyennes/classements par tag""" self.moyennes_tags = {} + for tag in self.tags_sorted: + moy_gen_tag = self.notes[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), + } - # ------------------------------------------------------------------------------------------------------------------- def compute_notes_cube(self): """Construit le cube de notes (etudid x tags x semestre_aggregé) nécessaire au calcul des moyennes de l'aggrégat @@ -115,7 +130,7 @@ class SetTag(pe_tagtable.TableTag): for frmsem_id in semestres_id: """Partant d'un dataframe vierge""" - df = pd.DataFrame(np.nan, index=etudids, columns=self.tags_sorted) + df = pd.DataFrame(np.nan, index=etudids, columns=tags) """Charge les notes du semestre tag""" notes = self.semestres_tags_aggreges[frmsem_id].notes @@ -138,11 +153,8 @@ class SetTag(pe_tagtable.TableTag): return etudids_x_tags_x_semestres - - - # ------------------------------------------------------------------------------------------------------------------- def get_etudids(self): - return list(self.identdict.keys()) + return list(self.etudiants.keys()) # ------------------------------------------------------------------------------------------------------------------- def do_taglist(self): @@ -156,68 +168,6 @@ class SetTag(pe_tagtable.TableTag): tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) return sorted(set(tags)) - # ------------------------------------------------------------------------------------------------------------------- - def do_tagdict(self): - """Synthétise la liste des modules pris en compte dans le calcul d'un tag (pour analyse des résultats)""" - self.tagdict = {} - for semtag in self.SemTagDict.values(): - for tag in semtag.get_all_tags(): - if tag != "but": - if tag not in self.tagdict: - self.tagdict[tag] = {} - for mod in semtag.tagdict[tag]: - self.tagdict[tag][mod] = semtag.tagdict[tag][mod] - - # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): - """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) - avec notes et coeffs deux listes""" - lesSemsDeLEtudiant = [ - self.parcoursDict[etudid][nom_sem] for nom_sem in self.parcours - ] # peuvent être None - - notes = [ - self.SemTagDict[fid].get_moy_from_resultats(tag, etudid) - for fid in lesSemsDeLEtudiant - if tag in self.SemTagDict[fid].taglist - ] # eventuellement None - coeffs = [ - self.SemTagDict[fid].get_coeff_from_resultats(tag, etudid) - for fid in lesSemsDeLEtudiant - if tag in self.SemTagDict[fid].taglist - ] - return (notes, coeffs) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_MoyennesSetTag(self, tag, force=False): - """Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués - de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération - appliqué dans cette moyenne. - - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - - Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} - """ - # if tag not in self.get_all_tags() : return None - - # Calcule les moyennes - lesMoyennes = [] - for ( - etudid - ) in ( - self.get_etudids() - ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag - (notes, coeffs_norm) = self.get_NotesEtCoeffsSetTagEtudiant( - tag, etudid - ) # lecture des notes associées au tag - (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme( - notes, coeffs_norm, force=force - ) - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes - class SetTagInterClasse(pe_tagtable.TableTag): """Récupère les moyennes de SetTag aggrégeant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2 @@ -339,3 +289,43 @@ class SetTagInterClasse(pe_tagtable.TableTag): (moyenne, somme_coeffs, etudid) ] # Un tuple (pour classement résumant les données) return lesMoyennes + + +def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): + """Calcul de la moyenne par tag sur plusieurs semestres. + La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles + + *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag + par aggrégat de plusieurs semestres. + + Args: + set_cube: notes moyennes aux modules ndarray + (etuds x modimpls x UEs), des floats avec des NaN + etudids: liste des étudiants (dim. 0 du cube) + tags: liste des tags (dim. 1 du cube) + Returns: + Un DataFrame avec pour columns les moyennes par tags, + et pour rows les etudid + """ + nb_etuds, nb_tags, nb_semestres = set_cube.shape + assert nb_etuds == len(etudids) + assert nb_tags == len(tags) + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + + # Enlève les NaN du cube pour les entrées manquantes + set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) + + # Les moyennes par tag + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) + + # Le dataFrame + etud_moy_tag_df = pd.DataFrame( + etud_moy_tag, + index=etudids, # les etudids + columns=tags, # les tags + ) + + return etud_moy_tag_df From 90c2516d01adc603705338e4109abbadf23f95ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 21 Jan 2024 18:55:21 +0100 Subject: [PATCH 13/23] Etat temporaire --- app/pe/pe_etudiant.py | 266 +++++++++++++---------- app/pe/pe_jurype.py | 370 +++++++++++++++----------------- app/pe/pe_settag.py | 123 ----------- app/pe/pe_settag_interclasse.py | 125 +++++++++++ app/pe/pe_tagtable.py | 21 +- app/pe/pe_tools.py | 4 + 6 files changed, 474 insertions(+), 435 deletions(-) create mode 100644 app/pe/pe_settag_interclasse.py diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 45c5bc23..cda39fa1 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -55,15 +55,17 @@ class EtudiantsJuryPE: def __init__(self): """ """ - "Les identités des étudiants du jury" + "Les identités des étudiants traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT - "Les cursus (semestres suivis, abandons, ...)" + "Les cursus (semestres suivis, abandons, ...) des étudiants" self.cursus = {} - "Les etudids des étudiants à considérer au jury" + + "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" self.etudiants_jury_ids = {} - "Les etudids des étudiants dont il faut calculer les moyennes/classements" + "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" self.etudiants_ids = {} - "Les formsemestres dont il faut calculer les moyennes" + + "Les formsemestres dont il faut calculer les moyennes par tag" self.formsemestres_jury_ids = {} def find_etudiants(self, annee_diplome: int, formation_id: int): @@ -75,38 +77,46 @@ class EtudiantsJuryPE: Args: annee_diplome: L'année de diplomation - formation_id: L'identifiant de la formation + formation_id: L'identifiant de la formation (inutilisé) *Remarque* : ex: JuryPE.get_etudiants_in_jury() """ "Les cosemestres donnant lieu à même année de diplome" - cosemestres = pe_tools.get_cosemestres_diplomants( - annee_diplome, None # formation_id, - ) + cosemestres = pe_tools.get_cosemestres_diplomants(annee_diplome, None) pe_tools.pe_print( "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) ) """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") - self.etudiants_ids = get_etudiants_dans_semestres( - cosemestres - ) # étudiants faisant partie de tous les cosemestres - pe_tools.pe_print(" => %d étudiants trouvés" % len(self.etudiants_ids)) + self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) + pe_tools.pe_print( + " => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) + ) - # L'analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, .... + """Analyse des parcours étudiants pour déterminer leur année effective de diplome + avec prise en compte des redoublements, des abandons, ....""" pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") no_etud = 0 for no_etud, etudid in enumerate(self.etudiants_ids): - self.add_etudid(etudid, cosemestres) + """L'identité de l'étudiant""" + identite = Identite.get_etud(etudid) + self.identites[etudid] = identite + + """L'analyse de son cursus""" + self.analyse_etat_etudiant(etudid, cosemestres) + + """L'analyse de son parcours pour atteindre chaque semestre de la formation""" + self.analyse_parcours_etudiant_dans_semestres(etudid) + if (no_etud + 1) % 10 == 0: pe_tools.pe_print((no_etud + 1), " ", end="") no_etud += 1 pe_tools.pe_print() """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" - self.etudiants_jury_ids = self.get_etudids(annee_diplome) + self.etudiants_jury_ids = self.get_etudiants(annee_diplome) """Les étudiants dont il faut calculer les moyennes""" self.etudiants_ids = {etudid for etudid in self.cursus} @@ -129,89 +139,48 @@ class EtudiantsJuryPE: + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) ) - 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: - annee_diplome: Année de diplomation visée pour le jury - ordre: Un ordre de tri - - Returns: - Une liste contenant des ``etudids`` - - Note: ex JuryPE.get_etudids_du_jury() - """ - 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"]) - for (etudid, etud) in self.cursus.items() - if etudid in etudids - ] - etudidsAvecNomTrie = sorted(etudidsAvecNom, key=lambda col: col[1]) - etudids = [etud[0] for etud in etudidsAvecNomTrie] - return etudids - - def get_etudiants(self, annee_diplome: int = None) -> dict[Identite]: + def get_etudiants(self, annee_diplome: int) -> 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} + etudids = [ + etudid + for etudid in self.cursus + if self.cursus[etudid]["diplome"] == annee_diplome and self.cursus[etudid]["abandon"] + ] + etudiants = {etudid: self.identites[etudid] 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 : - * des étudiants sur lesquels le jury va statuer (année de diplômation du jury considéré) - * des étudiants qui ne seront pas considérés dans le jury mais ont participé dans leur scolarité - à un (ou plusieurs) semestres communs aux étudiants du jury (et impacteront les classements) + def analyse_etat_etudiant( + self, etudid: int, cosemestres: dict[int, FormSemestre] + ): + """Analyse le cursus d'un étudiant pouvant être : - L'ajout consiste : + * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) + * un étudiant qui ne sera pas considéré dans le jury mais qui a participé dans sa scolarité + à un (ou plusieurs) semestres communs aux étudiants du jury (et impactera les classements) - * à insérer une entrée pour l'étudiant en mémorisant son identité, + L'analyse consiste : + + * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, avec son nom, prénom, etc... * à analyser son parcours, pour déterminer s'il n'a (ou non) 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 à ceux traiter par le jury - cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres de même année de diplomation - Note: ex JuryPE.add_etudid_to_jury() + cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres + de même année de diplomation """ - - """L'identité de l'étudiant""" identite = Identite.get_etud(etudid) - self.identites[etudid] = identite """Le cursus global de l'étudiant (restreint aux semestres APC)""" semestres_etudiant = { @@ -225,38 +194,121 @@ class EtudiantsJuryPE: "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 + "semestres": {}, + "aggregats": {}, } """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) - """Tri des semestres par n° de semestre""" + + def analyse_parcours_etudiant_dans_semestres(self, etudid): + """Structure les informations sur les semestres suivis par un + étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs + de moyennes PE. + + La structure s'appuie sur les numéros de semestre: pour chaque Si, stocke : + * le (ou les) formsemestres de numéro i qu'a suivi un étudiant (2 si redoublant) + * le dernier semestre de numéro i qu'il a suivi (1 ou 0 si pas encore suivi) + + Elle s'appuie également sur les aggrégats: pour chaque aggrégat (par ex, 3A=S1+S2+S3), + identifie les semestres que l'étudiant a suivi pour l'amener jusqu'au semestre terminal + de l'aggrégat. Ce parcours peut être : + * S1+S2+S1+S2+S3 si redoublement de la 1ère année + * S1+S2+(année de césure)+S3 si césure, ... + """ + semestres_etudiant = self.cursus[etudid]["formsemestres"] + for nom_sem in pe_tools.TOUS_LES_SEMESTRES: - i = int(nom_sem[1]) + 1 # le n° du semestre + i = int(nom_sem[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 + self.cursus[etudid]["aggregats"][nom_sem] = semestres_i + self.cursus[etudid]["semestres"][nom_sem] = get_dernier_semestre(semestres_i) - """Tri des semestres par aggrégat""" - for parcours in pe_tools.TOUS_LES_AGGREGATS: - """L'aggrégat considéré""" - noms_semestre_de_aggregat = pe_tools.PARCOURS[parcours]["aggregat"] - 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] - ) + """Tri des semestres par aggrégat et par semestre terminal""" + for aggregat in pe_tools.TOUS_LES_AGGREGATS: + self.cursus[etudid][aggregat] = {} + """L'aggrégat considéré (par ex: 3S), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" + noms_semestre_de_aggregat = pe_tools.PARCOURS[aggregat]["aggregat"] + nom_semestre_terminal = noms_semestre_de_aggregat[-1] + numero_semestre_terminal = int(nom_semestre_terminal[-1]) - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print( - parcours + "=" + str(self.cursus[etudid][parcours]), - end="", - ) + """Les semestres terminaux de l'aggrégat""" + # formsemestres_terminal = self.cursus[etudid]["aggregats"][nom_semestre_terminal] + # dernier_formsemestre_terminal = get_dernier_semestre(formsemestres_terminal) # le dernier en date + dernier_formsemestre_terminal = self.cursus[etudid]["semestres"][nom_semestre_terminal] + + # for formsem_id_term in formsemestres_terminal: + if dernier_formsemestre_terminal: # ne considérant que le dernier + formsem_id_term = list(dernier_formsemestre_terminal.keys())[0] + + formsemestre_terminal = self.cursus[etudid]["formsemestres"][formsem_id_term] + + """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" + semestres_aggreges = {} + for fid in self.cursus[etudid]["formsemestres"]: + semestre = self.cursus[etudid]["formsemestres"][fid] + if ( + semestre.semestre_id <= numero_semestre_terminal + and semestre.date_fin <= formsemestre_terminal.date_fin + ): + semestres_aggreges[fid] = semestre + + self.cursus[etudid][aggregat][formsem_id_term] = semestres_aggreges + + def get_formsemestres_terminaux_aggregat(self, aggregat: str): + """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat (pour l'aggrégat '3S' + incluant S1+S2+S3, a pour semestre terminal S3). Ces formsemestres traduisent : + + * les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI + ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA. + * les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année : + S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du + S3 (1ère session) et du S3 (2ème session) + + Args: + aggregat: L'aggrégat + + Returns: + Un dictionnaire {fid: FormSemestre(fid)} + """ + formsemestres_terminaux = {} + for etudid in self.cursus: + """Les formsemestre_id des semestres terminaux""" + fids = self.cursus[etudid][aggregat].keys() + """Pour chaque identifiant de semestre terminal, récupère le formsemestre associé""" + for fid in fids: + if fid not in formsemestres_terminaux: + formsemestres_terminaux[fid] = self.cursus[etudid][aggregat][fid][ + fid + ] + return formsemestres_terminaux + + def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): + """Pour un aggrégat donné associé à un formsemestre terminal cible, renvoie l'ensemble des semestres à + prendre en compte dans l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}. + + Fusionne les cursus individuels des étudiants, dont le cursus correspond à l'aggrégat visé. + + Args: + aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S) + formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au + dernier semestre de l'aggrégat + """ + noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] + + formsemestres = {} + for etudid in self.cursus: + cursus_etudiant = self.cursus[etudid][aggregat] + if formsemestre_id_terminal in cursus_etudiant: + formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal] + formsemestres = formsemestres | formsemestres_etudiant + return formsemestres def get_formsemestres_jury(self, semestres_recherches=None): """Ayant connaissance des étudiants dont il faut calculer les moyennes pour @@ -310,7 +362,7 @@ class EtudiantsJuryPE: nom_sem = semestres_recherches semestres = {} for etudid in self.etudiants_ids: - semestres = semestres | self.cursus[etudid][nom_sem] + semestres = semestres | self.cursus[etudid]["aggregats"][nom_sem] return semestres else: raise ValueError( @@ -368,25 +420,6 @@ def annee_diplome(identite: Identite) -> int: return None -def semestres_etudiant(etudid: int, semestre_id=None): - """La liste des semestres BUT d'un étudiant - pour un semestre_id (parmi 1, 2, 3, 4, 5, 6) 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 - - NOTE:: ex:: JuryPE.get_semestresBUT_d_un_etudiant() - TODO:: A revoir""" - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - nbre_semestres = int(pe_tools.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 arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool: """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : @@ -447,9 +480,10 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b return False -def get_dernier_semestre(semestres: dict[FormSemestre]): +def get_dernier_semestre(semestres: dict[int, FormSemestre]): """Renvoie le dernier semestre en date d'un dictionnaire - de semestres de la forme {fid: FormSemestre(fid) + de semestres de la forme {fid: FormSemestre(fid)}. + La date prise en compte est celle marquant la **fin** des semestres. Args: semestres: Un dictionnaire de semestres diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index dad54978..70fa3297 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -48,11 +48,13 @@ import os from zipfile import ZipFile import app.pe.pe_etudiant +import app.pe.pe_settag_interclasse 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.pe.pe_semestretag import SemestreTag from app.scodoc.gen_tables import GenTable, SeqGenTable import app.scodoc.sco_utils as scu @@ -117,13 +119,6 @@ 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 - self.setTagDict = ( - {} - ) # 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" @@ -137,18 +132,45 @@ class JuryPE(object): self.zipdata = io.BytesIO() self.zipfile = ZipFile(self.zipdata, "w") - "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 """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 = EtudiantsJuryPE() # Les infos sur les étudiants self.etudiants.find_etudiants(self.diplome, self.formation_id) - """Calcul des moyennes pour le jury PE""" - self.exe_calculs_juryPE() + """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" + self.semestres_taggues = compute_semestres_tag(self.etudiants) + + if pe_tools.PE_DEBUG: + """Intègre le bilan des semestres taggués au zip final""" + for fid in self.semestres_taggues: + formsemestretag = self.semestres_taggues[fid] + filename = formsemestretag.nom.replace(" ", "_") + ".csv" + pe_tools.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, formsemestretag.str_tagtable(), path="details_semestres" + ) + + """Génère les aggrégats de semestre (par ex: 1A, 3S, 5S) avec calcul + des moyennes pour le jury""" + self.aggregats_taggues = compute_aggregats_tag(self.etudiants, self.semestres_taggues) + + if pe_tools.PE_DEBUG: + """Intègre le bilan des aggrégats de semestres au zip final""" + for aggregat in self.aggregats_taggues: + for fid in self.aggregats_taggues[aggregat]: + set_tag = self.aggregats_taggues[aggregat][fid] + filename = set_tag.nom.replace(" ", "_") + ".csv" + pe_tools.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, set_tag.str_tagtable(), path="details_semestres" + ) + + """Génère les interclassements par (nom d') aggrégat""" + """Synthèse des éléments du jury PE""" if False: @@ -164,21 +186,35 @@ class JuryPE(object): 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()) + pe_tools.add_file_to_zip( + self.nom_export_zip, filename, self.xlsV2.excel() + ) # Pour debug # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug - # ------------------------------------------------------------------------------------------------------------------ - def add_file_to_zip(self, filename, data, path=""): + # Les interclassements + # -------------------- + if pe_tools.PE_DEBUG: + pe_tools.pe_print( + "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" + ) + if False: + self.get_promotags_in_jury() + + def add_file_to_zip(self, filename: str, data, path=""): """Add a file to our zip All files under NOM_EXPORT_ZIP/ path may specify a subdirectory + + Args: + 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(self.nom_export_zip, path, filename) self.zipfile.writestr(path_in_zip, data) - # ------------------------------------------------------------------------------------------------------------------ def get_zipped_data(self): """returns file-like data with a zip of all generated (CSV) files. Reset file cursor at the beginning ! @@ -189,191 +225,26 @@ class JuryPE(object): self.zipdata.seek(0) return self.zipdata - # **************************************************************************************************************** # - # Lancement des différentes actions permettant le calcul du jury PE - # **************************************************************************************************************** # - def exe_calculs_juryPE(self): - """Centralise les élements de calcul des moyennes de poursuites - d'études - """ - - """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é - # ----------------------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des moyennes sur différentes combinaisons de semestres et différents groupes d'étudiant" - ) - 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 - # -------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" - ) - if False: - self.get_promotags_in_jury() - # **************************************************************************************************************** # # Traitements des semestres impliqués dans le jury # **************************************************************************************************************** # - # ------------------------------------------------------------------------------------------------------------------ - 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 - """ - if formsemestre_id in self.semTagDict: - return - - """Créé le SemestreTag et exécute les calculs de moyennes""" - formsemestretag = pe_semestretag.SemestreTag(nom, formsemestre_id) - - self.semTagDict[formsemestre_id] = formsemestretag - - 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 - # **************************************************************************************************************** # - - # # ---------------------------------------------------------------------------------------------------------------- - # def get_antags_in_jury(self, avec_affichage_debug=True ): - # """Construit les settag associés aux années 1A et 2A du jury""" - # lesAnnees = {'1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'] } - # for nom_annee in lesAnnees: - # lesAidDesAnnees = self.get_anneeids_du_jury(annee= nom_annee) # les annee_ids des étudiants du jury - # for aid in lesAidDesAnnees: - # fidSemTagFinal = JuryPE.convert_aid_en_fid( aid ) - # lesEtudisDelAnnee = self.semTagDict[ fidSemTagFinal ].get_etudids() # les etudiants sont ceux inscrits dans le semestre final de l'année - # parcoursDesEtudiants = { etudid : self.PARCOURSINFO_DICT[etudid] for etudid in lesEtudisDelAnnee } # les parcours des etudid aka quels semestres sont à prendre en compte - # - # lesFidsDesEtudiants = self.get_formsemestreids_du_jury(lesEtudisDelAnnee, nom_annee) # les formsemestres_id à prendre en compte pour les moyennes - # # Manque-t-il des semtag associés ; si oui, les créé - # pe_tools.pe_print(aid, lesFidsDesEtudiants) - # for fid in lesFidsDesEtudiants: - # self.add_semtags_in_jury(fid, avec_affichage_debug=avec_affichage_debug) - # lesSemTagDesEtudiants = { fid: self.semTagDict[fid] for fid in lesFidsDesEtudiants } - # - # # Tous les semtag nécessaires pour ses étudiants avec ajout éventuel s'ils n'ont pas été chargés - # pe_tools.pe_print(" -> Création de l'année tagguée " + str( aid )) - # #settag_id, short_name, listeEtudId, groupe, listeSemAAggreger, ParcoursEtudDict, SemTagDict, with_comp_moy=True) - # self.anTagDict[ aid ] = pe_settag.SetTag( aid, "Annee " + self.semTagDict[fidSemTagFinal].short_name, \ - # lesEtudisDelAnnee, 'groupe', lesAnnees[ nom_annee ], parcoursDesEtudiants, lesSemTagDesEtudiants ) - # self.anTagDict[ aid ].comp_data_settag() # calcul les moyennes - # **************************************************************************************************************** # # Traitements des moyennes sur différentes combinaisons de parcours 1A, 2A, 3S et 4S, # impliquées dans le jury # **************************************************************************************************************** # - def get_settags_in_jury(self): - """Calcule les moyennes sur la totalité du parcours (S1 jusqu'à S3 ou S4) - en classant les étudiants au sein du semestre final du parcours (même S3, même S4, ...) - """ - - # Par groupe : - # combinaisons = { 'S1' : ['S1'], 'S2' : ['S2'], 'S3' : ['S3'], 'S4' : ['S4'], \ - # '1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'], - # '3S' : ['S1', 'S2', 'S3'], '4S' : ['S1', 'S2', 'S3', 'S4'] } - - # ---> sur 2 parcours DUT (cas S3 fini, cas S4 fini) - - 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.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 - 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: - pe_tools.pe_print(" - semestre final %s" % (fid)) - settag = pe_settag.SetTag( - nom, parcours=parcours - ) # Le set tag fusionnant les données - etudiants = self.semTagDict[ - fid - ].get_etudids() # Les étudiants du sem final - - # ajoute les étudiants au semestre - settag.set_Etudiants( - etudiants, - self.etudiants.cursus, - self.etudiants.identites, - nom_sem_final=self.semTagDict[fid].nom, - ) - - # manque-t-il des semestres ? Si oui, les ajoute au jurype puis au settag - for ffid in settag.get_Fids_in_settag(): - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print( - " -> ajout du semestre tagué %s" % (ffid) - ) - self.add_semestretag_in_jury(ffid) - settag.set_SemTagDict( - self.semTagDict - ) # ajoute les semestres au settag - - settag.comp_data_settag() # Calcul les moyennes, les rangs, .. - - self.setTagDict[nom][fid] = settag # Mémorise le résultat - - else: - 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)""" + """Interclasse les étudiants, (nom d') aggrégat par aggrégat, + pour fournir un classement sur la promo. + """ lesEtudids = self.etudiants.get_etudids(self.diplome) for i, nom in enumerate(pe_tools.PARCOURS.keys()): - settag = pe_settag.SetTagInterClasse(nom, diplome=self.diplome) + settag = app.pe.pe_settag_interclasse.SetTagInterClasse( + nom, diplome=self.diplome + ) nbreEtudInscrits = settag.set_Etudiants( lesEtudids, self.etudiants.cursus, self.etudiants.identites ) @@ -383,9 +254,9 @@ class JuryPE(object): "%d) %s avec interclassement sur la promo" % (i + 1, nom) ) if nom in pe_tools.TOUS_LES_SEMESTRES: - settag.set_SetTagDict(self.semTagDict) + settag.set_SetTagDict(self.semestres_taggues) else: # cas des aggrégats - settag.set_SetTagDict(self.setTagDict[nom]) + settag.set_SetTagDict(self.aggregats_taggues[nom]) settag.comp_data_settag() self.promoTagDict[nom] = settag else: @@ -435,9 +306,11 @@ class JuryPE(object): self.etudiants.cursus[etudid][nom] != None ): # Un parcours valide existe if nom in pe_tools.TOUS_LES_SEMESTRES: - tagtable = self.semTagDict[self.etudiants.cursus[etudid][nom]] + tagtable = self.semestres_taggues[ + self.etudiants.cursus[etudid][nom] + ] else: - tagtable = self.setTagDict[nom][ + tagtable = self.aggregats_taggues[nom][ self.etudiants.cursus[etudid][nom] ] for tag in tagtable.get_all_tags(): @@ -467,7 +340,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 = pe_etudiants.semestres_etudiant(etudid) + sems = self.etudiants.semestres_etudiant(etudid) infos = [] for sem in sems: @@ -505,8 +378,8 @@ class JuryPE(object): # les semestres et les aggrégats for nom_sem in pe_tools.TOUS_LES_PARCOURS: table = ( - self.semTagDict[donnees[nom_sem]].nom - if donnees[nom_sem] in self.semTagDict + self.semestres_taggues[donnees[nom_sem]].nom + if donnees[nom_sem] in self.semestres_taggues else "manquant" ) descr += [ @@ -746,7 +619,7 @@ class JuryPE(object): semtagid = self.etudiants.cursus[etudid][ nom_sem ] # le formsemestre_id du semestre taggué de l'étudiant - semtag = self.semTagDict[semtagid] + semtag = self.semestres_taggues[semtagid] chaine += "Semestre " + nom_sem + str(semtagid) + "\n" # le détail du calcul tag par tag # chaine += "Détail du calcul du tag\n" @@ -772,3 +645,112 @@ class JuryPE(object): if annees_debut: return str(min(annees_debut)) return "" + + +def compute_semestres_tag(etudiants: EtudiantsJuryPE): + """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. + Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire + des étudiants (cf. attribut etudiants.cursus). + En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé. + . + + Args: + etudiants: Un groupe d'étudiants participant au jury + + Returns: + Un dictionnaire {fid: SemestreTag(fid)} + """ + + """Création des semestres taggués, de type 'S1', 'S2', ...""" + pe_tools.pe_print("*** Création des semestres taggués") + + formsemestres = etudiants.get_formsemestres_jury( + semestres_recherches=pe_tools.TOUS_LES_SEMESTRES + ) + + semestres_tags = {} + for frmsem_id, formsemestre in formsemestres.items(): + """Choix d'un nom pour le semestretag""" + nom = "S%d %d %d-%d" % ( + formsemestre.semestre_id, + frmsem_id, + formsemestre.date_debut.year, + formsemestre.date_fin.year, + ) + + pe_tools.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}") + + """Créé le semestre_tag et exécute les calculs de moyennes""" + formsemestretag = pe_semestretag.SemestreTag(nom, frmsem_id) + + """Stocke le semestre taggué""" + semestres_tags[frmsem_id] = formsemestretag + + return semestres_tags + + +def compute_aggregats_tag( + self, etudiants: EtudiantsJuryPE, semestres_tag: dict[SemestreTag] +): + """Créé les combinaisons de semestres (aggrégat), en calculant les moyennes et les + classements par tag pour chacune. Chaque combinaison (aggrégat) est identifiée + par un formsemestre terminal. + + Par exemple : + + * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquentés 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) + + Return: + Un dictionnaire de la forme {nom_aggregat: {fid_terminal: SetTag(fid_terminal)} } + """ + + pe_tools.pe_print(" *** Création des aggrégats ") + + sets_tags = {} + + for aggregat in pe_tools.TOUS_LES_AGGREGATS: + sets_tags[aggregat] = {} + + """Semestres aggrégés""" + noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] + nom_semestre_terminal = noms_semestres_aggreges[-1] + + pe_tools.pe_print(f"* {aggregat}: " + "+".join(noms_semestres_aggreges)) + + """Les formsemestres terminaux des aggrégats""" + formsemestres_terminal = etudiants.get_formsemestres_terminaux_aggregat( + aggregat + ) + + for frmsem_id in formsemestres_terminal: + formsemestre_terminal = formsemestres_terminal[frmsem_id] + """Nom du set_tag""" + nom = "Aggrégat S%d %d %d-%d" % ( + formsemestre_terminal.semestre_id, + frmsem_id, + formsemestre_terminal.date_debut.year, + formsemestre_terminal.date_fin.year, + ) + + """Semestres à aggreger dans l'aggrégat ayant amené des étudiants jusqu'au formsemestre_terminal""" + semestres_aggreges = etudiants.get_semestres_a_aggreger(aggregat, frmsem_id) + + pe_tools.pe_print(" --> Fusion de :") + for fid in semestres_aggreges: + pe_tools.pe_print(str(semestres_aggreges[fid])) + + """Création du settag associé""" + settag = pe_settag.SetTag( + nom, formsemestre_terminal, semestres_aggreges, semestres_tag, etudiants + ) + + settag.compute_notes_cube() # Calcul les moyennes, les rangs, .. + + sets_tags[aggregat][fid] = settag # Mémorise le résultat + + return sets_tags diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index 61f4fe90..2474d0b9 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -39,7 +39,6 @@ from app.comp import moy_sem from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre from app.pe.pe_semestretag import SemestreTag -from app.pe.pe_tools import pe_print, PE_DEBUG from app.pe import pe_tagtable import pandas as pd import numpy as np @@ -169,128 +168,6 @@ class SetTag(pe_tagtable.TableTag): return sorted(set(tags)) -class SetTagInterClasse(pe_tagtable.TableTag): - """Récupère les moyennes de SetTag aggrégeant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2 - pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo - nom_combinaison = 'S1' ou '1A' - """ - - # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, diplome): - pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") - self.combinaison = nom_combinaison - self.parcoursDict = {} - - # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): - """Détermine la liste des étudiants à prendre en compte, en partant de - la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. - Renvoie le nombre d'étudiants effectivement inscrits.""" - if nom_sem_final: - self.nom += "_" + nom_sem_final - for etudid in etudiants: - if juryPEDict[etudid][self.combinaison] != None: - self.inscrlist.append(etudInfoDict[etudid]) - self.identdict[etudid] = etudInfoDict[etudid] - self.parcoursDict[etudid] = juryPEDict[etudid] - return len(self.inscrlist) - - # ------------------------------------------------------------------------------------------- - def get_Fids_in_settag(self): - """Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte - pour les moyennes, en considérant tous les étudiants inscrits""" - return list( - {self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict} - ) - - # --------------------------------------------------------------------------------------------- - def set_SetTagDict(self, SetTagDict): - """Mémorise les settag nécessaires au jury.""" - self.SetTagDict = { - fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None - } - if PE_DEBUG >= 1: - pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_data_settag(self): - """Calcule tous les données numériques relatives au settag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.taglist = self.do_taglist() - - # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) - - # Calcul des moyennes de chaque étudiant par tag - reussiteAjoutTag = {"OK": [], "KO": []} - for tag in self.taglist: - moyennes = self.get_MoyennesSetTag(tag, force=False) - res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne - reussiteAjoutTag["OK" if res else "KO"].append(tag) - if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: - pe_print( - " => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"])) - + ", ".join(reussiteAjoutTag["OK"]) - ) - if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: - pe_print( - " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) - + ", ".join(reussiteAjoutTag["KO"]) - ) - - # ------------------------------------------------------------------------------------------------------------------- - def get_etudids(self): - return list(self.identdict.keys()) - - # ------------------------------------------------------------------------------------------------------------------- - def do_taglist(self): - """Parcourt les tags des semestres taggués et les synthétise sous la forme - d'une liste en supprimant les doublons - """ - ensemble = [] - for settag in self.SetTagDict.values(): - ensemble.extend(settag.get_all_tags()) - return sorted(list(set(ensemble))) - - # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): - """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) - avec notes et coeffs deux listes""" - leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] - - note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid) - coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats( - tag, etudid - ) - return (note, coeff) - - # ------------------------------------------------------------------------------------------------------------------- - def get_MoyennesSetTag(self, tag, force=False): - """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, - et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération - appliqué dans cette moyenne. - - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - - Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} - """ - # if tag not in self.get_all_tags() : return None - - # Calcule les moyennes - lesMoyennes = [] - for ( - etudid - ) in ( - self.get_etudids() - ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag - (moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant( - tag, etudid - ) # lecture des notes associées au tag - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes - - def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): """Calcul de la moyenne par tag sur plusieurs semestres. La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles diff --git a/app/pe/pe_settag_interclasse.py b/app/pe/pe_settag_interclasse.py new file mode 100644 index 00000000..c3e6ad0f --- /dev/null +++ b/app/pe/pe_settag_interclasse.py @@ -0,0 +1,125 @@ +from app.pe import pe_tagtable +from app.pe.pe_tools import PE_DEBUG, pe_print + + +class SetTagInterClasse(pe_tagtable.TableTag): + """Récupère les moyennes de SetTag aggrégeant un même parcours (par ex un ['S1', 'S2'] + n'ayant pas fini au même S2 + pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo + nom_combinaison = 'S1' ou '1A' + """ + + # ------------------------------------------------------------------------------------------------------------------- + def __init__(self, nom_combinaison, diplome): + pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") + self.combinaison = nom_combinaison + self.parcoursDict = {} + + # ------------------------------------------------------------------------------------------- + def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): + """Détermine la liste des étudiants à prendre en compte, en partant de + la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. + Renvoie le nombre d'étudiants effectivement inscrits.""" + if nom_sem_final: + self.nom += "_" + nom_sem_final + for etudid in etudiants: + if juryPEDict[etudid][self.combinaison] != None: + self.inscrlist.append(etudInfoDict[etudid]) + self.identdict[etudid] = etudInfoDict[etudid] + self.parcoursDict[etudid] = juryPEDict[etudid] + return len(self.inscrlist) + + # ------------------------------------------------------------------------------------------- + def get_Fids_in_settag(self): + """Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte + pour les moyennes, en considérant tous les étudiants inscrits""" + return list( + {self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict} + ) + + # --------------------------------------------------------------------------------------------- + def set_SetTagDict(self, SetTagDict): + """Mémorise les settag nécessaires au jury.""" + self.SetTagDict = { + fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None + } + if PE_DEBUG >= 1: + pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) + + # ------------------------------------------------------------------------------------------------------------------- + def comp_data_settag(self): + """Calcule tous les données numériques relatives au settag""" + # Attributs relatifs aux tag pour les modules pris en compte + self.taglist = self.do_taglist() + + # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) + + # Calcul des moyennes de chaque étudiant par tag + reussiteAjoutTag = {"OK": [], "KO": []} + for tag in self.taglist: + moyennes = self.get_MoyennesSetTag(tag, force=False) + res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne + reussiteAjoutTag["OK" if res else "KO"].append(tag) + if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: + pe_print( + " => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"])) + + ", ".join(reussiteAjoutTag["OK"]) + ) + if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: + pe_print( + " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) + + ", ".join(reussiteAjoutTag["KO"]) + ) + + # ------------------------------------------------------------------------------------------------------------------- + def get_etudids(self): + return list(self.identdict.keys()) + + # ------------------------------------------------------------------------------------------------------------------- + def do_taglist(self): + """Parcourt les tags des semestres taggués et les synthétise sous la forme + d'une liste en supprimant les doublons + """ + ensemble = [] + for settag in self.SetTagDict.values(): + ensemble.extend(settag.get_all_tags()) + return sorted(list(set(ensemble))) + + # ------------------------------------------------------------------------------------------------------------------- + def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): + """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) + avec notes et coeffs deux listes""" + leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] + + note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid) + coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats( + tag, etudid + ) + return (note, coeff) + + # ------------------------------------------------------------------------------------------------------------------- + def get_MoyennesSetTag(self, tag, force=False): + """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, + et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération + appliqué dans cette moyenne. + + Force ou non le calcul de la moyenne lorsque des notes sont manquantes. + + Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} + """ + # if tag not in self.get_all_tags() : return None + + # Calcule les moyennes + lesMoyennes = [] + for ( + etudid + ) in ( + self.get_etudids() + ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag + (moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant( + tag, etudid + ) # lecture des notes associées au tag + lesMoyennes += [ + (moyenne, somme_coeffs, etudid) + ] # Un tuple (pour classement résumant les données) + return lesMoyennes diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index ad69a090..5c995095 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -83,8 +83,11 @@ class TableTag(object): # ----------------------------------------------------------------------------------------------------------- def get_all_tags(self): - """Renvoie la liste des tags du semestre triée par ordre alphabétique""" - # return self.taglist + """Liste des tags de la table, triée par ordre alphabétique + + Returns: + Liste de tags triés par ordre alphabétique + """ return sorted(self.moyennes_tags.keys()) @@ -270,6 +273,20 @@ class TableTag(object): str_moytag = classmethod(str_moytag) # ----------------------------------------------------------------------- + def df_tagtable(self): + """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags + + Returns: + Un dataframe etudids x tag (avec tag par ordre alphabétique) + """ + tags = self.get_all_tags() + if tags: + dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags} + df = pd.DataFrame(dict_series) + return df + else: + return None + def str_tagtable(self): """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags.""" diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index a2264fc0..b34ec3be 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -164,6 +164,10 @@ TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] TOUS_LES_PARCOURS = list(PARCOURS.keys()) + + + + # ---------------------------------------------------------------------------------------- def print_semestres_description(sems, avec_affichage_debug=False): """Dediee a l'affichage d'un semestre pour debug du module""" From 02976c99962d89a5077ae4b9a63fc2913024063f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 23 Jan 2024 09:05:52 +0100 Subject: [PATCH 14/23] =?UTF-8?q?Etat=20temporaire=20n=C2=B02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 106 +++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index cda39fa1..29d5f597 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -57,11 +57,13 @@ class EtudiantsJuryPE: "Les identités des étudiants traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT - "Les cursus (semestres suivis, abandons, ...) des étudiants" + "Les cursus (semestres suivis, abandons, dernier S1, S2, ...) des étudiants" self.cursus = {} + """Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants""" + self.aggregats = {} "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" - self.etudiants_jury_ids = {} + self.diplomes_ids = {} "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" self.etudiants_ids = {} @@ -116,7 +118,7 @@ class EtudiantsJuryPE: pe_tools.pe_print() """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" - self.etudiants_jury_ids = self.get_etudiants(annee_diplome) + self.diplomes_ids = self.get_etudiants(annee_diplome) """Les étudiants dont il faut calculer les moyennes""" self.etudiants_ids = {etudid for etudid in self.cursus} @@ -126,13 +128,13 @@ class EtudiantsJuryPE: # Synthèse pe_tools.pe_print( - f" => {len(self.etudiants_jury_ids)} étudiants à diplômer en {annee_diplome}" + f" => {len(self.diplomes_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]]) + + ", ".join([str(etudid) for etudid in list(self.diplomes_ids)[:10]]) ) pe_tools.pe_print( f" => semestres dont il faut calculer les moyennes : " @@ -194,8 +196,6 @@ class EtudiantsJuryPE: "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 - "semestres": {}, - "aggregats": {}, } """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" @@ -207,18 +207,22 @@ class EtudiantsJuryPE: étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs de moyennes PE. - La structure s'appuie sur les numéros de semestre: pour chaque Si, stocke : - * le (ou les) formsemestres de numéro i qu'a suivi un étudiant (2 si redoublant) - * le dernier semestre de numéro i qu'il a suivi (1 ou 0 si pas encore suivi) + Cette structuration (cf. attribut self.cursus) s'appuie sur les numéros de semestre: pour chaque Si, stocke : + le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les + interclassement par semestre dans la promo. - Elle s'appuie également sur les aggrégats: pour chaque aggrégat (par ex, 3A=S1+S2+S3), - identifie les semestres que l'étudiant a suivi pour l'amener jusqu'au semestre terminal - de l'aggrégat. Ce parcours peut être : - * S1+S2+S1+S2+S3 si redoublement de la 1ère année - * S1+S2+(année de césure)+S3 si césure, ... + Elle calcule également sur les aggrégats (cf. attribut self.aggregats): + + * pour un semestre de type Si, elle stocke le (ou les) formsemestres de numéro i qu'a suivi un étudiant + (2 si redoublant) dans self.aggregats + * pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie les semestres que l'étudiant + a suivi pour l'amener jusqu'au semestre terminal de l'aggrégat. Ce parcours peut être : + ** S1+S2+S1+S2+S3 si redoublement de la 1ère année + ** S1+S2+(année de césure)+S3 si césure, ... """ semestres_etudiant = self.cursus[etudid]["formsemestres"] + """Tri des semestres par numéro de semestre""" for nom_sem in pe_tools.TOUS_LES_SEMESTRES: i = int(nom_sem[1]) # le n° du semestre semestres_i = { @@ -226,22 +230,21 @@ class EtudiantsJuryPE: for fid in semestres_etudiant if semestres_etudiant[fid].semestre_id == i } # les semestres de n°i de l'étudiant - self.cursus[etudid]["aggregats"][nom_sem] = semestres_i - self.cursus[etudid]["semestres"][nom_sem] = get_dernier_semestre(semestres_i) + self.aggregats[etudid][nom_sem] = semestres_i + self.cursus[etudid][nom_sem] = get_dernier_semestre(semestres_i) """Tri des semestres par aggrégat et par semestre terminal""" for aggregat in pe_tools.TOUS_LES_AGGREGATS: - self.cursus[etudid][aggregat] = {} + self.aggregat[etudid][aggregat] = {} + """L'aggrégat considéré (par ex: 3S), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" noms_semestre_de_aggregat = pe_tools.PARCOURS[aggregat]["aggregat"] nom_semestre_terminal = noms_semestre_de_aggregat[-1] numero_semestre_terminal = int(nom_semestre_terminal[-1]) - """Les semestres terminaux de l'aggrégat""" - # formsemestres_terminal = self.cursus[etudid]["aggregats"][nom_semestre_terminal] - # dernier_formsemestre_terminal = get_dernier_semestre(formsemestres_terminal) # le dernier en date - dernier_formsemestre_terminal = self.cursus[etudid]["semestres"][nom_semestre_terminal] + """Le formsemestre terminal de l'aggrégat (par ex: son dernier S3 en date)""" + dernier_formsemestre_terminal = self.cursus[etudid][nom_semestre_terminal] # for formsem_id_term in formsemestres_terminal: if dernier_formsemestre_terminal: # ne considérant que le dernier @@ -259,7 +262,7 @@ class EtudiantsJuryPE: ): semestres_aggreges[fid] = semestre - self.cursus[etudid][aggregat][formsem_id_term] = semestres_aggreges + self.aggregat[etudid][aggregat][formsem_id_term] = semestres_aggreges def get_formsemestres_terminaux_aggregat(self, aggregat: str): """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat (pour l'aggrégat '3S' @@ -422,17 +425,32 @@ def annee_diplome(identite: Identite) -> int: def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool: - """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : + """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : - * d'une réorientation à l'initiative du jury de semestre ou d'une démission ; dans ce cas, utilise les - décisions prises dans les jury de semestres (code NAR pour réorienté & DEM pour démissionnaire) + * d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait + utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD, + mais pas nécessaire ici) - * d'un arrêt volontaire : l'étudiant disparait des listes d'inscrit, sans - pour autant avoir été indiqué NAR ou DEM. Dans ce cas, 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. + * d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour autant avoir été indiqué NAR ou DEM). + + Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres + (semestres conduisant à la même année de diplômation) connu dans Scodoc. + + Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une + instance des S5 qui conduisent à la diplomation dans l'année visée. S'il n'est que dans un S4, il a sans doute + arrêté. A moins qu'il ne soit parti à l'étranger et là, pas de notes. + TODO:: Cas de l'étranger, à coder/tester + + **Attention** : Cela suppose que toutes les instances d'un semestre donné (par ex: toutes les instances de S6 + accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme + ayant abandonnés) + TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre + + Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et + regarde s'il n'existe pas parmi les semestres existants dans Scodoc un semestre : + * dont les dates sont postérieures (en terme de date de début) + * de n° au moins égal à celui de son dernier semestre valide (S5 -> S5 ou S5 -> S6) + dans lequel il aurait pu s'inscrire mais ne l'a pas fait. Args: identite: L'identité d'un étudiant @@ -441,30 +459,22 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b Returns: Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? - TODO:: A reprendre avec l'accès aux résultats des jury prévu par Scodoc9 + TODO:: A reprendre pour le cas des étudiants à l'étranger """ etudid = identite.etudid reponse = False etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] (code, parcours) = sco_report.get_code_cursus_etud(etud) - # Est-il réorienté ou démissionnaire ? - if ( - len(codes_cursus.CODES_SEM_REO & set(parcours.values())) > 0 - ): # Eliminé car NAR apparait dans le parcours - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car réorienté (NAR)") - return True - - if "DEM" in list(parcours.values()): # Eliminé car DEM - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car DEM") - return True - - # A-t-il arrêté volontairement sa formation ? + """Son dernier semestre en date""" dernier_formsemestre = identite.get_formsemestres()[0] + numero_dernier_formsemestre = dernier_formsemestre.semestre_id - # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? + """Les numéro de semestres possible dans lesquels il pourrait s'incrire""" + if numero_dernier_formsemestre % 2 == 1: # semestre impair + numeros_possibles = list(range(numero_dernier_formsemestre+1, pe_tools.)) + + """Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?""" formsestres_superieurs_possibles = [] for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits if ( From 8b3efe9dadabd6ba631dd6f749aaf3a118f8f691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 23 Jan 2024 09:54:30 +0100 Subject: [PATCH 15/23] =?UTF-8?q?Am=C3=A9liore=20d=C3=A9tection=20=C3=A9tu?= =?UTF-8?q?diants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 62 +++++++++++++++++++++++++++---------------- app/pe/pe_tools.py | 7 +++++ 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 29d5f597..81c3e7ef 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -85,6 +85,7 @@ class EtudiantsJuryPE: """ "Les cosemestres donnant lieu à même année de diplome" cosemestres = pe_tools.get_cosemestres_diplomants(annee_diplome, None) + self.cosemestres = cosemestres pe_tools.pe_print( "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) ) @@ -121,7 +122,7 @@ class EtudiantsJuryPE: self.diplomes_ids = self.get_etudiants(annee_diplome) """Les étudiants dont il faut calculer les moyennes""" - self.etudiants_ids = {etudid for etudid in self.cursus} + self.etudiants_ids = {etudid for etudid in self.identites} """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" self.formsemestres_jury_ids = self.get_formsemestres_jury() @@ -130,8 +131,9 @@ class EtudiantsJuryPE: pe_tools.pe_print( f" => {len(self.diplomes_ids)} étudiants à diplômer en {annee_diplome}" ) - nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_ids) + nbre_abandons = len(self.etudiants_ids) - len(self.diplomes_ids) pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") + pe_tools.pe_print(f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne") pe_tools.pe_print( f" => quelques étudiants futurs diplômés : " + ", ".join([str(etudid) for etudid in list(self.diplomes_ids)[:10]]) @@ -155,15 +157,13 @@ class EtudiantsJuryPE: etudids = [ etudid for etudid in self.cursus - if self.cursus[etudid]["diplome"] == annee_diplome and self.cursus[etudid]["abandon"] + if self.cursus[etudid]["diplome"] == annee_diplome + and self.cursus[etudid]["abandon"] == False ] etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants - - def analyse_etat_etudiant( - self, etudid: int, cosemestres: dict[int, FormSemestre] - ): + def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): """Analyse le cursus d'un étudiant pouvant être : * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) @@ -194,14 +194,15 @@ class EtudiantsJuryPE: self.cursus[etudid] = { "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury + "nom": identite.nom, "diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme "formsemestres": semestres_etudiant, # les semestres de l'étudiant + "abandon": False, # va être traité en dessous } """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) - def analyse_parcours_etudiant_dans_semestres(self, etudid): """Structure les informations sur les semestres suivis par un étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs @@ -222,6 +223,8 @@ class EtudiantsJuryPE: """ semestres_etudiant = self.cursus[etudid]["formsemestres"] + self.aggregats[etudid] = {} + """Tri des semestres par numéro de semestre""" for nom_sem in pe_tools.TOUS_LES_SEMESTRES: i = int(nom_sem[1]) # le n° du semestre @@ -233,10 +236,9 @@ class EtudiantsJuryPE: self.aggregats[etudid][nom_sem] = semestres_i self.cursus[etudid][nom_sem] = get_dernier_semestre(semestres_i) - """Tri des semestres par aggrégat et par semestre terminal""" for aggregat in pe_tools.TOUS_LES_AGGREGATS: - self.aggregat[etudid][aggregat] = {} + self.aggregats[etudid][aggregat] = {} """L'aggrégat considéré (par ex: 3S), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" noms_semestre_de_aggregat = pe_tools.PARCOURS[aggregat]["aggregat"] @@ -247,10 +249,12 @@ class EtudiantsJuryPE: dernier_formsemestre_terminal = self.cursus[etudid][nom_semestre_terminal] # for formsem_id_term in formsemestres_terminal: - if dernier_formsemestre_terminal: # ne considérant que le dernier + if dernier_formsemestre_terminal: # ne considérant que le dernier formsem_id_term = list(dernier_formsemestre_terminal.keys())[0] - formsemestre_terminal = self.cursus[etudid]["formsemestres"][formsem_id_term] + formsemestre_terminal = self.cursus[etudid]["formsemestres"][ + formsem_id_term + ] """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" semestres_aggreges = {} @@ -262,7 +266,11 @@ class EtudiantsJuryPE: ): semestres_aggreges[fid] = semestre - self.aggregat[etudid][aggregat][formsem_id_term] = semestres_aggreges + self.aggregats[etudid][aggregat][formsem_id_term] = semestres_aggreges + + """Vérifications""" + dernier_semestre_aggregat = get_dernier_semestre(semestres_aggreges) + assert dernier_semestre_aggregat == dernier_formsemestre_terminal def get_formsemestres_terminaux_aggregat(self, aggregat: str): """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat (pour l'aggrégat '3S' @@ -365,7 +373,7 @@ class EtudiantsJuryPE: nom_sem = semestres_recherches semestres = {} for etudid in self.etudiants_ids: - semestres = semestres | self.cursus[etudid]["aggregats"][nom_sem] + semestres = semestres | self.aggregats[etudid][nom_sem] return semestres else: raise ValueError( @@ -423,7 +431,6 @@ def annee_diplome(identite: Identite) -> int: return None - def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool: """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : @@ -460,27 +467,36 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? TODO:: A reprendre pour le cas des étudiants à l'étranger + TODO:: A reprendre si BUT avec semestres décalés """ etudid = identite.etudid - reponse = False - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - (code, parcours) = sco_report.get_code_cursus_etud(etud) """Son dernier semestre en date""" dernier_formsemestre = identite.get_formsemestres()[0] numero_dernier_formsemestre = dernier_formsemestre.semestre_id """Les numéro de semestres possible dans lesquels il pourrait s'incrire""" - if numero_dernier_formsemestre % 2 == 1: # semestre impair - numeros_possibles = list(range(numero_dernier_formsemestre+1, pe_tools.)) + # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) + if numero_dernier_formsemestre % 2 == 1: + numeros_possibles = list( + range(numero_dernier_formsemestre + 1, pe_tools.NBRE_SEMESTRES_DIPLOMANT) + ) + # semestre pair => passage en année supérieure ou redoublement + else: # + numeros_possibles = list( + range( + max(numero_dernier_formsemestre - 1, 1), + pe_tools.NBRE_SEMESTRES_DIPLOMANT, + ) + ) """Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?""" formsestres_superieurs_possibles = [] for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits if ( - sem.formsemestre_id != dernier_formsemestre.formsemestre_id + fid != dernier_formsemestre.formsemestre_id + and sem.semestre_id in numeros_possibles and sem.date_debut.year >= dernier_formsemestre.date_debut.year - and sem.semestre_id > dernier_formsemestre.semestre_id ): # 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 formsestres_superieurs_possibles.append(fid) @@ -491,7 +507,7 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b def get_dernier_semestre(semestres: dict[int, FormSemestre]): - """Renvoie le dernier semestre en date d'un dictionnaire + """Renvoie le dernier semestre en date (de fin) d'un dictionnaire de semestres de la forme {fid: FormSemestre(fid)}. La date prise en compte est celle marquant la **fin** des semestres. diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index b34ec3be..dba1c00f 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -76,6 +76,12 @@ PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" # ---------------------------------------------------------------------------------------- +""" +Descriptif d'un parcours classique BUT + +TODO:: A améliorer si BUT en moins de 6 semestres +""" + PARCOURS = { "S1": { "aggregat": ["S1"], @@ -156,6 +162,7 @@ PARCOURS = { "affichage_long": "BUT (tout semestre inclus)", }, } +NBRE_SEMESTRES_DIPLOMANT = 6 AGGREGAT_DIPLOMANT = ( "6S" # aggrégat correspondant à la totalité des notes pour le diplôme ) From e3cde87a0f469a33a350251d4a0933dbe93eafcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 23 Jan 2024 18:44:44 +0100 Subject: [PATCH 16/23] =?UTF-8?q?Adaptation=20diverses=20pour=20la=20gesti?= =?UTF-8?q?on=20des=20aggr=C3=A9gats=20(dont=20les=20redoublements=20de=20?= =?UTF-8?q?semestre)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 115 ++++++++++++++++++++++++++++++------------ app/pe/pe_jurype.py | 29 +++++++---- app/pe/pe_tools.py | 31 ------------ 3 files changed, 100 insertions(+), 75 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 81c3e7ef..880de957 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -52,8 +52,14 @@ import datetime class EtudiantsJuryPE: """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" - def __init__(self): - """ """ + def __init__(self, annee_diplome: int): + """ + + Args: + annee_diplome: L'année de diplomation + """ + + self.annee_diplome = annee_diplome "Les identités des étudiants traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT @@ -70,21 +76,20 @@ class EtudiantsJuryPE: "Les formsemestres dont il faut calculer les moyennes par tag" self.formsemestres_jury_ids = {} - def find_etudiants(self, annee_diplome: int, formation_id: int): + def find_etudiants(self, formation_id: int): """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant de manière automatique par rapport à leur année de diplomation ``annee_diplome`` dans la formation ``formation_id``. Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE. - Args: - annee_diplome: L'année de diplomation + formation_id: L'identifiant de la formation (inutilisé) *Remarque* : ex: JuryPE.get_etudiants_in_jury() """ "Les cosemestres donnant lieu à même année de diplome" - cosemestres = pe_tools.get_cosemestres_diplomants(annee_diplome, None) + cosemestres = pe_tools.get_cosemestres_diplomants(self.annee_diplome, None) self.cosemestres = cosemestres pe_tools.pe_print( "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) @@ -119,7 +124,7 @@ class EtudiantsJuryPE: pe_tools.pe_print() """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" - self.diplomes_ids = self.get_etudiants(annee_diplome) + self.diplomes_ids = self.get_etudiants_diplomes() """Les étudiants dont il faut calculer les moyennes""" self.etudiants_ids = {etudid for etudid in self.identites} @@ -129,11 +134,13 @@ class EtudiantsJuryPE: # Synthèse pe_tools.pe_print( - f" => {len(self.diplomes_ids)} étudiants à diplômer en {annee_diplome}" + f" => {len(self.diplomes_ids)} étudiants à diplômer en {self.annee_diplome}" ) nbre_abandons = len(self.etudiants_ids) - len(self.diplomes_ids) pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") - pe_tools.pe_print(f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne") + pe_tools.pe_print( + f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" + ) pe_tools.pe_print( f" => quelques étudiants futurs diplômés : " + ", ".join([str(etudid) for etudid in list(self.diplomes_ids)[:10]]) @@ -142,14 +149,14 @@ class EtudiantsJuryPE: f" => semestres dont il faut calculer les moyennes : " + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) ) + # Les abandons : + # sorted([etudiants.cursus[etudid]['nom'] for etudid in etudiants.cursus if etudid not in etudiants.diplomes_ids]) - def get_etudiants(self, annee_diplome: int) -> dict[Identite]: + def get_etudiants_diplomes(self) -> dict[int, 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é. - Args: - annee_diplome: Année de diplomation visée pour le jury Returns: Un dictionnaire `{etudid: Identite(etudid)}` @@ -157,7 +164,7 @@ class EtudiantsJuryPE: etudids = [ etudid for etudid in self.cursus - if self.cursus[etudid]["diplome"] == annee_diplome + if self.cursus[etudid]["diplome"] == self.annee_diplome and self.cursus[etudid]["abandon"] == False ] etudiants = {etudid: self.identites[etudid] for etudid in etudids} @@ -203,7 +210,7 @@ class EtudiantsJuryPE: """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) - def analyse_parcours_etudiant_dans_semestres(self, etudid): + def analyse_parcours_etudiant_dans_semestres(self, etudid: int): """Structure les informations sur les semestres suivis par un étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs de moyennes PE. @@ -220,21 +227,60 @@ class EtudiantsJuryPE: a suivi pour l'amener jusqu'au semestre terminal de l'aggrégat. Ce parcours peut être : ** S1+S2+S1+S2+S3 si redoublement de la 1ère année ** S1+S2+(année de césure)+S3 si césure, ... + + Par ex: M. N..z (redoublant en 2ème année) au moment de son 2ème S3 : + {'1A': {26: {18: , + 26: }}, + '2A': {79: {18: , + 26: , + 56: , + 79: }}, + '3A': {}, + '3S': {112: {18: , + 26: , + 56: , + 112: }}, + '4S': {79: {18: , + 26: , + 56: , + 79: }}, + '5S': {}, + '6S': {}, + 'S1': {18: {18: }}, + 'S2': {26: {26: }}, + 'S3': {112: {56: , + 112: }}, + 'S4': {79: {79: }}, + 'S5': {}, + 'S6': {} + } """ semestres_etudiant = self.cursus[etudid]["formsemestres"] + """Ne conserve que les semestres qui l'auraient amené à être diplomé l'année visée""" + semestres_significatifs = {} + for fid in semestres_etudiant: + semestre = semestres_etudiant[fid] + if pe_tools.get_annee_diplome_semestre(semestre) <= self.annee_diplome: + semestres_significatifs[fid] = semestre + self.aggregats[etudid] = {} """Tri des semestres par numéro de semestre""" for nom_sem in pe_tools.TOUS_LES_SEMESTRES: i = int(nom_sem[1]) # le n° du semestre semestres_i = { - fid: semestres_etudiant[fid] - for fid in semestres_etudiant - if semestres_etudiant[fid].semestre_id == i + fid: semestres_significatifs[fid] + for fid in semestres_significatifs + if semestres_significatifs[fid].semestre_id == i } # les semestres de n°i de l'étudiant - self.aggregats[etudid][nom_sem] = semestres_i - self.cursus[etudid][nom_sem] = get_dernier_semestre(semestres_i) + dernier_semestre_i = get_dernier_semestre(semestres_i) + self.cursus[etudid][nom_sem] = dernier_semestre_i + self.aggregats[etudid][nom_sem] = {} + if dernier_semestre_i: + fid_dernier_semestre_i = list(dernier_semestre_i.keys())[0] + self.aggregats[etudid][nom_sem][fid_dernier_semestre_i] = semestres_i + """Tri des semestres par aggrégat et par semestre terminal""" for aggregat in pe_tools.TOUS_LES_AGGREGATS: @@ -258,8 +304,8 @@ class EtudiantsJuryPE: """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" semestres_aggreges = {} - for fid in self.cursus[etudid]["formsemestres"]: - semestre = self.cursus[etudid]["formsemestres"][fid] + for fid in semestres_significatifs: + semestre = semestres_significatifs[fid] if ( semestre.semestre_id <= numero_semestre_terminal and semestre.date_fin <= formsemestre_terminal.date_fin @@ -273,8 +319,9 @@ class EtudiantsJuryPE: assert dernier_semestre_aggregat == dernier_formsemestre_terminal def get_formsemestres_terminaux_aggregat(self, aggregat: str): - """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat (pour l'aggrégat '3S' - incluant S1+S2+S3, a pour semestre terminal S3). Ces formsemestres traduisent : + """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat + (pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3). + Ces formsemestres traduisent : * les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA. @@ -289,15 +336,15 @@ class EtudiantsJuryPE: Un dictionnaire {fid: FormSemestre(fid)} """ formsemestres_terminaux = {} - for etudid in self.cursus: - """Les formsemestre_id des semestres terminaux""" - fids = self.cursus[etudid][aggregat].keys() - """Pour chaque identifiant de semestre terminal, récupère le formsemestre associé""" - for fid in fids: - if fid not in formsemestres_terminaux: - formsemestres_terminaux[fid] = self.cursus[etudid][aggregat][fid][ - fid - ] + for etudid in self.aggregats: + if self.aggregats[etudid][aggregat]: + print(self.aggregats[etudid][aggregat]) + """Le formsemestre_id du semestre terminal de l'étudiant (s'il existe)""" + fid = list(self.aggregats[etudid][aggregat].keys())[0] + """Le formsemestre associé (en le prenant dans l'aggrégat)""" + formsemestres_terminaux[fid] = self.aggregats[etudid][aggregat][fid][ + fid + ] return formsemestres_terminaux def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): @@ -373,7 +420,9 @@ class EtudiantsJuryPE: nom_sem = semestres_recherches semestres = {} for etudid in self.etudiants_ids: - semestres = semestres | self.aggregats[etudid][nom_sem] + for sem_terminal in self.aggregats[etudid]: + for sem in self.aggregats[etudid][sem_terminal]: + semestres = semestres | self.aggregats[etudid][sem_terminal][sem] return semestres else: raise ValueError( diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 70fa3297..627dc68a 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -138,8 +138,8 @@ class JuryPE(object): pe_tools.pe_print( f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}" ) - self.etudiants = EtudiantsJuryPE() # Les infos sur les étudiants - self.etudiants.find_etudiants(self.diplome, self.formation_id) + self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants + self.etudiants.find_etudiants(self.formation_id) """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" self.semestres_taggues = compute_semestres_tag(self.etudiants) @@ -689,10 +689,8 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE): return semestres_tags -def compute_aggregats_tag( - self, etudiants: EtudiantsJuryPE, semestres_tag: dict[SemestreTag] -): - """Créé les combinaisons de semestres (aggrégat), en calculant les moyennes et les +def compute_aggregats_tag(etudiants: EtudiantsJuryPE, semestres_tag: dict[SemestreTag]): + """Créé les combinaisons de semestres (aggrégats), en calculant les moyennes et les classements par tag pour chacune. Chaque combinaison (aggrégat) est identifiée par un formsemestre terminal. @@ -701,6 +699,11 @@ def compute_aggregats_tag( * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquentés les étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison. + * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les + notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en + date (le S2 redoublé par les redoublants est forcément antérieur) + + Args: etudiants: Les données des étudiants semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) @@ -713,12 +716,15 @@ def compute_aggregats_tag( sets_tags = {} - for aggregat in pe_tools.TOUS_LES_AGGREGATS: + for aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: sets_tags[aggregat] = {} """Semestres aggrégés""" - noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] - nom_semestre_terminal = noms_semestres_aggreges[-1] + if aggregat in pe_tools.TOUS_LES_SEMESTRES: # par ex. 'S2' + noms_semestres_aggreges = [ aggregat ] + else: # par ex. "5S" + noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] + nom_semestre_terminal = noms_semestres_aggreges[-1] pe_tools.pe_print(f"* {aggregat}: " + "+".join(noms_semestres_aggreges)) @@ -730,9 +736,10 @@ def compute_aggregats_tag( for frmsem_id in formsemestres_terminal: formsemestre_terminal = formsemestres_terminal[frmsem_id] """Nom du set_tag""" - nom = "Aggrégat S%d %d %d-%d" % ( - formsemestre_terminal.semestre_id, + nom = "Aggrégat %s %d %s %d-%d" % ( + aggregat, frmsem_id, + "+".join(noms_semestres_aggreges), formsemestre_terminal.date_debut.year, formsemestre_terminal.date_fin.year, ) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index dba1c00f..c5ada421 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -170,37 +170,6 @@ TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"] TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] TOUS_LES_PARCOURS = list(PARCOURS.keys()) - - - - - -# ---------------------------------------------------------------------------------------- -def print_semestres_description(sems, avec_affichage_debug=False): - """Dediee a l'affichage d'un semestre pour debug du module""" - - def chaine_semestre(sem): - desc = ( - "S" - + str(sem["semestre_id"]) - + " " - + sem["modalite"] - + " " - + sem["anneescolaire"] - ) - desc += " (" + sem["annee_debut"] + "/" + sem["annee_fin"] + ") " - desc += str(sem["formation_id"]) + " / " + str(sem["formsemestre_id"]) - desc += " - " + sem["titre_num"] - return desc - - if avec_affichage_debug == True: - if isinstance(sems, list): - for sem in sems: - pe_print(chaine_semestre(sem)) - else: - pe_print(chaine_semestre(sems)) - - # ---------------------------------------------------------------------------------------- def calcul_age(born): """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. From 8477dc96cabbd6c30c5f98ffb731fbc22c218eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 23 Jan 2024 19:08:54 +0100 Subject: [PATCH 17/23] =?UTF-8?q?Etat=20interm=C3=A9diaire=20n=C2=B03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jurype.py | 97 ++++++++++++++++----------------- app/pe/pe_settag_interclasse.py | 19 ++++--- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 627dc68a..0d183165 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -156,7 +156,9 @@ class JuryPE(object): """Génère les aggrégats de semestre (par ex: 1A, 3S, 5S) avec calcul des moyennes pour le jury""" - self.aggregats_taggues = compute_aggregats_tag(self.etudiants, self.semestres_taggues) + self.aggregats_taggues = compute_aggregats_tag( + self.etudiants, self.semestres_taggues + ) if pe_tools.PE_DEBUG: """Intègre le bilan des aggrégats de semestres au zip final""" @@ -170,7 +172,16 @@ class JuryPE(object): ) """Génère les interclassements par (nom d') aggrégat""" - + self.aggregats_taggues_interclasses = compute_interclassements( + self.etudiants, # + self.aggregats_taggues, + ) + # Les interclassements + # -------------------- + if pe_tools.PE_DEBUG: + pe_tools.pe_print( + "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" + ) """Synthèse des éléments du jury PE""" if False: @@ -193,15 +204,6 @@ class JuryPE(object): # Pour debug # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug - # Les interclassements - # -------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" - ) - if False: - self.get_promotags_in_jury() - def add_file_to_zip(self, filename: str, data, path=""): """Add a file to our zip All files under NOM_EXPORT_ZIP/ @@ -229,42 +231,6 @@ class JuryPE(object): # Traitements des semestres impliqués dans le jury # **************************************************************************************************************** # - # **************************************************************************************************************** # - # Traitements des moyennes sur différentes combinaisons de parcours 1A, 2A, 3S et 4S, - # impliquées dans le jury - # **************************************************************************************************************** # - - def get_promotags_in_jury(self): - """Interclasse les étudiants, (nom d') aggrégat par aggrégat, - pour fournir un classement sur la promo. - """ - - lesEtudids = self.etudiants.get_etudids(self.diplome) - - for i, nom in enumerate(pe_tools.PARCOURS.keys()): - settag = app.pe.pe_settag_interclasse.SetTagInterClasse( - nom, diplome=self.diplome - ) - nbreEtudInscrits = settag.set_Etudiants( - 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 pe_tools.TOUS_LES_SEMESTRES: - settag.set_SetTagDict(self.semestres_taggues) - else: # cas des aggrégats - settag.set_SetTagDict(self.aggregats_taggues[nom]) - settag.comp_data_settag() - self.promoTagDict[nom] = settag - else: - 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 # ***************************************************************************************************************** @@ -720,9 +686,9 @@ def compute_aggregats_tag(etudiants: EtudiantsJuryPE, semestres_tag: dict[Semest sets_tags[aggregat] = {} """Semestres aggrégés""" - if aggregat in pe_tools.TOUS_LES_SEMESTRES: # par ex. 'S2' - noms_semestres_aggreges = [ aggregat ] - else: # par ex. "5S" + if aggregat in pe_tools.TOUS_LES_SEMESTRES: # par ex. 'S2' + noms_semestres_aggreges = [aggregat] + else: # par ex. "5S" noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] nom_semestre_terminal = noms_semestres_aggreges[-1] @@ -761,3 +727,34 @@ def compute_aggregats_tag(etudiants: EtudiantsJuryPE, semestres_tag: dict[Semest sets_tags[aggregat][fid] = settag # Mémorise le résultat return sets_tags + + +def compute_interclassements( + etudiants: EtudiantsJuryPE, aggregats_taggues: dict[str, dict] +): + """Interclasse les étudiants, (nom d') aggrégat par aggrégat, + pour fournir un classement sur la promo. Le classement est établit au regard du nombre + d'étudiants ayant participé au même aggrégat. + """ + etudiants_diplomes = etudiants.get_etudiants_diplomes() + + """ + for i, nom in enumerate(pe_tools.PARCOURS.keys()): + settag = app.pe.pe_settag_interclasse.SetTagInterClasse(nom, diplome=diplome) + nbreEtudInscrits = settag.set_Etudiants( + lesEtudids, self.etudiants.cursus, self.etudiants.identites + ) + if nbreEtudInscrits > 0: + pe_tools.pe_print("%d) %s avec interclassement sur la promo" % (i + 1, nom)) + if nom in pe_tools.TOUS_LES_SEMESTRES: + settag.set_SetTagDict(self.semestres_taggues) + else: # cas des aggrégats + settag.set_SetTagDict(self.aggregats_taggues[nom]) + settag.comp_data_settag() + self.promoTagDict[nom] = settag + else: + pe_tools.pe_print( + "%d) Pas d'interclassement %s sur la promo faute de notes" + % (i + 1, nom) + ) + """ \ No newline at end of file diff --git a/app/pe/pe_settag_interclasse.py b/app/pe/pe_settag_interclasse.py index c3e6ad0f..933383c3 100644 --- a/app/pe/pe_settag_interclasse.py +++ b/app/pe/pe_settag_interclasse.py @@ -1,19 +1,22 @@ from app.pe import pe_tagtable from app.pe.pe_tools import PE_DEBUG, pe_print +import app.pe.pe_etudiant as pe_etudiant class SetTagInterClasse(pe_tagtable.TableTag): - """Récupère les moyennes de SetTag aggrégeant un même parcours (par ex un ['S1', 'S2'] - n'ayant pas fini au même S2 - pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo - nom_combinaison = 'S1' ou '1A' + """Interclasse les étudiants d'une promo (ceux diplômé) par aggrégat de même nom + (par ex: un "3S"), pour stocker leur moyenne et fournir un classement de "promo". + + Les """ # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, diplome): - pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") - self.combinaison = nom_combinaison - self.parcoursDict = {} + def __init__(self, nom:str, etudiants: EtudiantsJuryPE, aggregats_taggues: dict[str, dict]): + """""" + pe_tagtable.TableTag.__init__(self, nom) + self.etudiants = etudiants + self.aggregats_taggues = aggregats_taggues # Les moyennes par aggrégats + # ------------------------------------------------------------------------------------------- def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): From 283daae4d9de21e57eddff46e41a43c3e302574d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 24 Jan 2024 15:37:50 +0100 Subject: [PATCH 18/23] =?UTF-8?q?Etat=20interm=C3=A9diaire=20n=C2=B04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 245 ++++++------------ app/pe/pe_interclassetag.py | 124 +++++++++ app/pe/pe_jurype.py | 89 +++---- app/pe/pe_settag_interclasse.py | 2 +- app/pe/pe_trajectoire.py | 149 +++++++++++ app/pe/{pe_settag.py => pe_trajectoiretag.py} | 43 +-- 6 files changed, 420 insertions(+), 232 deletions(-) create mode 100644 app/pe/pe_interclassetag.py create mode 100644 app/pe/pe_trajectoire.py rename app/pe/{pe_settag.py => pe_trajectoiretag.py} (86%) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 880de957..1c82d238 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -39,15 +39,6 @@ Created on 17/01/2024 import app.pe.pe_tools as pe_tools from app.models import FormSemestre, Identite from app.pe.pe_tools import pe_print -from app.scodoc import ( - sco_etud, - codes_cursus, - sco_formsemestre, - sco_formsemestre_inscriptions, - sco_report, -) -import datetime - class EtudiantsJuryPE: """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" @@ -63,19 +54,16 @@ class EtudiantsJuryPE: "Les identités des étudiants traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT - "Les cursus (semestres suivis, abandons, dernier S1, S2, ...) des étudiants" + "Les cursus (semestres suivis, abandons) des étudiants" self.cursus = {} """Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants""" - self.aggregats = {} + self.trajectoires = {} "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" self.diplomes_ids = {} "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" self.etudiants_ids = {} - "Les formsemestres dont il faut calculer les moyennes par tag" - self.formsemestres_jury_ids = {} - def find_etudiants(self, formation_id: int): """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant de manière automatique par rapport à leur année de diplomation ``annee_diplome`` @@ -116,7 +104,7 @@ class EtudiantsJuryPE: self.analyse_etat_etudiant(etudid, cosemestres) """L'analyse de son parcours pour atteindre chaque semestre de la formation""" - self.analyse_parcours_etudiant_dans_semestres(etudid) + self.structure_cursus_etudiant(etudid) if (no_etud + 1) % 10 == 0: pe_tools.pe_print((no_etud + 1), " ", end="") @@ -130,7 +118,7 @@ class EtudiantsJuryPE: self.etudiants_ids = {etudid for etudid in self.identites} """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" - self.formsemestres_jury_ids = self.get_formsemestres_jury() + self.formsemestres_jury_ids = self.get_formsemestres() # Synthèse pe_tools.pe_print( @@ -210,61 +198,37 @@ class EtudiantsJuryPE: """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) - def analyse_parcours_etudiant_dans_semestres(self, etudid: int): - """Structure les informations sur les semestres suivis par un - étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs - de moyennes PE. + def get_semestres_significatifs(self, etudid: int): + """Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé + l'année visée (supprime les semestres qui conduisent à une diplomation + postérieure à celle du jury visé) - Cette structuration (cf. attribut self.cursus) s'appuie sur les numéros de semestre: pour chaque Si, stocke : - le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les - interclassement par semestre dans la promo. + Args: + etudid: L'identifiant d'un étudiant - Elle calcule également sur les aggrégats (cf. attribut self.aggregats): - - * pour un semestre de type Si, elle stocke le (ou les) formsemestres de numéro i qu'a suivi un étudiant - (2 si redoublant) dans self.aggregats - * pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie les semestres que l'étudiant - a suivi pour l'amener jusqu'au semestre terminal de l'aggrégat. Ce parcours peut être : - ** S1+S2+S1+S2+S3 si redoublement de la 1ère année - ** S1+S2+(année de césure)+S3 si césure, ... - - Par ex: M. N..z (redoublant en 2ème année) au moment de son 2ème S3 : - {'1A': {26: {18: , - 26: }}, - '2A': {79: {18: , - 26: , - 56: , - 79: }}, - '3A': {}, - '3S': {112: {18: , - 26: , - 56: , - 112: }}, - '4S': {79: {18: , - 26: , - 56: , - 79: }}, - '5S': {}, - '6S': {}, - 'S1': {18: {18: }}, - 'S2': {26: {26: }}, - 'S3': {112: {56: , - 112: }}, - 'S4': {79: {79: }}, - 'S5': {}, - 'S6': {} - } + Returns: + Un dictionnaire ``{fid: FormSemestre(fid)`` dans lequel les semestres + amènent à une diplomation avant l'annee de diplomation du jury """ - semestres_etudiant = self.cursus[etudid]["formsemestres"] - """Ne conserve que les semestres qui l'auraient amené à être diplomé l'année visée""" + semestres_etudiant = self.cursus[etudid]["formsemestres"] semestres_significatifs = {} for fid in semestres_etudiant: semestre = semestres_etudiant[fid] if pe_tools.get_annee_diplome_semestre(semestre) <= self.annee_diplome: semestres_significatifs[fid] = semestre + return semestres_significatifs - self.aggregats[etudid] = {} + def structure_cursus_etudiant(self, etudid: int): + """Structure les informations sur les semestres suivis par un + étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs + de moyennes PE. + + Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke : + le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les + interclassement par semestre dans la promo. + """ + semestres_significatifs = self.get_semestres_significatifs(etudid) """Tri des semestres par numéro de semestre""" for nom_sem in pe_tools.TOUS_LES_SEMESTRES: @@ -274,49 +238,37 @@ class EtudiantsJuryPE: for fid in semestres_significatifs if semestres_significatifs[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 - self.aggregats[etudid][nom_sem] = {} - if dernier_semestre_i: - fid_dernier_semestre_i = list(dernier_semestre_i.keys())[0] - self.aggregats[etudid][nom_sem][fid_dernier_semestre_i] = semestres_i + self.cursus[etudid][nom_sem] = semestres_i - """Tri des semestres par aggrégat et par semestre terminal""" - for aggregat in pe_tools.TOUS_LES_AGGREGATS: - self.aggregats[etudid][aggregat] = {} + def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre): + """Ensemble des semestres parcourus par + un étudiant pour l'amener à un semestre terminal. - """L'aggrégat considéré (par ex: 3S), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" - noms_semestre_de_aggregat = pe_tools.PARCOURS[aggregat]["aggregat"] - nom_semestre_terminal = noms_semestre_de_aggregat[-1] - numero_semestre_terminal = int(nom_semestre_terminal[-1]) + Par ex: si formsemestre_terminal est un S3, ensemble des S1, + S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1, + ou S2, ou S3 s'il a redoublé). - """Le formsemestre terminal de l'aggrégat (par ex: son dernier S3 en date)""" - dernier_formsemestre_terminal = self.cursus[etudid][nom_semestre_terminal] + Les semestres parcourus sont antérieurs (en terme de date de fin) + au formsemestre_terminal. - # for formsem_id_term in formsemestres_terminal: - if dernier_formsemestre_terminal: # ne considérant que le dernier - formsem_id_term = list(dernier_formsemestre_terminal.keys())[0] + Args: + etudid: L'identifiant de l'étudiant + formsemestre_final: le semestre final visé + """ + numero_semestre_terminal = formsemestre_final.semestre_id + semestres_significatifs = self.get_semestres_significatifs(etudid) - formsemestre_terminal = self.cursus[etudid]["formsemestres"][ - formsem_id_term - ] - - """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" - semestres_aggreges = {} - for fid in semestres_significatifs: - semestre = semestres_significatifs[fid] - if ( - semestre.semestre_id <= numero_semestre_terminal - and semestre.date_fin <= formsemestre_terminal.date_fin - ): - semestres_aggreges[fid] = semestre - - self.aggregats[etudid][aggregat][formsem_id_term] = semestres_aggreges - - """Vérifications""" - dernier_semestre_aggregat = get_dernier_semestre(semestres_aggreges) - assert dernier_semestre_aggregat == dernier_formsemestre_terminal + """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" + semestres_aggreges = {} + for fid in semestres_significatifs: + semestre = semestres_significatifs[fid] + if ( + semestre.semestre_id <= numero_semestre_terminal + and semestre.date_fin <= formsemestre_final.date_fin + ): + semestres_aggreges[fid] = semestre + return semestres_aggreges def get_formsemestres_terminaux_aggregat(self, aggregat: str): """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat @@ -333,47 +285,25 @@ class EtudiantsJuryPE: aggregat: L'aggrégat Returns: - Un dictionnaire {fid: FormSemestre(fid)} + Un dictionnaire ``{fid: FormSemestre(fid)}`` """ formsemestres_terminaux = {} - for etudid in self.aggregats: - if self.aggregats[etudid][aggregat]: - print(self.aggregats[etudid][aggregat]) - """Le formsemestre_id du semestre terminal de l'étudiant (s'il existe)""" - fid = list(self.aggregats[etudid][aggregat].keys())[0] - """Le formsemestre associé (en le prenant dans l'aggrégat)""" - formsemestres_terminaux[fid] = self.aggregats[etudid][aggregat][fid][ - fid - ] + for etudid in self.trajectoires: + if self.trajectoires[etudid][aggregat]: + trajectoire = self.trajectoires[etudid][aggregat] + """Le semestre terminal de l'étudiant de l'aggrégat""" + fid = trajectoire.semestre_final.formsemestre_id + formsemestres_terminaux[fid] = trajectoire.semestre_final return formsemestres_terminaux - def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): - """Pour un aggrégat donné associé à un formsemestre terminal cible, renvoie l'ensemble des semestres à - prendre en compte dans l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}. - - Fusionne les cursus individuels des étudiants, dont le cursus correspond à l'aggrégat visé. - - Args: - aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S) - formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au - dernier semestre de l'aggrégat - """ - noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] - - formsemestres = {} - for etudid in self.cursus: - cursus_etudiant = self.cursus[etudid][aggregat] - if formsemestre_id_terminal in cursus_etudiant: - formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal] - formsemestres = formsemestres | formsemestres_etudiant - return formsemestres - - def get_formsemestres_jury(self, semestres_recherches=None): + def get_formsemestres(self, semestres_recherches=None): """Ayant connaissance des étudiants dont il faut calculer les moyennes pour - 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``. + le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres + parcourus), + renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` + contenant l'ensemble des formsemestres de leurs 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 : @@ -384,20 +314,20 @@ class EtudiantsJuryPE: * '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats Returns: - Un dictionnaire de la forme {fid: FormSemestre(fid)} + Un dictionnaire de la forme ``{fid: FormSemestre(fid)}`` Remarque: - Une liste de la forme `[ 'Si', 'iA' , ... ]` (combinant les formats précédents) est possible. + Une liste de la forme ``[ 'Si', 'iA' , ... ]`` (combinant les formats précédents) est possible. """ if semestres_recherches is None: """Appel récursif pour obtenir tous les semestres (validants)""" - semestres = self.get_formsemestres_jury(pe_tools.AGGREGAT_DIPLOMANT) + semestres = self.get_formsemestres(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_elmt = self.get_formsemestres(elmt) semestres = semestres | semestres_elmt return semestres elif ( @@ -405,7 +335,7 @@ class EtudiantsJuryPE: 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( + semestres = self.get_formsemestres( pe_tools.PARCOURS[semestres_recherches]["aggregat"] ) return semestres @@ -420,24 +350,21 @@ class EtudiantsJuryPE: nom_sem = semestres_recherches semestres = {} for etudid in self.etudiants_ids: - for sem_terminal in self.aggregats[etudid]: - for sem in self.aggregats[etudid][sem_terminal]: - semestres = semestres | self.aggregats[etudid][sem_terminal][sem] + if self.cursus[etudid][nom_sem]: + semestres = semestres | self.cursus[etudid][nom_sem] return semestres else: - raise ValueError( - "Probleme de paramètres d'appel dans get_formsemestreids_du_jury" - ) + raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") -def get_etudiants_dans_semestres(semestres: dict[FormSemestre]) -> set: +def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) inscrits à l'un des semestres de la liste de ``semestres``. Remarque : Les ``cosemestres`` sont généralement obtenus avec ``sco_formsemestre.do_formsemestre_list()`` Args: - semestres: Un dictionnaire {fid: Formsemestre(fid)} donnant un + semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un ensemble d'identifiant de semestres Returns: @@ -521,7 +448,8 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b etudid = identite.etudid """Son dernier semestre en date""" - dernier_formsemestre = identite.get_formsemestres()[0] + semestres = {sem.semestre_id: sem for sem in identite.get_formsemestres()} + dernier_formsemestre = get_dernier_semestre_en_date(semestres) numero_dernier_formsemestre = dernier_formsemestre.semestre_id """Les numéro de semestres possible dans lesquels il pourrait s'incrire""" @@ -555,27 +483,22 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b return False -def get_dernier_semestre(semestres: dict[int, FormSemestre]): - """Renvoie le dernier semestre en date (de fin) d'un dictionnaire - de semestres de la forme {fid: FormSemestre(fid)}. - La date prise en compte est celle marquant la **fin** des semestres. +def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]): + """Renvoie le dernier semestre en **date de fin** 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 + Le FormSemestre du semestre le plus récent """ if semestres: fid_dernier_semestre = list(semestres.keys())[0] - dernier_semestre = {fid_dernier_semestre: semestres[fid_dernier_semestre]} + dernier_semestre: FormSemestre = 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 + if semestres[fid].date_fin > dernier_semestre.date_fin: + dernier_semestre = semestres[fid] return dernier_semestre else: - return {} + return None diff --git a/app/pe/pe_interclassetag.py b/app/pe/pe_interclassetag.py new file mode 100644 index 00000000..985837a6 --- /dev/null +++ b/app/pe/pe_interclassetag.py @@ -0,0 +1,124 @@ +import pandas as pd + +from app.pe import pe_tagtable +from app.pe.pe_tools import PE_DEBUG, pe_print +import app.pe.pe_etudiant as pe_etudiant +from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE +from app.comp import moy_sem + +import pandas as pd +import numpy as np + + +class AggregatInterclasseTag(pe_tagtable.TableTag): + """Interclasse l'ensemble des étudiants diplômés à une année + donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S') + en reportant : + + * les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son + formsemestres) + * calculant le classement sur les étudiants diplômes + """ + + # ------------------------------------------------------------------------------------------------------------------- + def __init__( + self, + nom_aggregat: str, + etudiants: EtudiantsJuryPE, + trajectoires_jury_pe: TrajectoiresJuryPE, + trajectoires_taggues: dict[tuple, TrajectoireTag], + ): + """""" + """Table nommée au nom de l'aggrégat (par ex: 3S""" + pe_tagtable.TableTag.__init__(self, nom_aggregat) + + """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" + self.diplomes_ids = etudiants.diplomes_ids + self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} + + """Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat + """ + self.trajectoires: dict[int, Trajectoire] = {} + for trajectoire_id in trajectoires_jury_pe.trajectoires: + trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id] + if trajectoire_id[0] == nom_aggregat: + self.trajectoires[trajectoire_id] = trajectoire + + self.trajectoires_taggues: dict[int, Trajectoire] = {} + for trajectoire_id in self.trajectoires: + self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[ + trajectoire_id + ] + + """Les trajectoires suivies par les étudiants du jury, en ne gardant que + celles associées aux diplomés""" + self.suivis: dict[int, Trajectoire] = {} + for etudid in self.diplomes_ids: + self.suivis[etudid] = trajectoires_jury_pe.suivis[etudid][nom_aggregat] + + """Les tags""" + self.tags_sorted = self.do_taglist() + + """Construit la matrice de notes""" + self.notes = self.compute_notes_matrice() + + """Synthétise les moyennes/classements par tag""" + self.moyennes_tags = {} + for tag in self.tags_sorted: + moy_gen_tag = self.notes[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), + } + + def do_taglist(self): + """Synthétise les tags à partir des trajectoires_tagguées + + Returns: + Une liste de tags triés par ordre alphabétique + """ + tags = [] + for trajectoire_id in self.trajectoires_taggues: + trajectoire = self.trajectoires_taggues[trajectoire_id] + tags.extend(trajectoire.tags_sorted) + return sorted(set(tags)) + + def get_etudids(self): + return list(self.etudiants_diplomes.keys()) + + def compute_notes_matrice(self): + """Construit la matrice de notes (etudid x tags) + retraçant les moyennes obtenues par les étudiants dans les semestres associés à + l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat). + """ + nb_tags = len(self.tags_sorted) + nb_etudiants = len(self.diplomes_ids) + + """Index de la matrice (etudids -> dim 0, tags -> dim 1)""" + etudids = [etud.etudid for etud in self.diplomes_ids] + tags = self.tags_sorted + + """Partant d'un dataframe vierge""" + df = pd.DataFrame(np.nan, index=etudids, columns=tags) + + for trajectoire_id in self.trajectoires_taggues: + """Charge les moyennes par tag de la trajectoire tagguée""" + notes = self.trajectoires_taggues[trajectoire_id].notes + + """Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées""" + etudids_communs = df.index.intersection(notes.index) + tags_communs = df.columns.intersection(notes.columns) + + """Injecte les notes par tag""" + df.loc[etudids_communs, tags_communs] = notes.loc[ + etudids_communs, tags_communs + ] + + return df + diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 0d183165..5b36638f 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -68,8 +68,9 @@ 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 import pe_trajectoiretag from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import TrajectoiresJuryPE # ---------------------------------------------------------------------------------------- @@ -154,17 +155,24 @@ class JuryPE(object): filename, formsemestretag.str_tagtable(), path="details_semestres" ) + """Génère les trajectoires (combinaison de semestres suivis + par un étudiant pour atteindre le semestre final d'un aggrégat) + """ + self.trajectoires = TrajectoiresJuryPE(self.diplome) + self.trajectoires.cree_trajectoires(self.etudiants) + + """Génère les aggrégats de semestre (par ex: 1A, 3S, 5S) avec calcul des moyennes pour le jury""" - self.aggregats_taggues = compute_aggregats_tag( + self.trajectoires_tagguees = compute_trajectoires_tag( self.etudiants, self.semestres_taggues ) if pe_tools.PE_DEBUG: """Intègre le bilan des aggrégats de semestres au zip final""" - for aggregat in self.aggregats_taggues: - for fid in self.aggregats_taggues[aggregat]: - set_tag = self.aggregats_taggues[aggregat][fid] + for aggregat in self.trajectoires_tagguees: + for fid in self.trajectoires_tagguees[aggregat]: + set_tag = self.trajectoires_tagguees[aggregat][fid] filename = set_tag.nom.replace(" ", "_") + ".csv" pe_tools.pe_print(f" - Export csv de {filename} ") self.add_file_to_zip( @@ -174,7 +182,7 @@ class JuryPE(object): """Génère les interclassements par (nom d') aggrégat""" self.aggregats_taggues_interclasses = compute_interclassements( self.etudiants, # - self.aggregats_taggues, + self.trajectoires_tagguees, ) # Les interclassements # -------------------- @@ -276,7 +284,7 @@ class JuryPE(object): self.etudiants.cursus[etudid][nom] ] else: - tagtable = self.aggregats_taggues[nom][ + tagtable = self.trajectoires_tagguees[nom][ self.etudiants.cursus[etudid][nom] ] for tag in tagtable.get_all_tags(): @@ -630,7 +638,7 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE): """Création des semestres taggués, de type 'S1', 'S2', ...""" pe_tools.pe_print("*** Création des semestres taggués") - formsemestres = etudiants.get_formsemestres_jury( + formsemestres = etudiants.get_formsemestres( semestres_recherches=pe_tools.TOUS_LES_SEMESTRES ) @@ -655,14 +663,18 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE): return semestres_tags -def compute_aggregats_tag(etudiants: EtudiantsJuryPE, semestres_tag: dict[SemestreTag]): - """Créé les combinaisons de semestres (aggrégats), en calculant les moyennes et les - classements par tag pour chacune. Chaque combinaison (aggrégat) est identifiée - par un formsemestre terminal. +def compute_trajectoires_tag(trajectoires: TrajectoiresJuryPE, + etudiants: EtudiantsJuryPE, + semestres_taggues: dict[int, SemestreTag]): + """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens + d'un aggrégat (par ex: '3S')), + en calculant les moyennes et les classements par tag pour chacune. + + Pour rappel : Chaque trajectoire est identifiée 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és les + * 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. * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les @@ -680,53 +692,24 @@ def compute_aggregats_tag(etudiants: EtudiantsJuryPE, semestres_tag: dict[Semest pe_tools.pe_print(" *** Création des aggrégats ") - sets_tags = {} + trajectoires_tagguees = {} - for aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: - sets_tags[aggregat] = {} + for trajectoire_id in trajectoires_tagguees: + trajectoire = trajectoires[trajectoire_id] + nom = trajectoire.get_repr() - """Semestres aggrégés""" - if aggregat in pe_tools.TOUS_LES_SEMESTRES: # par ex. 'S2' - noms_semestres_aggreges = [aggregat] - else: # par ex. "5S" - noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] - nom_semestre_terminal = noms_semestres_aggreges[-1] + pe_tools.pe_print(f" --> Fusion {nom}") - pe_tools.pe_print(f"* {aggregat}: " + "+".join(noms_semestres_aggreges)) - - """Les formsemestres terminaux des aggrégats""" - formsemestres_terminal = etudiants.get_formsemestres_terminaux_aggregat( - aggregat - ) - - for frmsem_id in formsemestres_terminal: - formsemestre_terminal = formsemestres_terminal[frmsem_id] - """Nom du set_tag""" - nom = "Aggrégat %s %d %s %d-%d" % ( - aggregat, - frmsem_id, - "+".join(noms_semestres_aggreges), - formsemestre_terminal.date_debut.year, - formsemestre_terminal.date_fin.year, + """Création de la trajectoire_tagguee associée""" + trajectoire_tagguee = pe_trajectoiretag.TrajectoireTag( + nom, trajectoire, semestres_taggues, etudiants ) - """Semestres à aggreger dans l'aggrégat ayant amené des étudiants jusqu'au formsemestre_terminal""" - semestres_aggreges = etudiants.get_semestres_a_aggreger(aggregat, frmsem_id) + """Mémorise le résultat""" + trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee - pe_tools.pe_print(" --> Fusion de :") - for fid in semestres_aggreges: - pe_tools.pe_print(str(semestres_aggreges[fid])) + return trajectoires_tagguees - """Création du settag associé""" - settag = pe_settag.SetTag( - nom, formsemestre_terminal, semestres_aggreges, semestres_tag, etudiants - ) - - settag.compute_notes_cube() # Calcul les moyennes, les rangs, .. - - sets_tags[aggregat][fid] = settag # Mémorise le résultat - - return sets_tags def compute_interclassements( diff --git a/app/pe/pe_settag_interclasse.py b/app/pe/pe_settag_interclasse.py index 933383c3..83abe82d 100644 --- a/app/pe/pe_settag_interclasse.py +++ b/app/pe/pe_settag_interclasse.py @@ -1,7 +1,7 @@ from app.pe import pe_tagtable from app.pe.pe_tools import PE_DEBUG, pe_print import app.pe.pe_etudiant as pe_etudiant - +from app.pe.pe_etudiant import EtudiantsJuryPE class SetTagInterClasse(pe_tagtable.TableTag): """Interclasse les étudiants d'une promo (ceux diplômé) par aggrégat de même nom diff --git a/app/pe/pe_trajectoire.py b/app/pe/pe_trajectoire.py new file mode 100644 index 00000000..fbbf1ca8 --- /dev/null +++ b/app/pe/pe_trajectoire.py @@ -0,0 +1,149 @@ +import app.pe.pe_tools as pe_tools +from app.models import FormSemestre +from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date + + +class Trajectoire: + """Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A') + et un ensemble d'étudiants donnés, + la combinaison des formsemestres des étudiants amenant à un semestre + terminal visé. + + Si l'aggrégat est un semestre de type Si, elle stocke le (ou les) + formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si + (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) + + Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal de la trajectoire (par ex: ici un S3). + Ces semestres peuvent être : + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... + """ + + def __init__(self, nom_aggregat: str, semestre_final: FormSemestre): + """Modélise un ensemble de formsemestres d'étudiants + amenant à un semestre terminal + + Args: + nom_aggregat: Un nom d'aggrégat (par ex: '5S') + semestre_final: Le semestre final de l'aggrégat + """ + self.nom = nom_aggregat + self.semestre_final = semestre_final + self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id) + + """Les semestres à aggréger""" + self.semestres_aggreges = {} + + + def add_semestres_a_aggreger(self, semestres: dict[int: FormSemestre]): + """Ajoute des semestres au semestre à aggréger + + Args: + semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter`` + """ + self.semestres_aggreges = self.semestres_aggreges | semestres + + + + def get_repr(self): + """Représentation textuelle d'une trajectoire + basée sur ses semestres aggrégés""" + noms = [] + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}({fid})") + noms = sorted(noms) + repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}" + if noms: + repr += " - " + "+".join(noms) + return repr + + +class TrajectoiresJuryPE: + """Centralise toutes les trajectoires du jury PE""" + + def __init__(self, annee_diplome: int): + """ + Args: + annee_diplome: L'année de diplomation + """ + + self.annee_diplome = annee_diplome + """Toutes les trajectoires possibles""" + self.trajectoires: dict[tuple: Trajectoire] = {} + """Quelle trajectoires pour quel étudiant : + dictionnaire {etudid: {nom_aggregat: Trajectoire}}""" + self.suivi: dict[int: str] = {} + + + def cree_trajectoires(self, etudiants: EtudiantsJuryPE): + """Créé toutes les trajectoires, au regard du cursus des étudiants + analysés + les mémorise dans les données de l'étudiant + """ + + for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: + + """L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" + noms_semestre_de_aggregat = pe_tools.PARCOURS[nom_aggregat]["aggregat"] + nom_semestre_terminal = noms_semestre_de_aggregat[-1] + + for etudid in etudiants.cursus: + if etudid not in self.suivi: + self.suivi[etudid] = {} + + """Le formsemestre terminal (dernier en date) associé au + semestre marquant la fin de l'aggrégat + (par ex: son dernier S3 en date)""" + semestres = etudiants.cursus[etudid][nom_semestre_terminal] + if semestres: + formsemestre_final = get_dernier_semestre_en_date(semestres) + + """Ajout ou récupération de la trajectoire""" + trajectoire_id = (nom_aggregat, formsemestre_final.formsemestre_id) + if trajectoire_id not in self.trajectoires: + trajectoire = Trajectoire(nom_aggregat, formsemestre_final) + self.trajectoires[trajectoire_id] = trajectoire + else: + trajectoire = self.trajectoires[trajectoire_id] + + """La liste des semestres de l'étudiant à prendre en compte + pour cette trajectoire""" + semestres_a_aggreger = etudiants.get_trajectoire(etudid, formsemestre_final) + + """Ajout des semestres à la trajectoire""" + trajectoire.add_semestres_a_aggreger(semestres_a_aggreger) + + """Mémoire la trajectoire suivie par l'étudiant""" + self.suivi[etudid][nom_aggregat] = trajectoire + + """Vérifications""" + # dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges) + # assert dernier_semestre_aggregat == formsemestre_terminal + + +def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): + """Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible + identifié par son formsemestre_id (par ex: 'S3 2022-2023'), + renvoie l'ensemble des semestres à prendre en compte dans + l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}. + + Fusionne les cursus individuels des étudiants, dont le cursus correspond + à l'aggrégat visé. + + Args: + aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S) + formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au + dernier semestre de l'aggrégat + """ + noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] + + formsemestres = {} + for etudid in self.cursus: + cursus_etudiant = self.cursus[etudid][aggregat] + if formsemestre_id_terminal in cursus_etudiant: + formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal] + formsemestres = formsemestres | formsemestres_etudiant + return formsemestres + diff --git a/app/pe/pe_settag.py b/app/pe/pe_trajectoiretag.py similarity index 86% rename from app/pe/pe_settag.py rename to app/pe/pe_trajectoiretag.py index 2474d0b9..547599e4 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_trajectoiretag.py @@ -35,6 +35,7 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ + from app.comp import moy_sem from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre @@ -42,35 +43,43 @@ from app.pe.pe_semestretag import SemestreTag from app.pe import pe_tagtable import pandas as pd import numpy as np - +from app.pe.pe_trajectoire import Trajectoire from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import TrajectoiresJuryPE -class SetTag(pe_tagtable.TableTag): - """Agrège plusieurs semestres (ou settag) taggués (SemestreTag/Settag de 1 à 4) pour extraire des moyennes - et des classements par tag pour un groupe d'étudiants donnés. - par. exemple fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S' - Le settag est identifié sur la base du dernier semestre (ici le 'S3') ; - les étudiants considérés sont donc ceux inscrits dans ce S3 - à condition qu'ils disposent d'un parcours sur tous les semestres fusionnés valides (par. ex - un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte). +class TrajectoireTag(pe_tagtable.TableTag): + """Calcule les moyennes par tag d'une combinaison de semestres + (trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et + par un semestre terminal, pour extraire les classements par tag pour un + groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous + participé au semestre terminal. + + Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S' + """ + # ------------------------------------------------------------------------------------------------------------------- def __init__( self, - nom, - formsemestre_terminal: FormSemestre, - semestres_aggreges: dict[int, FormSemestre], + nom: str, + trajectoire: Trajectoire, semestres_taggues: dict[int, SemestreTag], donnees_etudiants: EtudiantsJuryPE, ): - pe_tagtable.TableTag.__init__(self, nom) + """ """ + pe_tagtable.TableTag.__init__(self, nom=nom) + + """La trajectoire associée""" + self.trajectoire_id = trajectoire.trajectoire_id + self.trajectoire = trajectoire """Le formsemestre terminal et les semestres aggrégés""" - self.formsemestre_terminal = formsemestre_terminal - nt = load_formsemestre_results(formsemestre_terminal) - self.semestres_aggreges = semestres_aggreges + self.formsemestre_terminal = trajectoire.semestre_final + nt = load_formsemestre_results(self.formsemestre_terminal) + + self.semestres_aggreges = trajectoire.semestres_aggreges """Les semestres tags associés aux semestres aggrégés""" try: @@ -83,6 +92,7 @@ class SetTag(pe_tagtable.TableTag): """Les étudiants (état civil + cursus connu)""" self.etuds = nt.etuds + # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} self.cursus = { etudid: donnees_etudiants.cursus[etudid] for etudid in self.etudiants @@ -155,7 +165,6 @@ class SetTag(pe_tagtable.TableTag): def get_etudids(self): return list(self.etudiants.keys()) - # ------------------------------------------------------------------------------------------------------------------- def do_taglist(self): """Synthétise les tags à partir des semestres (taggués) aggrégés From 82713752c21da1bb28257bfdd993ec727a01968f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 24 Jan 2024 19:37:45 +0100 Subject: [PATCH 19/23] Mise en place des interclassements --- app/pe/pe_etudiant.py | 11 +- ..._interclassetag.py => pe_interclasstag.py} | 16 ++- app/pe/pe_jurype.py | 129 +++++++++--------- app/pe/pe_trajectoire.py | 3 +- app/pe/pe_trajectoiretag.py | 5 + 5 files changed, 85 insertions(+), 79 deletions(-) rename app/pe/{pe_interclassetag.py => pe_interclasstag.py} (92%) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 1c82d238..fdbbc72b 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -60,7 +60,9 @@ class EtudiantsJuryPE: self.trajectoires = {} "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" + self.etudiants_diplomes = {} self.diplomes_ids = {} + "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" self.etudiants_ids = {} @@ -112,7 +114,8 @@ class EtudiantsJuryPE: pe_tools.pe_print() """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" - self.diplomes_ids = self.get_etudiants_diplomes() + self.etudiants_diplomes = self.get_etudiants_diplomes() + self.diplomes_ids = set(self.etudiants_diplomes.keys()) """Les étudiants dont il faut calculer les moyennes""" self.etudiants_ids = {etudid for etudid in self.identites} @@ -122,16 +125,16 @@ class EtudiantsJuryPE: # Synthèse pe_tools.pe_print( - f" => {len(self.diplomes_ids)} étudiants à diplômer en {self.annee_diplome}" + f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" ) - nbre_abandons = len(self.etudiants_ids) - len(self.diplomes_ids) + nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") pe_tools.pe_print( f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" ) pe_tools.pe_print( f" => quelques étudiants futurs diplômés : " - + ", ".join([str(etudid) for etudid in list(self.diplomes_ids)[:10]]) + + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]]) ) pe_tools.pe_print( f" => semestres dont il faut calculer les moyennes : " diff --git a/app/pe/pe_interclassetag.py b/app/pe/pe_interclasstag.py similarity index 92% rename from app/pe/pe_interclassetag.py rename to app/pe/pe_interclasstag.py index 985837a6..f20dbf24 100644 --- a/app/pe/pe_interclassetag.py +++ b/app/pe/pe_interclasstag.py @@ -5,6 +5,7 @@ from app.pe.pe_tools import PE_DEBUG, pe_print import app.pe.pe_etudiant as pe_etudiant from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE +from app.pe.pe_trajectoiretag import TrajectoireTag from app.comp import moy_sem import pandas as pd @@ -34,7 +35,7 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): pe_tagtable.TableTag.__init__(self, nom_aggregat) """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" - self.diplomes_ids = etudiants.diplomes_ids + self.diplomes_ids = etudiants.etudiants_diplomes self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} """Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat @@ -53,9 +54,9 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): """Les trajectoires suivies par les étudiants du jury, en ne gardant que celles associées aux diplomés""" - self.suivis: dict[int, Trajectoire] = {} + self.suivi: dict[int, Trajectoire] = {} for etudid in self.diplomes_ids: - self.suivis[etudid] = trajectoires_jury_pe.suivis[etudid][nom_aggregat] + self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat] """Les tags""" self.tags_sorted = self.do_taglist() @@ -77,6 +78,11 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): "nb_inscrits": len(moy_gen_tag), } + def get_repr(self) -> str: + """Une représentation textuelle""" + return f"Aggrégat {self.nom}" + + def do_taglist(self): """Synthétise les tags à partir des trajectoires_tagguées @@ -89,8 +95,6 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): tags.extend(trajectoire.tags_sorted) return sorted(set(tags)) - def get_etudids(self): - return list(self.etudiants_diplomes.keys()) def compute_notes_matrice(self): """Construit la matrice de notes (etudid x tags) @@ -101,7 +105,7 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): nb_etudiants = len(self.diplomes_ids) """Index de la matrice (etudids -> dim 0, tags -> dim 1)""" - etudids = [etud.etudid for etud in self.diplomes_ids] + etudids = list(self.diplomes_ids) tags = self.tags_sorted """Partant d'un dataframe vierge""" diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 5b36638f..932d03e4 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -47,30 +47,23 @@ import io import os from zipfile import ZipFile -import app.pe.pe_etudiant -import app.pe.pe_settag_interclasse + 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.pe.pe_semestretag import SemestreTag +from app.pe.pe_interclasstag import AggregatInterclasseTag +from app.pe.pe_trajectoiretag import TrajectoireTag from app.scodoc.gen_tables import GenTable, SeqGenTable import app.scodoc.sco_utils as scu -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.scodoc import codes_cursus from app.pe import pe_tools from app.pe import pe_semestretag -from app.pe import pe_trajectoiretag from app.pe.pe_etudiant import EtudiantsJuryPE -from app.pe.pe_trajectoire import TrajectoiresJuryPE +from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire # ---------------------------------------------------------------------------------------- @@ -143,6 +136,7 @@ class JuryPE(object): self.etudiants.find_etudiants(self.formation_id) """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" + pe_tools.pe_print("*** Génère les semestres taggués") self.semestres_taggues = compute_semestres_tag(self.etudiants) if pe_tools.PE_DEBUG: @@ -158,42 +152,50 @@ class JuryPE(object): """Génère les trajectoires (combinaison de semestres suivis par un étudiant pour atteindre le semestre final d'un aggrégat) """ + pe_tools.pe_print( + "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" + ) self.trajectoires = TrajectoiresJuryPE(self.diplome) self.trajectoires.cree_trajectoires(self.etudiants) - - """Génère les aggrégats de semestre (par ex: 1A, 3S, 5S) avec calcul - des moyennes pour le jury""" + """Génère les moyennes par tags des trajectoires""" + pe_tools.pe_print("*** Calcule les moyennes par tag des trajectoires possibles") self.trajectoires_tagguees = compute_trajectoires_tag( - self.etudiants, self.semestres_taggues + self.trajectoires, self.etudiants, self.semestres_taggues ) if pe_tools.PE_DEBUG: - """Intègre le bilan des aggrégats de semestres au zip final""" - for aggregat in self.trajectoires_tagguees: - for fid in self.trajectoires_tagguees[aggregat]: - set_tag = self.trajectoires_tagguees[aggregat][fid] - filename = set_tag.nom.replace(" ", "_") + ".csv" - pe_tools.pe_print(f" - Export csv de {filename} ") - self.add_file_to_zip( - filename, set_tag.str_tagtable(), path="details_semestres" - ) + """Intègre le bilan des trajectoires tagguées au zip final""" + for trajectoire_id in self.trajectoires_tagguees: + trajectoire_tagguee = self.trajectoires_tagguees[trajectoire_id] + filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv" + pe_tools.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + trajectoire_tagguee.str_tagtable(), + path="details_semestres", + ) - """Génère les interclassements par (nom d') aggrégat""" - self.aggregats_taggues_interclasses = compute_interclassements( - self.etudiants, # - self.trajectoires_tagguees, + """Génère les interclassements (par promo et) par (nom d') aggrégat""" + pe_tools.pe_print("*** Génère les interclassements par aggrégat") + self.interclassements_taggues = compute_interclassements( + self.etudiants, self.trajectoires, self.trajectoires_tagguees ) - # Les interclassements - # -------------------- + if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" - ) + """Intègre le bilan des aggrégats (par promo) au zip final""" + for nom_aggregat in self.interclassements_taggues: + interclass_tag = self.interclassements_taggues[nom_aggregat] + filename = interclass_tag.get_repr().replace(" ", "_") + ".csv" + pe_tools.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + interclass_tag.str_tagtable(), + path="details_semestres", + ) """Synthèse des éléments du jury PE""" - if False: - self.synthetise_juryPE() + self.synthetise_juryPE() # Export des données => mode 1 seule feuille -> supprimé # filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls' @@ -243,7 +245,7 @@ class JuryPE(object): # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** def synthetise_juryPE(self): - """Synthétise tous les résultats du jury PE dans un dictionnaire""" + """Synthétise tous les résultats du jury PE dans des dataframess""" self.syntheseJury = {} for etudid in self.etudiants.get_etudids(self.diplome): etudinfo = self.ETUDINFO_DICT[etudid] @@ -663,9 +665,11 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE): return semestres_tags -def compute_trajectoires_tag(trajectoires: TrajectoiresJuryPE, - etudiants: EtudiantsJuryPE, - semestres_taggues: dict[int, SemestreTag]): +def compute_trajectoires_tag( + trajectoires: TrajectoiresJuryPE, + etudiants: EtudiantsJuryPE, + semestres_taggues: dict[int, SemestreTag], +): """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens d'un aggrégat (par ex: '3S')), en calculant les moyennes et les classements par tag pour chacune. @@ -694,16 +698,16 @@ def compute_trajectoires_tag(trajectoires: TrajectoiresJuryPE, trajectoires_tagguees = {} - for trajectoire_id in trajectoires_tagguees: - trajectoire = trajectoires[trajectoire_id] + for trajectoire_id in trajectoires.trajectoires: + trajectoire = trajectoires.trajectoires[trajectoire_id] nom = trajectoire.get_repr() pe_tools.pe_print(f" --> Fusion {nom}") """Création de la trajectoire_tagguee associée""" - trajectoire_tagguee = pe_trajectoiretag.TrajectoireTag( - nom, trajectoire, semestres_taggues, etudiants - ) + trajectoire_tagguee = TrajectoireTag( + nom, trajectoire, semestres_taggues, etudiants + ) """Mémorise le résultat""" trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee @@ -711,33 +715,22 @@ def compute_trajectoires_tag(trajectoires: TrajectoiresJuryPE, return trajectoires_tagguees - def compute_interclassements( - etudiants: EtudiantsJuryPE, aggregats_taggues: dict[str, dict] + etudiants: EtudiantsJuryPE, + trajectoires_jury_pe: TrajectoiresJuryPE, + trajectoires_tagguees: dict[tuple, Trajectoire], ): """Interclasse les étudiants, (nom d') aggrégat par aggrégat, - pour fournir un classement sur la promo. Le classement est établit au regard du nombre + pour fournir un classement sur la promo. Le classement est établi au regard du nombre d'étudiants ayant participé au même aggrégat. """ - etudiants_diplomes = etudiants.get_etudiants_diplomes() + pe_tools.pe_print(" Interclassement sur la promo") - """ - for i, nom in enumerate(pe_tools.PARCOURS.keys()): - settag = app.pe.pe_settag_interclasse.SetTagInterClasse(nom, diplome=diplome) - nbreEtudInscrits = settag.set_Etudiants( - lesEtudids, self.etudiants.cursus, self.etudiants.identites + aggregats_interclasses_taggues = {} + for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: + pe_tools.pe_print(f" --> {nom_aggregat}") + interclass = AggregatInterclasseTag( + nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees ) - if nbreEtudInscrits > 0: - pe_tools.pe_print("%d) %s avec interclassement sur la promo" % (i + 1, nom)) - if nom in pe_tools.TOUS_LES_SEMESTRES: - settag.set_SetTagDict(self.semestres_taggues) - else: # cas des aggrégats - settag.set_SetTagDict(self.aggregats_taggues[nom]) - settag.comp_data_settag() - self.promoTagDict[nom] = settag - else: - pe_tools.pe_print( - "%d) Pas d'interclassement %s sur la promo faute de notes" - % (i + 1, nom) - ) - """ \ No newline at end of file + aggregats_interclasses_taggues[nom_aggregat] = interclass + return aggregats_interclasses_taggues diff --git a/app/pe/pe_trajectoire.py b/app/pe/pe_trajectoire.py index fbbf1ca8..415fee24 100644 --- a/app/pe/pe_trajectoire.py +++ b/app/pe/pe_trajectoire.py @@ -91,7 +91,8 @@ class TrajectoiresJuryPE: for etudid in etudiants.cursus: if etudid not in self.suivi: - self.suivi[etudid] = {} + self.suivi[etudid] = {aggregat: None + for aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS} """Le formsemestre terminal (dernier en date) associé au semestre marquant la fin de l'aggrégat diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py index 547599e4..abefa008 100644 --- a/app/pe/pe_trajectoiretag.py +++ b/app/pe/pe_trajectoiretag.py @@ -122,6 +122,11 @@ class TrajectoireTag(pe_tagtable.TableTag): "nb_inscrits": len(moy_gen_tag), } + def get_repr(self): + """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle + est basée)""" + return self.trajectoire.get_repr() + def compute_notes_cube(self): """Construit le cube de notes (etudid x tags x semestre_aggregé) nécessaire au calcul des moyennes de l'aggrégat From decc28b89667dbfb843d0af37e05b9347d946903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Thu, 25 Jan 2024 17:17:01 +0100 Subject: [PATCH 20/23] Presque la version 2? --- app/pe/pe_affichage.py | 68 ++ app/pe/pe_comp.py | 384 +++++++ app/pe/pe_etudiant.py | 70 +- app/pe/pe_interclasstag.py | 9 +- app/pe/pe_jury.py | 477 +++++++++ app/pe/pe_jurype.py | 736 ------------- app/pe/{pe_semestretag.py => pe_semtag.py} | 26 +- app/pe/pe_settag_interclasse.py | 128 --- app/pe/{pe_tagtable.py => pe_tabletags.py} | 0 app/pe/pe_tools.py | 1126 -------------------- app/pe/pe_trajectoire.py | 2 +- app/pe/pe_trajectoiretag.py | 11 +- 12 files changed, 992 insertions(+), 2045 deletions(-) create mode 100644 app/pe/pe_affichage.py create mode 100644 app/pe/pe_comp.py create mode 100644 app/pe/pe_jury.py delete mode 100644 app/pe/pe_jurype.py rename app/pe/{pe_semestretag.py => pe_semtag.py} (97%) delete mode 100644 app/pe/pe_settag_interclasse.py rename app/pe/{pe_tagtable.py => pe_tabletags.py} (100%) delete mode 100644 app/pe/pe_tools.py diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py new file mode 100644 index 00000000..484ae5da --- /dev/null +++ b/app/pe/pe_affichage.py @@ -0,0 +1,68 @@ +from app.models import Formation, FormSemestre +from app.scodoc import codes_cursus + + +def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: + """Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité + d'un étudiant. + + Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec : + + * 2 le numéro du semestre, + * FI la modalité, + * 2014-2015 les dates + + Args: + semestre: Un ``FormSemestre`` + avec_fid: Ajoute le n° du semestre à la description + + Returns: + La chaine de caractères décrivant succintement le semestre + """ + formation: Formation = semestre.formation + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) + + description = [ + parcours.SESSION_NAME.capitalize(), + str(semestre.semestre_id), + semestre.modalite, # eg FI ou FC + f"{semestre.date_debut.year}-{semestre.date_fin.year}", + ] + if avec_fid: + description.append(f"({semestre.forsemestre_id})") + + return " ".join(description) + + +def etapes_du_cursus(semestres: dict[int, FormSemestre], nbre_etapes_max: int) -> list[str]: + """Partant d'un dictionnaire de semestres (qui retrace + la scolarité d'un étudiant), liste les noms des + semestres (en version abbrégée) + qu'un étudiant a suivi au cours de sa scolarité à l'IUT. + Les noms des semestres sont renvoyés dans un dictionnaire + ``{"etape i": nom_semestre_a_etape_i}`` + avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i, + le nom affiché est vide. + + La fonction suppose la liste des semestres triées par ordre + décroissant de date. + + Args: + semestres: une liste de ``FormSemestre`` + nbre_etapes_max: le nombre d'étapes max prise en compte + + Returns: + Une liste de nom de semestre (dans le même ordre que les ``semestres``) + + See also: + app.pe.pe_affichage.nom_semestre_etape + """ + assert len(semestres) <= nbre_etapes_max + + noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()] + noms = noms[::-1] # trie par ordre croissant + + dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)} + for (i, nom) in enumerate(noms): # Charge les noms de semestres + dico[f"Etape {i+1}"] = nom + return dico diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py new file mode 100644 index 00000000..e57dfa4d --- /dev/null +++ b/app/pe/pe_comp.py @@ -0,0 +1,384 @@ +# -*- 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 Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import os +import datetime +import re +import unicodedata + + +from flask import g + +import app.scodoc.sco_utils as scu +from app import log +from app.models import FormSemestre +from app.scodoc import sco_formsemestre +from app.scodoc.sco_logos import find_logo + +PE_DEBUG = 1 + +if not PE_DEBUG: + # log to notes.log + def pe_print(*a, **kw): + # kw is ignored. log always add a newline + log(" ".join(a)) + +else: + pe_print = print # print function + + +# Generated LaTeX files are encoded as: +PE_LATEX_ENCODING = "utf-8" + +# /opt/scodoc/tools/doc_poursuites_etudes +REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/") +REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/") + +PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex" +PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex" +PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex" +PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" + +# ---------------------------------------------------------------------------------------- + +""" +Descriptif d'un parcours classique BUT + +TODO:: A améliorer si BUT en moins de 6 semestres +""" + +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)", + }, +} +NBRE_SEMESTRES_DIPLOMANT = 6 +AGGREGAT_DIPLOMANT = ( + "6S" # aggrégat correspondant à la totalité des notes pour le diplôme +) +TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"] +TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] +TOUS_LES_PARCOURS = list(PARCOURS.keys()) + +# ---------------------------------------------------------------------------------------- +def calcul_age(born: datetime.date) -> int: + """Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé + à partir de l'horloge système). + + Args: + born: La date de naissance + + Return: + L'age (au regard de la date actuelle) + """ + if not born or not isinstance(born, datetime.date): + return None + + today = datetime.date.today() + return ( + today.year + - born.year + - ((today.month, today.day) < (born.month, born.day)) + ) + + +def remove_accents(input_unicode_str): + """Supprime les accents d'une chaine unicode""" + nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) + only_ascii = nfkd_form.encode("ASCII", "ignore") + return only_ascii + + +def escape_for_latex(s): + """Protège les caractères pour inclusion dans du source LaTeX""" + if not s: + return "" + conv = { + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\^{}", + "\\": r"\textbackslash{}", + "<": r"\textless ", + ">": r"\textgreater ", + } + exp = re.compile( + "|".join( + re.escape(key) + for key in sorted(list(conv.keys()), key=lambda item: -len(item)) + ) + ) + return exp.sub(lambda match: conv[match.group()], s) + + +# ---------------------------------------------------------------------------------------- +def list_directory_filenames(path): + """List of regular filenames in a directory (recursive) + Excludes files and directories begining with . + """ + R = [] + for root, dirs, files in os.walk(path, topdown=True): + dirs[:] = [d for d in dirs if d[0] != "."] + R += [os.path.join(root, fn) for fn in files if fn[0] != "."] + return R + + +def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): + """Read pathname server file and add content to zip under path_in_zip""" + rooted_path_in_zip = os.path.join(ziproot, path_in_zip) + zipfile.write(filename=pathname, arcname=rooted_path_in_zip) + # data = open(pathname).read() + # zipfile.writestr(rooted_path_in_zip, data) + + +def add_refs_to_register(register, directory): + """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme + filename => pathname + """ + length = len(directory) + for pathname in list_directory_filenames(directory): + filename = pathname[length + 1 :] + register[filename] = pathname + + +def add_pe_stuff_to_zip(zipfile, ziproot): + """Add auxiliary files to (already opened) zip + Put all local files found under config/doc_poursuites_etudes/local + and config/doc_poursuites_etudes/distrib + If a file is present in both subtrees, take the one in local. + + Also copy logos + """ + register = {} + # first add standard (distrib references) + distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") + add_refs_to_register(register=register, directory=distrib_dir) + # then add local references (some oh them may overwrite distrib refs) + local_dir = os.path.join(REP_LOCAL_AVIS, "local") + add_refs_to_register(register=register, directory=local_dir) + # at this point register contains all refs (filename, pathname) to be saved + for filename, pathname in register.items(): + add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) + + # Logos: (add to logos/ directory in zip) + logos_names = ["header", "footer"] + for name in logos_names: + logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) + if logo is not None: + add_local_file_to_zip( + zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename + ) + + +# ---------------------------------------------------------------------------------------- +def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int: + """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres) + et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle + sera remis le diplôme BUT des étudiants qui y sont scolarisés + (en supposant qu'il n'y ait pas de redoublement à venir). + + **Remarque sur le calcul** : 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'année 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 + + La fonction est adaptée au cas des semestres décalés. + + Par exemple : + + * S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026 + * S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027 + + Args: + sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit : + + * un ``FormSemestre`` (Scodoc9) + * un dict (format compatible avec Scodoc7) + + nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT) + """ + + if isinstance(sem_base, FormSemestre): + sem_id = sem_base.semestre_id + annee_fin = sem_base.date_fin.year + annee_debut = sem_base.date_debut.year + else: # sem_base est un dictionnaire (Scodoc 7) + sem_id = sem_base["semestre_id"] + annee_fin = int(sem_base["annee_fin"]) + annee_debut = int(sem_base["annee_debut"]) + if ( + 1 <= sem_id <= nbre_sem_formation + ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? + nbreSemRestant = ( + nbre_sem_formation - sem_id + ) # nombre de semestres restant avant diplome + nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome + # Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé + # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon + delta = annee_fin - annee_debut + decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 + increment = decalage * (1 - delta) + return annee_fin + nbreAnRestant + increment + + +def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list: + """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome`` + et s'intégrant à la formation donnée par son ``formation_id``. + + **Définition** : Un co-semestre est un semestre : + + * dont l'année de diplômation prédite (sans redoublement) est la même + * dont la formation est la même (optionnel) + * qui a des étudiants inscrits + + Si formation_id == None, ne prend pas en compte l'identifiant de formation + TODO:: A raccrocher à un programme + + Args: + annee_diplome: L'année de diplomation + formation_id: L'identifiant de la formation + """ + tousLesSems = ( + sco_formsemestre.do_formsemestre_list() + ) # tous les semestres memorisés dans scodoc + + if formation_id: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + and sem["formation_id"] == formation_id + } + else: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + } + + cosemestres = {} + for fid in cosemestres_fids: + cosem = FormSemestre.get_formsemestre(fid) + if len(cosem.etuds_inscriptions) > 0: + cosemestres[fid] = cosem + + return cosemestres + diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index fdbbc72b..26edc720 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -36,9 +36,10 @@ Created on 17/01/2024 @author: barasc """ -import app.pe.pe_tools as pe_tools +import app.pe.pe_comp as pe_comp from app.models import FormSemestre, Identite -from app.pe.pe_tools import pe_print +from app.pe.pe_comp import pe_print + class EtudiantsJuryPE: """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" @@ -79,22 +80,22 @@ class EtudiantsJuryPE: *Remarque* : ex: JuryPE.get_etudiants_in_jury() """ "Les cosemestres donnant lieu à même année de diplome" - cosemestres = pe_tools.get_cosemestres_diplomants(self.annee_diplome, None) + cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None) self.cosemestres = cosemestres - pe_tools.pe_print( + pe_comp.pe_print( "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) ) """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" - pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") + pe_comp.pe_print("2) Liste des étudiants dans les différents co-semestres") self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) - pe_tools.pe_print( + pe_comp.pe_print( " => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) ) """Analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, ....""" - pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") + pe_comp.pe_print("3) Analyse des parcours individuels des étudiants") no_etud = 0 for no_etud, etudid in enumerate(self.etudiants_ids): @@ -109,9 +110,9 @@ class EtudiantsJuryPE: self.structure_cursus_etudiant(etudid) if (no_etud + 1) % 10 == 0: - pe_tools.pe_print((no_etud + 1), " ", end="") + pe_comp.pe_print((no_etud + 1), " ", end="") no_etud += 1 - pe_tools.pe_print() + pe_comp.pe_print() """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" self.etudiants_diplomes = self.get_etudiants_diplomes() @@ -124,19 +125,19 @@ class EtudiantsJuryPE: self.formsemestres_jury_ids = self.get_formsemestres() # Synthèse - pe_tools.pe_print( + pe_comp.pe_print( f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" ) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) - pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") - pe_tools.pe_print( + pe_comp.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") + pe_comp.pe_print( f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" ) - pe_tools.pe_print( + pe_comp.pe_print( f" => quelques étudiants futurs diplômés : " + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]]) ) - pe_tools.pe_print( + pe_comp.pe_print( f" => semestres dont il faut calculer les moyennes : " + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) ) @@ -183,9 +184,11 @@ class EtudiantsJuryPE: identite = Identite.get_etud(etudid) """Le cursus global de l'étudiant (restreint aux semestres APC)""" + formsemestres = identite.get_formsemestres() + semestres_etudiant = { frmsem.formsemestre_id: frmsem - for frmsem in identite.get_formsemestres() + for frmsem in formsemestres if frmsem.formation.is_apc() } @@ -193,8 +196,10 @@ class EtudiantsJuryPE: "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury "nom": identite.nom, + "entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT "diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme "formsemestres": semestres_etudiant, # les semestres de l'étudiant + "nb_semestres": len(semestres_etudiant), # le nombre de semestres de l'étudiant "abandon": False, # va être traité en dessous } @@ -218,7 +223,7 @@ class EtudiantsJuryPE: semestres_significatifs = {} for fid in semestres_etudiant: semestre = semestres_etudiant[fid] - if pe_tools.get_annee_diplome_semestre(semestre) <= self.annee_diplome: + if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome: semestres_significatifs[fid] = semestre return semestres_significatifs @@ -234,7 +239,7 @@ class EtudiantsJuryPE: semestres_significatifs = self.get_semestres_significatifs(etudid) """Tri des semestres par numéro de semestre""" - for nom_sem in pe_tools.TOUS_LES_SEMESTRES: + for nom_sem in pe_comp.TOUS_LES_SEMESTRES: i = int(nom_sem[1]) # le n° du semestre semestres_i = { fid: semestres_significatifs[fid] @@ -324,7 +329,7 @@ class EtudiantsJuryPE: """ if semestres_recherches is None: """Appel récursif pour obtenir tous les semestres (validants)""" - semestres = self.get_formsemestres(pe_tools.AGGREGAT_DIPLOMANT) + semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT) return semestres elif isinstance(semestres_recherches, list): """Appel récursif sur tous les éléments de la liste""" @@ -335,16 +340,16 @@ class EtudiantsJuryPE: return semestres elif ( isinstance(semestres_recherches, str) - and semestres_recherches in pe_tools.TOUS_LES_AGGREGATS + and semestres_recherches in pe_comp.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( - pe_tools.PARCOURS[semestres_recherches]["aggregat"] + pe_comp.PARCOURS[semestres_recherches]["aggregat"] ) return semestres elif ( isinstance(semestres_recherches, str) - and semestres_recherches in pe_tools.TOUS_LES_SEMESTRES + and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES ): """semestres_recherches est un nom de semestre de type S1, pour une recherche parmi les étudiants à prendre en compte @@ -359,6 +364,15 @@ class EtudiantsJuryPE: else: raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") + def nbre_etapes_max_diplomes(self): + """Connaissant les étudiants diplomes du jury PE, + nombre de semestres (étapes) maximum suivis par les étudiants du jury. + """ + nbres_semestres = [] + for etudid in self.diplomes_ids: + nbres_semestres.append( self.cursus[etudid]["nb_semestres"] ) + return max(nbres_semestres) + def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) @@ -402,7 +416,7 @@ def annee_diplome(identite: Identite) -> int: if formsemestres: return max( [ - pe_tools.get_annee_diplome_semestre(sem_base) + pe_comp.get_annee_diplome_semestre(sem_base) for sem_base in formsemestres ] ) @@ -459,14 +473,14 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) if numero_dernier_formsemestre % 2 == 1: numeros_possibles = list( - range(numero_dernier_formsemestre + 1, pe_tools.NBRE_SEMESTRES_DIPLOMANT) + range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) ) # semestre pair => passage en année supérieure ou redoublement else: # numeros_possibles = list( range( max(numero_dernier_formsemestre - 1, 1), - pe_tools.NBRE_SEMESTRES_DIPLOMANT, + pe_comp.NBRE_SEMESTRES_DIPLOMANT, ) ) @@ -486,9 +500,11 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b return False -def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]): + + +def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: """Renvoie le dernier semestre en **date de fin** d'un dictionnaire - de semestres de la forme ``{fid: FormSemestre(fid)}``. + de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``. Args: semestres: Un dictionnaire de semestres @@ -505,3 +521,5 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]): return dernier_semestre else: return None + + diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index f20dbf24..8fc4727b 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -1,8 +1,5 @@ -import pandas as pd -from app.pe import pe_tagtable -from app.pe.pe_tools import PE_DEBUG, pe_print -import app.pe.pe_etudiant as pe_etudiant +from app.pe.pe_tabletags import TableTag from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE from app.pe.pe_trajectoiretag import TrajectoireTag @@ -12,7 +9,7 @@ import pandas as pd import numpy as np -class AggregatInterclasseTag(pe_tagtable.TableTag): +class AggregatInterclasseTag(TableTag): """Interclasse l'ensemble des étudiants diplômés à une année donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S') en reportant : @@ -32,7 +29,7 @@ class AggregatInterclasseTag(pe_tagtable.TableTag): ): """""" """Table nommée au nom de l'aggrégat (par ex: 3S""" - pe_tagtable.TableTag.__init__(self, nom_aggregat) + TableTag.__init__(self, nom_aggregat) """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" self.diplomes_ids = etudiants.etudiants_diplomes diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py new file mode 100644 index 00000000..1f858504 --- /dev/null +++ b/app/pe/pe_jury.py @@ -0,0 +1,477 @@ +# -*- 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 +from zipfile import ZipFile + + +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.models import FormSemestre +from app.models.etudiants import Identite + +from app.scodoc.gen_tables import GenTable, SeqGenTable +import app.scodoc.sco_utils as scu +from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire +import app.pe.pe_comp as pe_comp +from app.pe.pe_semtag import SemestreTag +from app.pe.pe_interclasstag import AggregatInterclasseTag +from app.pe.pe_trajectoiretag import TrajectoireTag +import app.pe.pe_affichage as pe_affichage + +import pandas as pd +import numpy as np + +# ---------------------------------------------------------------------------------------- + + +# ---------------------------------------------------------------------------------------- +class JuryPE(object): + """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. + Modèle basé sur NotesTable. + + Attributs : + + * 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', '', }}`` + + Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue + et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif + """ + + # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et + # leur affichage dans les avis latex + + # ------------------------------------------------------------------------------------------------------------------ + 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, + 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. + + Args: + sem_base: le FormSemestre donnant le semestre à la base du jury PE + semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term) + meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, + si False, permet des programmes differents + """ + self.promoTagDict = {} + + "L'année du diplome" + self.diplome = 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") + + + + """Chargement des étudiants à prendre en compte dans le jury""" + pe_comp.pe_print( + f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}" + ) + self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants + self.etudiants.find_etudiants(self.formation_id) + self.diplomes_ids = self.etudiants.diplomes_ids + + """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" + pe_comp.pe_print("*** Génère les semestres taggués") + self.semestres_taggues = compute_semestres_tag(self.etudiants) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des semestres taggués au zip final""" + for fid in self.semestres_taggues: + formsemestretag = self.semestres_taggues[fid] + filename = formsemestretag.nom.replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, formsemestretag.str_tagtable(), path="details_semestres" + ) + + """Génère les trajectoires (combinaison de semestres suivis + par un étudiant pour atteindre le semestre final d'un aggrégat) + """ + pe_comp.pe_print( + "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" + ) + self.trajectoires = TrajectoiresJuryPE(self.diplome) + self.trajectoires.cree_trajectoires(self.etudiants) + + """Génère les moyennes par tags des trajectoires""" + pe_comp.pe_print("*** Calcule les moyennes par tag des trajectoires possibles") + self.trajectoires_tagguees = compute_trajectoires_tag( + self.trajectoires, self.etudiants, self.semestres_taggues + ) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des trajectoires tagguées au zip final""" + for trajectoire_id in self.trajectoires_tagguees: + trajectoire_tagguee = self.trajectoires_tagguees[trajectoire_id] + filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + trajectoire_tagguee.str_tagtable(), + path="details_semestres", + ) + + """Génère les interclassements (par promo et) par (nom d') aggrégat""" + pe_comp.pe_print("*** Génère les interclassements par aggrégat") + self.interclassements_taggues = compute_interclassements( + self.etudiants, self.trajectoires, self.trajectoires_tagguees + ) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des aggrégats (par promo) au zip final""" + for nom_aggregat in self.interclassements_taggues: + interclass_tag = self.interclassements_taggues[nom_aggregat] + filename = interclass_tag.get_repr().replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + interclass_tag.str_tagtable(), + path="details_semestres", + ) + + """Synthèse des éléments du jury PE""" + self.synthese = self.synthetise_juryPE() + + # Export des données => mode 1 seule feuille -> supprimé + pe_comp.pe_print("*** Export du jury de synthese") + filename = "synthese_jury_" + str(self.diplome) + '.xls' + with pd.ExcelWriter(filename, engine="openpyxl") as writer: + for onglet in self.synthese: + df = self.synthese[onglet] + df.to_excel(writer, onglet, index=True, header=True) # écriture dans l'onglet + # worksheet = writer.sheets[onglet] # l'on + + self.zipfile.write(filename) + + """Fin !!!! Tada :)""" + + def add_file_to_zip(self, filename: str, data, path=""): + """Add a file to our zip + All files under NOM_EXPORT_ZIP/ + path may specify a subdirectory + + Args: + 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(self.nom_export_zip, path, filename) + self.zipfile.writestr(path_in_zip, data) + + def get_zipped_data(self): + """returns file-like data with a zip of all generated (CSV) files. + Reset file cursor at the beginning ! + """ + if self.zipfile: + self.zipfile.close() + self.zipfile = None + self.zipdata.seek(0) + return self.zipdata + + def do_tags_list(self, interclassements: dict[str, AggregatInterclasseTag]): + """La liste des tags extraites des interclassements""" + tags = [] + for aggregat in interclassements: + interclass = interclassements[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_juryPE(self): + """Synthétise tous les résultats du jury PE dans des dataframes""" + + pe_comp.pe_print("*** Synthèse finale des moyennes ***") + + synthese = {} + pe_comp.pe_print(" -> Synthèse des données administratives") + synthese["administratif"] = self.df_administratif() + + tags = self.do_tags_list(self.interclassements_taggues) + for tag in tags: + pe_comp.pe_print(f" -> Synthèse du tag {tag}") + synthese[tag] = self.df_tag(tag) + return synthese + + + def df_administratif(self): + """Synthétise toutes les données administratives des étudiants""" + + etudids = list(self.diplomes_ids) + + """Récupération des données des étudiants""" + administratif = {} + nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes() + + for etudid in etudids: + etudiant = self.etudiants.identites[etudid] + cursus = self.etudiants.cursus[etudid] + formsemestres = cursus["formsemestres"] + + administratif[etudid] = { + "Nom": etudiant.nom, + "Prenom": etudiant.prenom, + "Civilite": etudiant.civilite_str, + "Age": pe_comp.calcul_age(etudiant.date_naissance), + "Date d'entree": cursus["entree"], + "Date de diplome": cursus["diplome"], + "Nbre de semestres": len(formsemestres) + } + + # Ajout des noms de semestres parcourus + etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max) + administratif[etudid] |= etapes + + """Construction du dataframe""" + df = pd.DataFrame.from_dict(administratif, orient='index') + return df + + + def df_tag(self, tag): + """Génère le DataFrame synthétisant les moyennes/classements (groupe, + interclassement promo) pour tous les aggrégats prévus, + tels que fourni dans l'excel final. + + Args: + tag: Un des tags (a minima `but`) + + Returns: + """ + + etudids = list(self.diplomes_ids) + aggregats = pe_comp.TOUS_LES_PARCOURS + + + donnees = {} + + for etudid in etudids: + etudiant = self.etudiants.identites[etudid] + donnees[etudid] = { + "Nom": etudiant.nom, + "Prenom": etudiant.prenom, + "Civilite": etudiant.civilite_str, + } + + for aggregat in aggregats: + """La trajectoire de l'étudiant sur l'aggrégat""" + trajectoire = self.trajectoires.suivi[etudid][aggregat] + """Les moyennes par tag de cette trajectoire""" + if trajectoire: + trajectoire_tagguee = self.trajectoires_tagguees[trajectoire.trajectoire_id] + bilan = trajectoire_tagguee.moyennes_tags[tag] + + donnees[etudid] |= { + f"{aggregat} notes ": f"{bilan['notes'].loc[etudid]:.1f}", + f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}", + f"{aggregat} min/moy/max (groupe)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}"} + else: + donnees[etudid] |= { + f"{aggregat} notes ": "-", + f"{aggregat} class. (groupe)": "-", + f"{aggregat} min/moy/max (groupe)": "-" + } + + """L'interclassement""" + interclass = self.interclassements_taggues[aggregat] + if tag in interclass.moyennes_tags: + bilan = interclass.moyennes_tags[tag] + + donnees[etudid] |= { + f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}", + f"{aggregat} min/moy/max (promo)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}" + } + else: + donnees[etudid] |= { + f"{aggregat} class. (promo)": "-", + f"{aggregat} min/moy/max (promo)": "-" + } + + # Fin de l'aggrégat + + df = pd.DataFrame.from_dict(donnees, orient='index') + return df + + + + def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury + """Table(s) du jury + mode: singlesheet ou multiplesheet pour export excel + """ + sT = SeqGenTable() # le fichier excel à générer + + + if mode == "singlesheet": + return sT.get_genTable("singlesheet") + else: + return sT + + +def compute_semestres_tag(etudiants: EtudiantsJuryPE): + """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. + Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire + des étudiants (cf. attribut etudiants.cursus). + En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé. + . + + Args: + etudiants: Un groupe d'étudiants participant au jury + + Returns: + Un dictionnaire {fid: SemestreTag(fid)} + """ + + """Création des semestres taggués, de type 'S1', 'S2', ...""" + pe_comp.pe_print("*** Création des semestres taggués") + + formsemestres = etudiants.get_formsemestres( + semestres_recherches=pe_comp.TOUS_LES_SEMESTRES + ) + + semestres_tags = {} + for frmsem_id, formsemestre in formsemestres.items(): + """Choix d'un nom pour le semestretag""" + nom = "S%d %d %d-%d" % ( + formsemestre.semestre_id, + frmsem_id, + formsemestre.date_debut.year, + formsemestre.date_fin.year, + ) + + pe_comp.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}") + + """Créé le semestre_tag et exécute les calculs de moyennes""" + formsemestretag = SemestreTag(nom, frmsem_id) + + """Stocke le semestre taggué""" + semestres_tags[frmsem_id] = formsemestretag + + return semestres_tags + + +def compute_trajectoires_tag( + trajectoires: TrajectoiresJuryPE, + etudiants: EtudiantsJuryPE, + semestres_taggues: dict[int, SemestreTag], +): + """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens + d'un aggrégat (par ex: '3S')), + en calculant les moyennes et les classements par tag pour chacune. + + Pour rappel : Chaque trajectoire est identifiée 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. + + * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les + notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en + date (le S2 redoublé par les redoublants est forcément antérieur) + + + Args: + etudiants: Les données des étudiants + semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) + + Return: + Un dictionnaire de la forme {nom_aggregat: {fid_terminal: SetTag(fid_terminal)} } + """ + + pe_comp.pe_print(" *** Création des aggrégats ") + + trajectoires_tagguees = {} + + for trajectoire_id in trajectoires.trajectoires: + trajectoire = trajectoires.trajectoires[trajectoire_id] + nom = trajectoire.get_repr() + + pe_comp.pe_print(f" --> Fusion {nom}") + + """Création de la trajectoire_tagguee associée""" + trajectoire_tagguee = TrajectoireTag( + nom, trajectoire, semestres_taggues, etudiants + ) + + """Mémorise le résultat""" + trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee + + return trajectoires_tagguees + + +def compute_interclassements( + etudiants: EtudiantsJuryPE, + trajectoires_jury_pe: TrajectoiresJuryPE, + trajectoires_tagguees: dict[tuple, Trajectoire], +): + """Interclasse les étudiants, (nom d') aggrégat par aggrégat, + pour fournir un classement sur la promo. Le classement est établi au regard du nombre + d'étudiants ayant participé au même aggrégat. + """ + pe_comp.pe_print(" Interclassement sur la promo") + + aggregats_interclasses_taggues = {} + for nom_aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS: + pe_comp.pe_print(f" --> {nom_aggregat}") + interclass = AggregatInterclasseTag( + nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees + ) + aggregats_interclasses_taggues[nom_aggregat] = interclass + return aggregats_interclasses_taggues diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py deleted file mode 100644 index 932d03e4..00000000 --- a/app/pe/pe_jurype.py +++ /dev/null @@ -1,736 +0,0 @@ -# -*- 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 -""" -import datetime - -# ---------------------------------------------------------- -# 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 -from zipfile import ZipFile - - -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.pe.pe_semestretag import SemestreTag -from app.pe.pe_interclasstag import AggregatInterclasseTag -from app.pe.pe_trajectoiretag import TrajectoireTag - -from app.scodoc.gen_tables import GenTable, SeqGenTable -import app.scodoc.sco_utils as scu -from app.scodoc import codes_cursus -from app.pe import pe_tools -from app.pe import pe_semestretag -from app.pe.pe_etudiant import EtudiantsJuryPE -from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire - - -# ---------------------------------------------------------------------------------------- -def comp_nom_semestre_dans_parcours(sem): - """Le nom a afficher pour titrer un semestre - par exemple: "semestre 2 FI 2015" - """ - formation: Formation = Formation.query.get_or_404(sem["formation_id"]) - parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) - return "%s %s %s %s" % ( - parcours.SESSION_NAME, # eg "semestre" - sem["semestre_id"], # eg 2 - sem.get("modalite", ""), # eg FI ou FC - sem["annee_debut"], # eg 2015 - ) - - -# ---------------------------------------------------------------------------------------- -class JuryPE(object): - """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. - Modèle basé sur NotesTable. - - Attributs : - - * 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', '', }}`` - - Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue - et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif - """ - - # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et - # leur affichage dans les avis latex - - # ------------------------------------------------------------------------------------------------------------------ - 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, - 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. - - Args: - sem_base: le FormSemestre donnant le semestre à la base du jury PE - semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term) - meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, - si False, permet des programmes differents - """ - self.promoTagDict = {} - - "L'année du diplome" - self.diplome = 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.syntheseJury = {} # Le jury de synthèse - - """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 = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants - self.etudiants.find_etudiants(self.formation_id) - - """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" - pe_tools.pe_print("*** Génère les semestres taggués") - self.semestres_taggues = compute_semestres_tag(self.etudiants) - - if pe_tools.PE_DEBUG: - """Intègre le bilan des semestres taggués au zip final""" - for fid in self.semestres_taggues: - formsemestretag = self.semestres_taggues[fid] - filename = formsemestretag.nom.replace(" ", "_") + ".csv" - pe_tools.pe_print(f" - Export csv de {filename} ") - self.add_file_to_zip( - filename, formsemestretag.str_tagtable(), path="details_semestres" - ) - - """Génère les trajectoires (combinaison de semestres suivis - par un étudiant pour atteindre le semestre final d'un aggrégat) - """ - pe_tools.pe_print( - "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" - ) - self.trajectoires = TrajectoiresJuryPE(self.diplome) - self.trajectoires.cree_trajectoires(self.etudiants) - - """Génère les moyennes par tags des trajectoires""" - pe_tools.pe_print("*** Calcule les moyennes par tag des trajectoires possibles") - self.trajectoires_tagguees = compute_trajectoires_tag( - self.trajectoires, self.etudiants, self.semestres_taggues - ) - - if pe_tools.PE_DEBUG: - """Intègre le bilan des trajectoires tagguées au zip final""" - for trajectoire_id in self.trajectoires_tagguees: - trajectoire_tagguee = self.trajectoires_tagguees[trajectoire_id] - filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv" - pe_tools.pe_print(f" - Export csv de {filename} ") - self.add_file_to_zip( - filename, - trajectoire_tagguee.str_tagtable(), - path="details_semestres", - ) - - """Génère les interclassements (par promo et) par (nom d') aggrégat""" - pe_tools.pe_print("*** Génère les interclassements par aggrégat") - self.interclassements_taggues = compute_interclassements( - self.etudiants, self.trajectoires, self.trajectoires_tagguees - ) - - if pe_tools.PE_DEBUG: - """Intègre le bilan des aggrégats (par promo) au zip final""" - for nom_aggregat in self.interclassements_taggues: - interclass_tag = self.interclassements_taggues[nom_aggregat] - filename = interclass_tag.get_repr().replace(" ", "_") + ".csv" - pe_tools.pe_print(f" - Export csv de {filename} ") - self.add_file_to_zip( - filename, - interclass_tag.str_tagtable(), - path="details_semestres", - ) - - """Synthèse des éléments du jury PE""" - self.synthetise_juryPE() - - # Export des données => mode 1 seule feuille -> supprimé - # filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls' - # self.xls = self.table_syntheseJury(mode="singlesheet") - # self.add_file_to_zip(filename, self.xls.excel()) - - # Fabrique 1 fichier excel résultat avec 1 seule feuille => trop gros - if False: - filename = self.nom_export_zip + "_jurySyntheseDict" + scu.XLSX_SUFFIX - self.xlsV2 = self.table_syntheseJury(mode="multiplesheet") - if self.xlsV2: - pe_tools.add_file_to_zip( - self.nom_export_zip, filename, self.xlsV2.excel() - ) - - # Pour debug - # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug - - def add_file_to_zip(self, filename: str, data, path=""): - """Add a file to our zip - All files under NOM_EXPORT_ZIP/ - path may specify a subdirectory - - Args: - 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(self.nom_export_zip, path, filename) - self.zipfile.writestr(path_in_zip, data) - - def get_zipped_data(self): - """returns file-like data with a zip of all generated (CSV) files. - Reset file cursor at the beginning ! - """ - if self.zipfile: - self.zipfile.close() - self.zipfile = None - self.zipdata.seek(0) - return self.zipdata - - # **************************************************************************************************************** # - # Traitements des semestres impliqués dans le jury - # **************************************************************************************************************** # - - # **************************************************************************************************************** # - # Méthodes pour la synthèse du juryPE - # ***************************************************************************************************************** - def synthetise_juryPE(self): - """Synthétise tous les résultats du jury PE dans des dataframess""" - self.syntheseJury = {} - for etudid in self.etudiants.get_etudids(self.diplome): - etudinfo = self.ETUDINFO_DICT[etudid] - self.syntheseJury[etudid] = { - "nom": etudinfo["nom"], - "prenom": etudinfo["prenom"], - "civilite": etudinfo["civilite"], - "civilite_str": etudinfo["civilite_str"], - "age": str(pe_tools.calcul_age(etudinfo["date_naissance"])), - "lycee": etudinfo["nomlycee"] - + ( - " (" + etudinfo["villelycee"] + ")" - if etudinfo["villelycee"] != "" - else "" - ), - "bac": etudinfo["bac"], - "code_nip": etudinfo["code_nip"], # pour la photo - "entree": self.get_dateEntree(etudid), - "promo": self.diplome, - } - # Le parcours - self.syntheseJury[etudid]["parcours"] = self.get_parcoursIUT( - etudid - ) # liste des semestres - self.syntheseJury[etudid]["nbSemestres"] = len( - self.syntheseJury[etudid]["parcours"] - ) # nombre de semestres - - # Ses résultats - 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.etudiants.cursus[etudid][nom] != None - ): # Un parcours valide existe - if nom in pe_tools.TOUS_LES_SEMESTRES: - tagtable = self.semestres_taggues[ - self.etudiants.cursus[etudid][nom] - ] - else: - tagtable = self.trajectoires_tagguees[nom][ - self.etudiants.cursus[etudid][nom] - ] - for tag in tagtable.get_all_tags(): - self.syntheseJury[etudid][nom]["groupe"][ - tag - ] = tagtable.get_resultatsEtud( - tag, etudid - ) # Le tuple des résultats - - # interclassé dans la promo - tagtable = self.promoTagDict[nom] - for tag in tagtable.get_all_tags(): - self.syntheseJury[etudid][nom]["promo"][ - tag - ] = tagtable.get_resultatsEtud(tag, etudid) - - def get_dateEntree(self, etudid): - """Renvoie l'année d'entrée de l'étudiant à l'IUT""" - # etudinfo = self.ETUDINFO_DICT[etudid] - semestres = self.get_semestresBUT_d_un_etudiant(etudid) - if semestres: - # le 1er sem à l'IUT - return semestres[0]["annee_debut"] - else: - return "" - - 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.etudiants.semestres_etudiant(etudid) - - infos = [] - for sem in sems: - nomsem = comp_nom_semestre_dans_parcours(sem) - infos.append( - { - "nom_semestre_dans_parcours": nomsem, - "titreannee": sem["titreannee"], - "formsemestre_id": sem["formsemestre_id"], # utile dans le futur ? - } - ) - return infos - - # **************************************************************************************************************** # - # Méthodes d'affichage pour debug - # **************************************************************************************************************** # - def str_etudiants_in_jury(self, delim=";"): - # En tete: - entete = ["Id", "Nom", "Abandon", "Diplome"] - for nom_sem in pe_tools.TOUS_LES_PARCOURS: - entete += [nom_sem, "descr"] - chaine = delim.join(entete) + "\n" - - for etudid in self.etudiants.cursus: - donnees = self.etudiants.cursus[etudid] - # pe_tools.pe_print(etudid, donnees) - # les infos générales - descr = [ - etudid, - donnees["nom"], - str(donnees["abandon"]), - str(donnees["diplome"]), - ] - - # les semestres et les aggrégats - for nom_sem in pe_tools.TOUS_LES_PARCOURS: - table = ( - self.semestres_taggues[donnees[nom_sem]].nom - if donnees[nom_sem] in self.semestres_taggues - else "manquant" - ) - descr += [ - donnees[nom_sem] if donnees[nom_sem] != None else "manquant", - table, - ] - - chaine += delim.join(descr) + "\n" - return chaine - - # - def export_juryPEDict(self): - """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" - self.zipfile.writestr(filename, self.str_etudiants_in_jury()) - - def get_allTagForAggregat(self, nom_aggregat): - """Extrait du dictionnaire syntheseJury la liste des tags d'un semestre ou - d'un aggrégat donné par son nom (S1, S2, S3 ou S4, 1A, ...). Renvoie [] si aucun tag. - """ - taglist = set() - for etudid in self.etudiants.get_etudids(): - taglist = taglist.union( - set(self.syntheseJury[etudid][nom_aggregat]["groupe"].keys()) - ) - taglist = taglist.union( - set(self.syntheseJury[etudid][nom_aggregat]["promo"].keys()) - ) - return list(taglist) - - def get_allTagInSyntheseJury(self): - """Extrait tous les tags du dictionnaire syntheseJury trié par - ordre alphabétique. [] si aucun tag""" - allTags = set() - 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 [] - - def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury - """Table(s) du jury - mode: singlesheet ou multiplesheet pour export excel - """ - sT = SeqGenTable() # le fichier excel à générer - - # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom - donnees_tries = sorted( - [ - ( - etudid, - self.syntheseJury[etudid]["nom"] - + " " - + self.syntheseJury[etudid]["prenom"], - ) - for etudid in self.syntheseJury.keys() - ], - key=lambda c: c[1], - ) - etudids = [e[0] for e in donnees_tries] - if not etudids: # Si pas d'étudiants - T = GenTable( - columns_ids=["pas d'étudiants"], - rows=[], - titles={"pas d'étudiants": "pas d'étudiants"}, - html_sortable=True, - xls_sheet_name="but", - ) - sT.add_genTable("but", T) - return sT - - # Si des étudiants - maxParcours = max( - [self.syntheseJury[etudid]["nbSemestres"] for etudid in etudids] - ) - - infos = ["civilite", "nom", "prenom", "age", "nbSemestres"] - entete = ["etudid"] - entete.extend(infos) - entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) - champs = [ - "note", - "class groupe", - "class promo", - "min/moy/max groupe", - "min/moy/max promo", - ] - - # Les aggrégats à afficher par ordre tel que indiqué dans le dictionnaire parcours - aggregats = list(pe_tools.PARCOURS.keys()) # ['S1', 'S2', ..., '1A', '4S'] - # aggregats = sorted( - # aggregats, key=lambda t: pe_tools.PARCOURS[t]["ordre"] - # ) # Tri des aggrégats - - if mode == "multiplesheet": - allSheets = ( - self.get_allTagInSyntheseJury() - ) # tous les tags de syntheseJuryDict - allSheets = sorted(allSheets) # Tri des tags par ordre alphabétique - 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 ( - 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] - ) - - columns_ids = entete # les id et les titres de colonnes sont ici identiques - titles = {i: i for i in columns_ids} - - for ( - sheet - ) in ( - allSheets - ): # Pour tous les sheets à générer (1 si singlesheet, autant que de tags si multiplesheet) - rows = [] - for etudid in etudids: - e = self.syntheseJury[etudid] - # Les info générales: - row = { - "etudid": etudid, - "civilite": e["civilite"], - "nom": e["nom"], - "prenom": e["prenom"], - "age": e["age"], - "nbSemestres": e["nbSemestres"], - } - # Les parcours: P1, P2, ... - n = 1 - for p in e["parcours"]: - row["P%d" % n] = p["titreannee"] - n += 1 - # if self.syntheseJury[etudid]['nbSemestres'] < maxParcours: - # descr += delim.join( ['']*( maxParcours -self.syntheseJury[etudid]['nbSemestres']) ) + delim - for sem in aggregats: # pe_tools.PARCOURS.keys(): - listeTags = ( - self.get_allTagForAggregat(sem) - if mode == "singlesheet" - else [sheet] - ) - for tag in listeTags: - if tag in self.syntheseJury[etudid][sem]["groupe"]: - resgroupe = self.syntheseJury[etudid][sem]["groupe"][ - tag - ] # tuple - else: - resgroupe = (None, None, None, None, None, None, None) - if tag in self.syntheseJury[etudid][sem]["promo"]: - respromo = self.syntheseJury[etudid][sem]["promo"][tag] - else: - respromo = (None, None, None, None, None, None, None) - - # note = "%2.2f" % resgroupe[0] if isinstance(resgroupe[0], float) else str(resgroupe[0]) - champ = ( - "%s %s " % (sem, tag) - if mode == "singlesheet" - else "%s " % (sem) - ) - row[champ + "note"] = scu.fmt_note(resgroupe[0]) - row[champ + "class groupe"] = "%s / %s" % ( - resgroupe[2] if resgroupe[2] else "-", - resgroupe[3] if resgroupe[3] else "-", - ) - row[champ + "class promo"] = "%s / %s" % ( - respromo[2] if respromo[2] else "-", - respromo[3] if respromo[3] else "-", - ) - row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple( - (scu.fmt_note(x) if x is not None else "-") - for x in (resgroupe[6], resgroupe[4], resgroupe[5]) - ) - row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple( - (scu.fmt_note(x) if x is not None else "-") - for x in (respromo[6], respromo[4], respromo[5]) - ) - rows.append(row) - - T = GenTable( - columns_ids=columns_ids, - rows=rows, - titles=titles, - html_sortable=True, - xls_sheet_name=sheet, - ) - sT.add_genTable(sheet, T) - - if mode == "singlesheet": - return sT.get_genTable("singlesheet") - else: - return sT - - # **************************************************************************************************************** # - # Méthodes de classe pour gestion d'un cache de données accélérant les calculs / intérêt à débattre - # **************************************************************************************************************** # - - # ------------------------------------------------------------------------------------------------------------------ - 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] = Identite.get_etud(etudid=etudid) - # sco_etud.get_etud_info( - # etudid=etudid, filled=True - # ))[0] - return self.ETUDINFO_DICT[etudid] - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat: - """Charge la table des notes d'un formsemestre""" - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - return res_sem.load_formsemestre_results(formsemestre) - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_semestresBUT_d_un_etudiant(self, identite: Identite, semestre_id=None): - """cf. pe_etudiant.semestres_etudiant()""" - - return None - - # ********************************************* - # Fonctions d'affichage pour debug - def get_resultat_d_un_etudiant(self, etudid): - chaine = "" - 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.semestres_taggues[semtagid] - chaine += "Semestre " + nom_sem + str(semtagid) + "\n" - # le détail du calcul tag par tag - # chaine += "Détail du calcul du tag\n" - # chaine += "-----------------------\n" - # for tag in semtag.taglist: - # chaine += "Tag=" + tag + "\n" - # chaine += semtag.str_detail_resultat_d_un_tag(tag, etudid=etudid) + "\n" - # le bilan des tags - chaine += "Bilan des tags\n" - chaine += "--------------\n" - for tag in semtag.taglist: - chaine += ( - tag + ";" + semtag.str_resTag_d_un_etudiant(tag, etudid) + "\n" - ) - chaine += "\n" - return chaine - - def get_date_entree_etudiant(self, etudid) -> str: - """Renvoie la date d'entree d'un étudiant: "1996" """ - annees_debut = [ - int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"] - ] - if annees_debut: - return str(min(annees_debut)) - return "" - - -def compute_semestres_tag(etudiants: EtudiantsJuryPE): - """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. - Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire - des étudiants (cf. attribut etudiants.cursus). - En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé. - . - - Args: - etudiants: Un groupe d'étudiants participant au jury - - Returns: - Un dictionnaire {fid: SemestreTag(fid)} - """ - - """Création des semestres taggués, de type 'S1', 'S2', ...""" - pe_tools.pe_print("*** Création des semestres taggués") - - formsemestres = etudiants.get_formsemestres( - semestres_recherches=pe_tools.TOUS_LES_SEMESTRES - ) - - semestres_tags = {} - for frmsem_id, formsemestre in formsemestres.items(): - """Choix d'un nom pour le semestretag""" - nom = "S%d %d %d-%d" % ( - formsemestre.semestre_id, - frmsem_id, - formsemestre.date_debut.year, - formsemestre.date_fin.year, - ) - - pe_tools.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}") - - """Créé le semestre_tag et exécute les calculs de moyennes""" - formsemestretag = pe_semestretag.SemestreTag(nom, frmsem_id) - - """Stocke le semestre taggué""" - semestres_tags[frmsem_id] = formsemestretag - - return semestres_tags - - -def compute_trajectoires_tag( - trajectoires: TrajectoiresJuryPE, - etudiants: EtudiantsJuryPE, - semestres_taggues: dict[int, SemestreTag], -): - """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens - d'un aggrégat (par ex: '3S')), - en calculant les moyennes et les classements par tag pour chacune. - - Pour rappel : Chaque trajectoire est identifiée 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. - - * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les - notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en - date (le S2 redoublé par les redoublants est forcément antérieur) - - - Args: - etudiants: Les données des étudiants - semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) - - Return: - Un dictionnaire de la forme {nom_aggregat: {fid_terminal: SetTag(fid_terminal)} } - """ - - pe_tools.pe_print(" *** Création des aggrégats ") - - trajectoires_tagguees = {} - - for trajectoire_id in trajectoires.trajectoires: - trajectoire = trajectoires.trajectoires[trajectoire_id] - nom = trajectoire.get_repr() - - pe_tools.pe_print(f" --> Fusion {nom}") - - """Création de la trajectoire_tagguee associée""" - trajectoire_tagguee = TrajectoireTag( - nom, trajectoire, semestres_taggues, etudiants - ) - - """Mémorise le résultat""" - trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee - - return trajectoires_tagguees - - -def compute_interclassements( - etudiants: EtudiantsJuryPE, - trajectoires_jury_pe: TrajectoiresJuryPE, - trajectoires_tagguees: dict[tuple, Trajectoire], -): - """Interclasse les étudiants, (nom d') aggrégat par aggrégat, - pour fournir un classement sur la promo. Le classement est établi au regard du nombre - d'étudiants ayant participé au même aggrégat. - """ - pe_tools.pe_print(" Interclassement sur la promo") - - aggregats_interclasses_taggues = {} - for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: - pe_tools.pe_print(f" --> {nom_aggregat}") - interclass = AggregatInterclasseTag( - nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees - ) - aggregats_interclasses_taggues[nom_aggregat] = interclass - return aggregats_interclasses_taggues diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semtag.py similarity index 97% rename from app/pe/pe_semestretag.py rename to app/pe/pe_semtag.py index a97c5000..dbf0c02a 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semtag.py @@ -37,22 +37,18 @@ Created on Fri Sep 9 09:15:05 2016 """ from app import db, log -from app.comp import res_sem, inscr_mod, moy_ue, moy_sem -from app.comp.res_common import ResultatsSemestre +from app.comp import res_sem, moy_ue, moy_sem from app.comp.res_compat import NotesTableCompat from app.comp.res_sem import load_formsemestre_results -from app.models import FormSemestre, Identite, DispenseUE +from app.models import FormSemestre from app.models.moduleimpls import ModuleImpl -from app.pe import pe_tagtable -from app.pe import pe_tools -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 +import app.pe.pe_comp as pe_comp +from app.pe.pe_tabletags import (TableTag, TAGS_RESERVES) - -class SemestreTag(pe_tagtable.TableTag): +class SemestreTag(TableTag): """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. @@ -67,10 +63,8 @@ class SemestreTag(pe_tagtable.TableTag): nom: Nom à donner au SemestreTag formsemestre_id: Identifiant du FormSemestre sur lequel il se base """ - pe_tagtable.TableTag.__init__( - self, - nom=nom - ) + TableTag.__init__(self, nom=nom) + """Le semestre""" self.formsemestre_id = formsemestre_id self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) @@ -97,7 +91,7 @@ class SemestreTag(pe_tagtable.TableTag): """Les tags (en supprimant les tags réservés)""" self.tags = get_synthese_tags_semestre(self.nt.formsemestre) - for tag in pe_tagtable.TAGS_RESERVES: + for tag in TAGS_RESERVES: if tag in self.tags: del self.tags[tag] @@ -105,7 +99,7 @@ class SemestreTag(pe_tagtable.TableTag): self.moyennes_tags = {} for tag in self.tags: - pe_tools.pe_print(f" -> Traitement du tag {tag}") + pe_comp.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] = { @@ -118,7 +112,7 @@ class SemestreTag(pe_tagtable.TableTag): } """Ajoute les moyennes générales de BUT pour le semestre considéré""" - pe_tools.pe_print(f" -> Traitement du tag but") + pe_comp.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"] = { diff --git a/app/pe/pe_settag_interclasse.py b/app/pe/pe_settag_interclasse.py deleted file mode 100644 index 83abe82d..00000000 --- a/app/pe/pe_settag_interclasse.py +++ /dev/null @@ -1,128 +0,0 @@ -from app.pe import pe_tagtable -from app.pe.pe_tools import PE_DEBUG, pe_print -import app.pe.pe_etudiant as pe_etudiant -from app.pe.pe_etudiant import EtudiantsJuryPE - -class SetTagInterClasse(pe_tagtable.TableTag): - """Interclasse les étudiants d'une promo (ceux diplômé) par aggrégat de même nom - (par ex: un "3S"), pour stocker leur moyenne et fournir un classement de "promo". - - Les - """ - - # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom:str, etudiants: EtudiantsJuryPE, aggregats_taggues: dict[str, dict]): - """""" - pe_tagtable.TableTag.__init__(self, nom) - self.etudiants = etudiants - self.aggregats_taggues = aggregats_taggues # Les moyennes par aggrégats - - - # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): - """Détermine la liste des étudiants à prendre en compte, en partant de - la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. - Renvoie le nombre d'étudiants effectivement inscrits.""" - if nom_sem_final: - self.nom += "_" + nom_sem_final - for etudid in etudiants: - if juryPEDict[etudid][self.combinaison] != None: - self.inscrlist.append(etudInfoDict[etudid]) - self.identdict[etudid] = etudInfoDict[etudid] - self.parcoursDict[etudid] = juryPEDict[etudid] - return len(self.inscrlist) - - # ------------------------------------------------------------------------------------------- - def get_Fids_in_settag(self): - """Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte - pour les moyennes, en considérant tous les étudiants inscrits""" - return list( - {self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict} - ) - - # --------------------------------------------------------------------------------------------- - def set_SetTagDict(self, SetTagDict): - """Mémorise les settag nécessaires au jury.""" - self.SetTagDict = { - fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None - } - if PE_DEBUG >= 1: - pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_data_settag(self): - """Calcule tous les données numériques relatives au settag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.taglist = self.do_taglist() - - # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) - - # Calcul des moyennes de chaque étudiant par tag - reussiteAjoutTag = {"OK": [], "KO": []} - for tag in self.taglist: - moyennes = self.get_MoyennesSetTag(tag, force=False) - res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne - reussiteAjoutTag["OK" if res else "KO"].append(tag) - if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: - pe_print( - " => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"])) - + ", ".join(reussiteAjoutTag["OK"]) - ) - if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: - pe_print( - " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) - + ", ".join(reussiteAjoutTag["KO"]) - ) - - # ------------------------------------------------------------------------------------------------------------------- - def get_etudids(self): - return list(self.identdict.keys()) - - # ------------------------------------------------------------------------------------------------------------------- - def do_taglist(self): - """Parcourt les tags des semestres taggués et les synthétise sous la forme - d'une liste en supprimant les doublons - """ - ensemble = [] - for settag in self.SetTagDict.values(): - ensemble.extend(settag.get_all_tags()) - return sorted(list(set(ensemble))) - - # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): - """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) - avec notes et coeffs deux listes""" - leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] - - note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid) - coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats( - tag, etudid - ) - return (note, coeff) - - # ------------------------------------------------------------------------------------------------------------------- - def get_MoyennesSetTag(self, tag, force=False): - """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, - et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération - appliqué dans cette moyenne. - - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - - Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} - """ - # if tag not in self.get_all_tags() : return None - - # Calcule les moyennes - lesMoyennes = [] - for ( - etudid - ) in ( - self.get_etudids() - ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag - (moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant( - tag, etudid - ) # lecture des notes associées au tag - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tabletags.py similarity index 100% rename from app/pe/pe_tagtable.py rename to app/pe/pe_tabletags.py diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py deleted file mode 100644 index c5ada421..00000000 --- a/app/pe/pe_tools.py +++ /dev/null @@ -1,1126 +0,0 @@ -# -*- 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 Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import os -import datetime -import re -import unicodedata - - -from flask import g - -import app.scodoc.sco_utils as scu -from app import log -from app.models import FormSemestre -from app.scodoc import sco_formsemestre -from app.scodoc.sco_logos import find_logo - -PE_DEBUG = 1 - -if not PE_DEBUG: - # log to notes.log - def pe_print(*a, **kw): - # kw is ignored. log always add a newline - log(" ".join(a)) - -else: - pe_print = print # print function - - -# Generated LaTeX files are encoded as: -PE_LATEX_ENCODING = "utf-8" - -# /opt/scodoc/tools/doc_poursuites_etudes -REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/") -REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/") - -PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex" -PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex" -PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex" -PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" - -# ---------------------------------------------------------------------------------------- - -""" -Descriptif d'un parcours classique BUT - -TODO:: A améliorer si BUT en moins de 6 semestres -""" - -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)", - }, -} -NBRE_SEMESTRES_DIPLOMANT = 6 -AGGREGAT_DIPLOMANT = ( - "6S" # aggrégat correspondant à la totalité des notes pour le diplôme -) -TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"] -TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] -TOUS_LES_PARCOURS = list(PARCOURS.keys()) - -# ---------------------------------------------------------------------------------------- -def calcul_age(born): - """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. - Aucun test de validité sur le format de la date n'est fait. - """ - if not isinstance(born, str) or born == "": - return "" - - donnees = born.split("/") - naissance = datetime.datetime(int(donnees[2]), int(donnees[1]), int(donnees[0])) - today = datetime.date.today() - return ( - today.year - - naissance.year - - ((today.month, today.day) < (naissance.month, naissance.day)) - ) - - -def remove_accents(input_unicode_str): - """Supprime les accents d'une chaine unicode""" - nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) - only_ascii = nfkd_form.encode("ASCII", "ignore") - return only_ascii - - -def escape_for_latex(s): - """Protège les caractères pour inclusion dans du source LaTeX""" - if not s: - return "" - conv = { - "&": r"\&", - "%": r"\%", - "$": r"\$", - "#": r"\#", - "_": r"\_", - "{": r"\{", - "}": r"\}", - "~": r"\textasciitilde{}", - "^": r"\^{}", - "\\": r"\textbackslash{}", - "<": r"\textless ", - ">": r"\textgreater ", - } - exp = re.compile( - "|".join( - re.escape(key) - for key in sorted(list(conv.keys()), key=lambda item: -len(item)) - ) - ) - return exp.sub(lambda match: conv[match.group()], s) - - -# ---------------------------------------------------------------------------------------- -def list_directory_filenames(path): - """List of regular filenames in a directory (recursive) - Excludes files and directories begining with . - """ - R = [] - for root, dirs, files in os.walk(path, topdown=True): - dirs[:] = [d for d in dirs if d[0] != "."] - R += [os.path.join(root, fn) for fn in files if fn[0] != "."] - return R - - -def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): - """Read pathname server file and add content to zip under path_in_zip""" - rooted_path_in_zip = os.path.join(ziproot, path_in_zip) - zipfile.write(filename=pathname, arcname=rooted_path_in_zip) - # data = open(pathname).read() - # zipfile.writestr(rooted_path_in_zip, data) - - -def add_refs_to_register(register, directory): - """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme - filename => pathname - """ - length = len(directory) - for pathname in list_directory_filenames(directory): - filename = pathname[length + 1 :] - register[filename] = pathname - - -def add_pe_stuff_to_zip(zipfile, ziproot): - """Add auxiliary files to (already opened) zip - Put all local files found under config/doc_poursuites_etudes/local - and config/doc_poursuites_etudes/distrib - If a file is present in both subtrees, take the one in local. - - Also copy logos - """ - register = {} - # first add standard (distrib references) - distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") - add_refs_to_register(register=register, directory=distrib_dir) - # then add local references (some oh them may overwrite distrib refs) - local_dir = os.path.join(REP_LOCAL_AVIS, "local") - add_refs_to_register(register=register, directory=local_dir) - # at this point register contains all refs (filename, pathname) to be saved - for filename, pathname in register.items(): - add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) - - # Logos: (add to logos/ directory in zip) - logos_names = ["header", "footer"] - for name in logos_names: - logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) - if logo is not None: - add_local_file_to_zip( - zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename - ) - - -# ---------------------------------------------------------------------------------------- -def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int: - """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres) - et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle - sera remis le diplôme BUT des étudiants qui y sont scolarisés - (en supposant qu'il n'y ait pas de redoublement à venir). - - **Remarque sur le calcul** : 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'année 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 - - La fonction est adaptée au cas des semestres décalés. - - Par exemple : - - * S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026 - * S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027 - - Args: - sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit : - - * un ``FormSemestre`` (Scodoc9) - * un dict (format compatible avec Scodoc7) - - nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT) - """ - - if isinstance(sem_base, FormSemestre): - sem_id = sem_base.semestre_id - annee_fin = sem_base.date_fin.year - annee_debut = sem_base.date_debut.year - else: # sem_base est un dictionnaire (Scodoc 7) - sem_id = sem_base["semestre_id"] - annee_fin = int(sem_base["annee_fin"]) - annee_debut = int(sem_base["annee_debut"]) - if ( - 1 <= sem_id <= nbre_sem_formation - ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? - nbreSemRestant = ( - nbre_sem_formation - sem_id - ) # nombre de semestres restant avant diplome - nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome - # Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé - # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon - delta = annee_fin - annee_debut - decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 - increment = decalage * (1 - delta) - return annee_fin + nbreAnRestant + increment - - -def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list: - """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome`` - et s'intégrant à la formation donnée par son ``formation_id``. - - **Définition** : Un co-semestre est un semestre : - - * dont l'année de diplômation prédite (sans redoublement) est la même - * dont la formation est la même (optionnel) - * qui a des étudiants inscrits - - Si formation_id == None, ne prend pas en compte l'identifiant de formation - TODO:: A raccrocher à un programme - - Args: - annee_diplome: L'année de diplomation - formation_id: L'identifiant de la formation - """ - tousLesSems = ( - sco_formsemestre.do_formsemestre_list() - ) # tous les semestres memorisés dans scodoc - - if formation_id: - cosemestres_fids = { - sem["id"] - for sem in tousLesSems - if get_annee_diplome_semestre(sem) == annee_diplome - and sem["formation_id"] == formation_id - } - else: - cosemestres_fids = { - sem["id"] - for sem in tousLesSems - if get_annee_diplome_semestre(sem) == annee_diplome - } - - cosemestres = {} - for fid in cosemestres_fids: - cosem = FormSemestre.get_formsemestre(fid) - if len(cosem.etuds_inscriptions) > 0: - cosemestres[fid] = cosem - - return cosemestres - - -# ---------------------------------------------------------------------------------------- -# Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long) -JURY_SYNTHESE_POUR_DEBUG = { - "EID1810": { - "nom": "ROUX", - "entree": "2016", - "civilite_str": "M.", - "promo": 2016, - "S2": { - "groupe": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "18", - 78, - 9.731491508491509, - 18.46846153846154, - 18.46846153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "16", - 78, - 9.948540264387688, - 18.29285714285714, - 18.29285714285714, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "20", - 78, - 9.83147528118408, - 17.691755169172936, - 17.691755169172936, - ), - "mathematiques": ( - 12.25, - 0.1, - "15 ex", - 78, - 8.45153073717949, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "19", - 78, - 10.151630181286441, - 17.881104750512645, - 17.881104750512645, - ), - }, - "promo": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "25", - 73, - 11.696187214611871, - 18.51346153846154, - 18.51346153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "23", - 73, - 11.862307379173147, - 17.616047267953675, - 17.616047267953675, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "28", - 73, - 11.571004424603757, - 16.706338951857248, - 16.706338951857248, - ), - "mathematiques": ( - 12.25, - 0.1, - "18 ex", - 73, - 10.00886454908676, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "25", - 73, - 11.88798432763965, - 17.397627309377608, - 17.397627309377608, - ), - }, - }, - "S1": { - "groupe": { - "informatique": ( - 16.064999999999998, - 0.16666666666666669, - "11", - 82, - 11.020296296296294, - 19.325999999999997, - 19.325999999999997, - ), - "technique": ( - 14.513007894736845, - 0.6333333333333333, - "11", - 82, - 11.195082967479676, - 18.309764912280702, - 18.309764912280702, - ), - "pe": ( - 13.260301515151516, - 1.1, - "19", - 82, - 10.976036277232245, - 17.7460505050505, - 17.7460505050505, - ), - "mathematiques": ( - 11.142850000000001, - 0.13333333333333333, - "34", - 82, - 10.314605121951217, - 19.75, - 19.75, - ), - "dut": ( - 13.54367375, - 1.0, - "19", - 82, - 11.22193801880508, - 18.226902529333334, - 18.226902529333334, - ), - }, - "promo": { - "informatique": ( - 16.064999999999998, - 0.16666666666666669, - "15", - 73, - 13.265276712328768, - 19.325999999999997, - 19.325999999999997, - ), - "technique": ( - 14.513007894736845, - 0.6333333333333333, - "16", - 73, - 12.996048795361693, - 18.309764912280702, - 18.309764912280702, - ), - "pe": ( - 13.260301515151516, - 1.1, - "25", - 73, - 12.4107195879539, - 17.7460505050505, - 17.7460505050505, - ), - "mathematiques": ( - 11.142850000000001, - 0.13333333333333333, - "39", - 73, - 11.320606952054794, - 19.75, - 19.75, - ), - "dut": ( - 13.54367375, - 1.0, - "25", - 73, - 12.730581289342638, - 18.226902529333334, - 18.226902529333334, - ), - }, - }, - "4S": { - "groupe": { - "informatique": ( - 14.84359375, - 0.5333333333333333, - "2", - 19, - 10.69933552631579, - 18.28646875, - 18.28646875, - ), - "pe": ( - 12.93828572598162, - 3.75, - "4", - 19, - 11.861967145815218, - 15.737718967605682, - 15.737718967605682, - ), - "mathematiques": (None, None, "1 ex", 19, None, None, None), - "ptut": (None, None, "1 ex", 19, None, None, None), - "dut": ( - 13.511767410105122, - 4.0, - "4", - 19, - 12.573349864933606, - 15.781651391587998, - 15.781651391587998, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "S4": { - "groupe": { - "informatique": ( - 16.075, - 0.1, - "1", - 19, - 8.799078947368422, - 16.075, - 16.075, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "4", - 19, - 12.238304655870447, - 16.521153846153847, - 16.521153846153847, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "4", - 19, - 12.292846491228072, - 16.25833333333334, - 16.25833333333334, - ), - "dut": ( - 14.425416666666665, - 1.0, - "6", - 19, - 13.628367861842106, - 15.267566666666665, - 15.267566666666665, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "11", - 73, - 12.086685508009952, - 17.25909420289855, - 17.25909420289855, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "ptut": ( - 13.5, - 0.13333333333333333, - "50", - 73, - 13.898173515981734, - 17.083333333333332, - 17.083333333333332, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "1A": { - "groupe": { - "informatique": ( - 14.43673913043478, - 0.38333333333333336, - "16", - 78, - 11.046040002787066, - 18.85992173913043, - 18.85992173913043, - ), - "technique": ( - 13.754459142857144, - 1.25, - "14", - 78, - 11.179785631638866, - 18.493250340136054, - 18.493250340136054, - ), - "pe": ( - 12.633767581547854, - 2.216666666666667, - "18", - 78, - 10.912253971396854, - 18.39547581699347, - 18.39547581699347, - ), - "mathematiques": ( - 11.617342857142857, - 0.23333333333333334, - "24", - 78, - 9.921286855287565, - 19.375000000000004, - 19.375000000000004, - ), - "dut": ( - 12.990587518622945, - 2.0, - "18", - 78, - 11.2117147027821, - 18.391345156695156, - 18.391345156695156, - ), - }, - "promo": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "25", - 73, - 11.696187214611871, - 18.51346153846154, - 18.51346153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "23", - 73, - 11.862307379173147, - 17.616047267953675, - 17.616047267953675, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "28", - 73, - 11.571004424603757, - 16.706338951857248, - 16.706338951857248, - ), - "mathematiques": ( - 12.25, - 0.1, - "18 ex", - 73, - 10.00886454908676, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "25", - 73, - 11.88798432763965, - 17.397627309377608, - 17.397627309377608, - ), - }, - }, - "2A": { - "groupe": { - "informatique": ( - 15.88333333333333, - 0.15000000000000002, - "2", - 19, - 9.805818713450288, - 17.346666666666668, - 17.346666666666668, - ), - "pe": ( - 13.378513043478259, - 1.5333333333333334, - "6", - 19, - 12.099566454042717, - 16.06209927536232, - 16.06209927536232, - ), - "technique": ( - 13.965093333333336, - 1.1666666666666665, - "5", - 19, - 12.51068332957394, - 16.472092380952386, - 16.472092380952386, - ), - "mathematiques": (None, None, "1 ex", 19, None, None, None), - "dut": ( - 14.032947301587301, - 2.0, - "4", - 19, - 13.043386086541773, - 15.574706269841268, - 15.574706269841268, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "11", - 73, - 12.086685508009952, - 17.25909420289855, - 17.25909420289855, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "nbSemestres": 4, - "code_nip": "21414563", - "prenom": "Baptiste", - "age": "21", - "lycee": "PONCET", - "3S": { - "groupe": { - "informatique": ( - 14.559423076923077, - 0.43333333333333335, - "3", - 19, - 11.137856275303646, - 18.8095, - 18.8095, - ), - "pe": ( - 12.84815019664546, - 3.25, - "4", - 19, - 11.795678015751701, - 15.657624449801428, - 15.657624449801428, - ), - "technique": ( - 13.860638395358142, - 1.9833333333333334, - "3", - 19, - 12.395950358235925, - 17.340302131732695, - 17.340302131732695, - ), - "mathematiques": ( - 11.494044444444445, - 0.3, - "6", - 19, - 9.771571754385965, - 14.405358333333334, - 14.405358333333334, - ), - "dut": ( - 13.207217657917942, - 3.0, - "4", - 19, - 12.221677199297439, - 15.953012966561774, - 15.953012966561774, - ), - }, - "promo": { - "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "17", - 73, - 11.854843423685786, - 16.191317607526884, - 16.191317607526884, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "10", - 73, - 11.929466899200335, - 16.6400384469697, - 16.6400384469697, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "40", - 73, - 11.418430205479451, - 19.53, - 19.53, - ), - "dut": ( - 13.640477936507937, - 1.0, - "14", - 73, - 12.097377866597594, - 16.97088994741667, - 16.97088994741667, - ), - }, - }, - "bac": "STI2D", - "S3": { - "groupe": { - "informatique": (15.5, 0.05, "5", 19, 12.842105263157896, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "8", - 19, - 12.339608902093943, - 15.967147311827956, - 15.967147311827956, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "7", - 19, - 13.128539816586922, - 16.44310151515152, - 16.44310151515152, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "6", - 19, - 9.280921052631578, - 16.125, - 16.125, - ), - "dut": ( - 13.640477936507937, - 1.0, - "8", - 19, - 12.83638061385213, - 15.881845873015871, - 15.881845873015871, - ), - }, - "promo": { - "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "17", - 73, - 11.854843423685786, - 16.191317607526884, - 16.191317607526884, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "10", - 73, - 11.929466899200335, - 16.6400384469697, - 16.6400384469697, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "40", - 73, - 11.418430205479451, - 19.53, - 19.53, - ), - "dut": ( - 13.640477936507937, - 1.0, - "14", - 73, - 12.097377866597594, - 16.97088994741667, - 16.97088994741667, - ), - }, - }, - "parcours": [ - { - "nom_semestre_dans_parcours": "semestre 4 FAP 2016", - "titreannee": "DUT RT UFA (PPN 2013), semestre 4 FAP 2016", - }, - { - "nom_semestre_dans_parcours": "semestre 3 FAP 2015-2016", - "titreannee": "DUT RT UFA (PPN 2013), semestre 3 FAP 2015-2016", - }, - { - "nom_semestre_dans_parcours": "semestre 2 FI 2015", - "titreannee": "DUT RT, semestre 2 FI 2015", - }, - { - "nom_semestre_dans_parcours": "semestre 1 FI 2014-2015", - "titreannee": "DUT RT, semestre 1 FI 2014-2015", - }, - ], - } -} diff --git a/app/pe/pe_trajectoire.py b/app/pe/pe_trajectoire.py index 415fee24..d527de3c 100644 --- a/app/pe/pe_trajectoire.py +++ b/app/pe/pe_trajectoire.py @@ -1,4 +1,4 @@ -import app.pe.pe_tools as pe_tools +import app.pe.pe_comp as pe_tools from app.models import FormSemestre from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py index abefa008..de9449c2 100644 --- a/app/pe/pe_trajectoiretag.py +++ b/app/pe/pe_trajectoiretag.py @@ -38,18 +38,17 @@ Created on Fri Sep 9 09:15:05 2016 from app.comp import moy_sem from app.comp.res_sem import load_formsemestre_results -from app.models import FormSemestre -from app.pe.pe_semestretag import SemestreTag -from app.pe import pe_tagtable +from app.pe.pe_semtag import SemestreTag +from app.pe import pe_tabletags import pandas as pd import numpy as np from app.pe.pe_trajectoire import Trajectoire from app.pe.pe_etudiant import EtudiantsJuryPE -from app.pe.pe_trajectoire import TrajectoiresJuryPE +from app.pe.pe_tabletags import TableTag -class TrajectoireTag(pe_tagtable.TableTag): +class TrajectoireTag(TableTag): """Calcule les moyennes par tag d'une combinaison de semestres (trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et par un semestre terminal, pour extraire les classements par tag pour un @@ -69,7 +68,7 @@ class TrajectoireTag(pe_tagtable.TableTag): donnees_etudiants: EtudiantsJuryPE, ): """ """ - pe_tagtable.TableTag.__init__(self, nom=nom) + TableTag.__init__(self, nom=nom) """La trajectoire associée""" self.trajectoire_id = trajectoire.trajectoire_id From cd8d73b41f1e32098ef96d7b952839663ad28080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Thu, 25 Jan 2024 19:42:22 +0100 Subject: [PATCH 21/23] Version 2 fonctionnelle --- app/pe/pe_etudiant.py | 4 ++- app/pe/pe_jury.py | 71 +++++++++++++++++++------------------ app/pe/pe_trajectoiretag.py | 8 ++--- app/pe/pe_view.py | 22 ++++++------ 4 files changed, 53 insertions(+), 52 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 26edc720..ea043f3d 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -142,7 +142,9 @@ class EtudiantsJuryPE: + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) ) # Les abandons : - # sorted([etudiants.cursus[etudid]['nom'] for etudid in etudiants.cursus if etudid not in etudiants.diplomes_ids]) + self.abandons = sorted([self.cursus[etudid]['nom'] + for etudid in self.cursus if etudid not in self.diplomes_ids]) + def get_etudiants_diplomes(self) -> dict[int, Identite]: """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 1f858504..8e8f03fa 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -71,17 +71,17 @@ import numpy as np # ---------------------------------------------------------------------------------------- class JuryPE(object): """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. - Modèle basé sur NotesTable. + Modèle basé sur NotesTable. - Attributs : + Attributs : - * 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', '', }}`` - - Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue - et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif + * 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', '', }}`` + a + Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue + et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif """ # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et @@ -100,8 +100,6 @@ 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.promoTagDict = {} - "L'année du diplome" self.diplome = diplome @@ -113,8 +111,6 @@ class JuryPE(object): self.zipdata = io.BytesIO() self.zipfile = ZipFile(self.zipdata, "w") - - """Chargement des étudiants à prendre en compte dans le jury""" pe_comp.pe_print( f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}" @@ -187,14 +183,19 @@ class JuryPE(object): # Export des données => mode 1 seule feuille -> supprimé pe_comp.pe_print("*** Export du jury de synthese") - filename = "synthese_jury_" + str(self.diplome) + '.xls' + filename = "synthese_jury_" + str(self.diplome) + ".xlsx" with pd.ExcelWriter(filename, engine="openpyxl") as writer: for onglet in self.synthese: df = self.synthese[onglet] - df.to_excel(writer, onglet, index=True, header=True) # écriture dans l'onglet + df.to_excel( + writer, onglet, index=True, header=True + ) # écriture dans l'onglet # worksheet = writer.sheets[onglet] # l'on - self.zipfile.write(filename) + self.add_file_to_zip( + filename, + open(filename, "rb").read(), + ) """Fin !!!! Tada :)""" @@ -208,7 +209,7 @@ class JuryPE(object): data: Les données du fichier path: Un dossier dans l'arborescence du zip """ - path_in_zip = os.path.join(self.nom_export_zip, path, filename) + path_in_zip = os.path.join(path, filename) # self.nom_export_zip, self.zipfile.writestr(path_in_zip, data) def get_zipped_data(self): @@ -231,7 +232,6 @@ class JuryPE(object): tags = sorted(set(tags)) return tags - # **************************************************************************************************************** # # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** @@ -251,7 +251,6 @@ class JuryPE(object): synthese[tag] = self.df_tag(tag) return synthese - def df_administratif(self): """Synthétise toutes les données administratives des étudiants""" @@ -270,10 +269,10 @@ class JuryPE(object): "Nom": etudiant.nom, "Prenom": etudiant.prenom, "Civilite": etudiant.civilite_str, - "Age": pe_comp.calcul_age(etudiant.date_naissance), + "Age": pe_comp.calcul_age(etudiant.date_naissance), "Date d'entree": cursus["entree"], "Date de diplome": cursus["diplome"], - "Nbre de semestres": len(formsemestres) + "Nbre de semestres": len(formsemestres), } # Ajout des noms de semestres parcourus @@ -281,9 +280,11 @@ class JuryPE(object): administratif[etudid] |= etapes """Construction du dataframe""" - df = pd.DataFrame.from_dict(administratif, orient='index') - return df + df = pd.DataFrame.from_dict(administratif, orient="index") + """Tri par nom/prénom""" + df.sort_values(by=["Nom", "Prenom"], inplace = True) + return df def df_tag(self, tag): """Génère le DataFrame synthétisant les moyennes/classements (groupe, @@ -299,7 +300,6 @@ class JuryPE(object): etudids = list(self.diplomes_ids) aggregats = pe_comp.TOUS_LES_PARCOURS - donnees = {} for etudid in etudids: @@ -315,18 +315,21 @@ class JuryPE(object): trajectoire = self.trajectoires.suivi[etudid][aggregat] """Les moyennes par tag de cette trajectoire""" if trajectoire: - trajectoire_tagguee = self.trajectoires_tagguees[trajectoire.trajectoire_id] + trajectoire_tagguee = self.trajectoires_tagguees[ + trajectoire.trajectoire_id + ] bilan = trajectoire_tagguee.moyennes_tags[tag] donnees[etudid] |= { f"{aggregat} notes ": f"{bilan['notes'].loc[etudid]:.1f}", f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}", - f"{aggregat} min/moy/max (groupe)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}"} + f"{aggregat} min/moy/max (groupe)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}", + } else: donnees[etudid] |= { f"{aggregat} notes ": "-", f"{aggregat} class. (groupe)": "-", - f"{aggregat} min/moy/max (groupe)": "-" + f"{aggregat} min/moy/max (groupe)": "-", } """L'interclassement""" @@ -335,29 +338,29 @@ class JuryPE(object): bilan = interclass.moyennes_tags[tag] donnees[etudid] |= { - f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}", - f"{aggregat} min/moy/max (promo)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}" + f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}", + f"{aggregat} min/moy/max (promo)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}", } else: donnees[etudid] |= { f"{aggregat} class. (promo)": "-", - f"{aggregat} min/moy/max (promo)": "-" + f"{aggregat} min/moy/max (promo)": "-", } # Fin de l'aggrégat + """Construction du dataFrame""" + df = pd.DataFrame.from_dict(donnees, orient="index") - df = pd.DataFrame.from_dict(donnees, orient='index') + """Tri par nom/prénom""" + df.sort_values(by=["Nom", "Prenom"], inplace = True) return df - - def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury """Table(s) du jury mode: singlesheet ou multiplesheet pour export excel """ sT = SeqGenTable() # le fichier excel à générer - if mode == "singlesheet": return sT.get_genTable("singlesheet") else: diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py index de9449c2..71b665dc 100644 --- a/app/pe/pe_trajectoiretag.py +++ b/app/pe/pe_trajectoiretag.py @@ -93,9 +93,6 @@ class TrajectoireTag(TableTag): self.etuds = nt.etuds # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} - self.cursus = { - etudid: donnees_etudiants.cursus[etudid] for etudid in self.etudiants - } """Les tags extraits de tous les semestres""" self.tags_sorted = self.do_taglist() @@ -104,7 +101,7 @@ class TrajectoireTag(TableTag): self.notes_cube = self.compute_notes_cube() """Calcul les moyennes par tag sous forme d'un dataframe""" - etudids = self.get_etudids() + etudids = list(self.etudiants.keys()) self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) """Synthétise les moyennes/classements par tag""" @@ -166,8 +163,7 @@ class TrajectoireTag(TableTag): return etudids_x_tags_x_semestres - def get_etudids(self): - return list(self.etudiants.keys()) + def do_taglist(self): """Synthétise les tags à partir des semestres (taggués) aggrégés diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 779e049d..e464ab34 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -45,8 +45,8 @@ from app.scodoc import sco_formsemestre from app.scodoc import html_sco_header from app.scodoc import sco_preferences -from app.pe import pe_tools -from app.pe import pe_jurype +from app.pe import pe_comp +from app.pe import pe_jury from app.pe import pe_avislatex @@ -75,7 +75,7 @@ def _pe_view_sem_recap_form(formsemestre_id): return "\n".join(H) + html_sco_header.sco_footer() # L'année du diplome - diplome = pe_tools.get_annee_diplome_semestre(sem_base) + diplome = pe_comp.get_annee_diplome_semestre(sem_base) H = [ html_sco_header.sco_header(page_title="Avis de poursuite d'études"), @@ -136,12 +136,12 @@ def pe_view_sem_recap( ) # L'année du diplome - diplome = pe_tools.get_annee_diplome_semestre(sem_base) + diplome = pe_comp.get_annee_diplome_semestre(sem_base) - jury = pe_jurype.JuryPE(diplome, sem_base.formation.formation_id) + jury = pe_jury.JuryPE(diplome, sem_base.formation.formation_id) # Ajout avis LaTeX au même zip: - etudids = list(jury.syntheseJury.keys()) + # etudids = list(jury.syntheseJury.keys()) # Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE # (chaines unicodes, html non quoté) @@ -187,11 +187,11 @@ def pe_view_sem_recap( ) # Ajout des annotations PE dans un fichier excel - sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe) - if sT: - jury.add_file_to_zip( - jury.nom_export_zip + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel() - ) + # sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe) + # if sT: + # jury.add_file_to_zip( + # jury.nom_export_zip + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel() + # ) if False: latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex From 776b0fb228dab3771a474c41160e6508db0f091e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Thu, 25 Jan 2024 19:54:39 +0100 Subject: [PATCH 22/23] =?UTF-8?q?Mise=20=C3=A0=200=20de=20pe=5Fcomp.PE=5FD?= =?UTF-8?q?EBUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_comp.py | 2 +- app/pe/pe_etudiant.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index e57dfa4d..0b9000c9 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -50,7 +50,7 @@ from app.models import FormSemestre from app.scodoc import sco_formsemestre from app.scodoc.sco_logos import find_logo -PE_DEBUG = 1 +PE_DEBUG = 0 if not PE_DEBUG: # log to notes.log diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index ea043f3d..384765f3 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -110,7 +110,7 @@ class EtudiantsJuryPE: self.structure_cursus_etudiant(etudid) if (no_etud + 1) % 10 == 0: - pe_comp.pe_print((no_etud + 1), " ", end="") + pe_comp.pe_print(f"{no_etud + 1}") no_etud += 1 pe_comp.pe_print() From efd735542ebf6a73b571d8bb53fc0f16bba3c1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Thu, 25 Jan 2024 20:04:42 +0100 Subject: [PATCH 23/23] Coquille --- app/pe/pe_jury.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 8e8f03fa..62129ebb 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -367,7 +367,7 @@ class JuryPE(object): return sT -def compute_semestres_tag(etudiants: EtudiantsJuryPE): +def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire des étudiants (cf. attribut etudiants.cursus).