############################################################################## # Module "Avis de poursuite d'étude" # conçu et développé par Cléo Baras (IUT de Grenoble) ############################################################################## """ Created on 01-2024 @author: barasc """ import app.pe.pe_comp as pe_comp from app.models import FormSemestre from app.pe import pe_sxtag, pe_affichage from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date TYPES_RCS = { "S1": { "aggregat": ["S1"], "descr": "Semestre 1 (S1)", }, "S2": { "aggregat": ["S2"], "descr": "Semestre 2 (S2)", }, "1A": { "aggregat": ["S1", "S2"], "descr": "BUT1 (S1+S2)", }, "S3": { "aggregat": ["S3"], "descr": "Semestre 3 (S3)", }, "S4": { "aggregat": ["S4"], "descr": "Semestre 4 (S4)", }, "2A": { "aggregat": ["S3", "S4"], "descr": "BUT2 (S3+S4)", }, "3S": { "aggregat": ["S1", "S2", "S3"], "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", }, "4S": { "aggregat": ["S1", "S2", "S3", "S4"], "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", }, "S5": { "aggregat": ["S5"], "descr": "Semestre 5 (S5)", }, "S6": { "aggregat": ["S6"], "descr": "Semestre 6 (S6)", }, "3A": { "aggregat": ["S5", "S6"], "descr": "3ème année (S5+S6)", }, "5S": { "aggregat": ["S1", "S2", "S3", "S4", "S5"], "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", }, "6S": { "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", }, } """Dictionnaire détaillant les différents regroupements cohérents de semestres (RCS), en leur attribuant un nom et en détaillant le nom des semestres qu'ils regroupent et l'affichage qui en sera fait dans les tableurs de synthèse. """ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] TOUS_LES_RCS = list(TYPES_RCS.keys()) TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] def get_descr_rcs(nom_rcs: str) -> str: """Renvoie la description pour les tableurs de synthèse Excel d'un nom de RCS""" return TYPES_RCS[nom_rcs]["descr"] class RCS: """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" def __init__(self, nom_rcs: str, semestre_final: FormSemestre): self.nom: str = nom_rcs """Nom du RCS""" self.formsemestre_final: FormSemestre = semestre_final """FormSemestre terminal du RCS""" self.rang_final = self.formsemestre_final.semestre_id """Le rang du formsemestre final""" self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" def get_formsemestre_id_final(self) -> int: """Renvoie l'identifiant du formsemestre final du RCS Returns: L'id du formsemestre final (marquant la fin) du RCS """ return self.formsemestre_final.formsemestre_id def __repr__(self): """Représentation textuelle d'un RCS""" return f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" def get_repr(self): return self.__repr__() def __eq(self, other): """Egalité de RCS""" return ( self.nom == other.nom and self.formsemestre_final == other.formsemestre_final ) class RCF(RCS): """Modélise un ensemble de (form)semestres d'étudiants associé à un type de regroupement cohérent de semestres donné (par ex: 'S2', '3S', '2A'). Si le RCF est un semestre de type Si, stocke les formsemestres de numéro i qu'ont suivi 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 le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie les semestres que les étudiants ont suivis pour les amener jusqu'au semestre terminal du RCS (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, ... Args: nom_rcs: Un nom du RCS (par ex: '5S') semestre_final: Le formsemestre final du RCS """ def __init__(self, nom_rcs: str, semestre_final: FormSemestre): RCS.__init__(self, nom_rcs, semestre_final) self.semestres_aggreges: dict[int:FormSemestre] = {} """Formsemestres regroupés dans le RCS""" def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): """Ajout de semestres aux semestres à regrouper Args: semestres: Dictionnaire ``{fid: Formsemestre)`` """ self.semestres_aggreges = self.semestres_aggreges | semestres def get_repr(self, verbose=True) -> str: """Représentation textuelle d'un RCS basé 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) title = f"""{str(self)}""" if verbose and noms: title += " - " + "+".join(noms) return title def get_rcf_from_semestres_aggreges( rcfs: dict[(str, int):RCF], semestres_a_aggreges: list[FormSemestre] ) -> (str, int): """Partant d'un dictionnaire de RCFs (de la forme ``{ (nom_rcs, fid): RCF }, et connaissant une liste de (form)semestres à aggréger, renvoie l'identifiant (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont les semestres_aggregés par le RCF sont les même que les semestres_a_aggreger. Returns: rcf_id: L'identifiant du RCF trouvé """ fids_semestres_a_aggreger = set( [frms.formsemestre_id for frms in semestres_a_aggreges] ) for rcf_id, rcf in rcfs.items(): fids_rcf = set(rcf.semestres_aggreges) if fids_rcf == fids_semestres_a_aggreger: return rcf_id return None class RCRCF: """Modélise les RCF d'étudiants suivis par un étudiant dans le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. Il est identifié par le formsemestre de S3 marquant la fin du regroupement. Args: nom_rcs: Un nom du RCS (par ex: '5S') semestre_final: Le semestre final du RCS """ def __init__(self, nom_rcs: str, semestre_final: FormSemestre): RCS.__init__(self, nom_rcs, semestre_final) self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} """Les RCFs à aggréger""" def add_rcfs_a_aggreger(self, rcfs: dict[(str, int):RCF]): """Ajout des RCFs aux RCFS à regrouper Args: rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter """ self.rcfs_aggreges = self.rcfs_aggreges | rcfs def get_repr(self, verbose=True) -> str: """Représentation textuelle d'un RCSF basé sur ses RCF aggrégés""" noms = [] for rcf_id, rcf in self.rcfs_aggreges.items(): noms.append(rcf.get_repr()) title = f"""{str(self)}""" if verbose and noms: title += " : " + "+".join(noms) return title class RCSsJuryPE: """Classe centralisant tous les regroupements cohérents de semestres (RCS) des étudiants à prendre en compte dans un jury PE Args: annee_diplome: L'année de diplomation """ def __init__(self, annee_diplome: int): self.annee_diplome = annee_diplome """Année de diplômation""" self.rcss: dict[tuple(int, str) : RCF] = {} """Ensemble des RCS recensés""" self.rcss_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCS}}""" self.rcfs: dict[tuple(int, str) : RCF] = {} """Ensemble des RCF recensés : {(nom_RCS, fid_terminal): RCF}""" self.rcfs_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCF}}""" self.rcrcfs: dict[tuple(int, str) : RCRCF] = {} """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCRCF}""" self.rcrcfs_suivis: dict[int:str] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCRCF : {etudid: {nom_RCS: RCSx}}""" def cree_rcss(self, etudiants: EtudiantsJuryPE): """Créé tous les RCS, au regard du cursus des étudiants analysés + les mémorise dans les données de l'étudiant Args: etudiants: Les étudiants à prendre en compte dans le Jury PE """ for etudid in etudiants.trajectoires: self.rcss_suivis[etudid] = { aggregat: None for aggregat in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM } for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: # 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 = TYPES_RCS[nom_rcs]["aggregat"] nom_semestre_terminal = noms_semestre_de_aggregat[-1] for etudid in etudiants.trajectoires: # Le formsemestre terminal (dernier en date) associé au # semestre marquant la fin de l'aggrégat # (par ex: son dernier S3 en date) trajectoire = etudiants.trajectoires[etudid][nom_semestre_terminal] if trajectoire: formsemestre_final = get_dernier_semestre_en_date(trajectoire) # Ajout ou récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) if rcs_id not in self.rcss: self.rcss[rcs_id] = RCF(nom_rcs, formsemestre_final) rcs = self.rcss[rcs_id] # La liste des semestres de l'étudiant à prendre en compte # pour cette trajectoire semestres_a_aggreger = get_rcs_etudiant( etudiants.trajectoires[etudid], formsemestre_final, nom_rcs ) # Ajout des semestres au RCS rcs.add_semestres_a_aggreger(semestres_a_aggreger) # Mémorise le RCS suivi par l'étudiant self.rcss_suivis[etudid][nom_rcs] = rcs # Affichage pour debug jeunes = list(enumerate(self.rcss_suivis)) for no_etud, etudid in jeunes[:20]: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") for nom_rcs, rcs in self.rcss_suivis[etudid].items(): if rcs: pe_affichage.pe_print(f" > RCS {nom_rcs}: {rcs.get_repr()}") def cree_rcfs(self, etudiants: EtudiantsJuryPE): """Créé les RCFs en ne conservant dans les RCS que les regroupements de type Sx""" self.rcfs = {} for rcs_id, rcs in self.rcss.items(): if rcs and rcs.nom in pe_comp.TOUS_LES_SEMESTRES: self.rcfs[rcs_id] = rcs print(self.rcfs) for etudid in self.rcss_suivis: for nom_rcs, rcs in self.rcss_suivis[etudid].items(): if rcs and nom_rcs in pe_comp.TOUS_LES_SEMESTRES: if etudid not in self.rcfs_suivis: self.rcfs_suivis[etudid] = {} self.rcfs_suivis[etudid][nom_rcs] = rcs # Affichage pour debug jeunes = list(enumerate(self.rcfs_suivis)) for no_etud, etudid in jeunes[:20]: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") for nom_rcs, rcs in self.rcfs_suivis[etudid].items(): if rcs: pe_affichage.pe_print(f" > RCF {nom_rcs}: {rcs.get_repr()}") else: pe_affichage.pe_print(f" > RCF {nom_rcs}: !!! ") def cree_rcrcfs(self, etudiants: EtudiantsJuryPE): """Créé tous les RCRCF, au regard du cursus des étudiants analysés (trajectoires traduisant son parcours dans les différents semestres) + les mémorise dans les données de l'étudiant """ # Pour tous les étudiants du jury for etudid in self.rcss_suivis: self.rcrcfs_suivis[etudid] = {} for rcf_id, rcf in self.rcfs_suivis[etudid].items(): # Pour chaque RCS semestres_a_aggreger = rcf.semestres_aggreges # Tri des semestres par rang semestres_tries = pe_comp.tri_semestres_par_rang(semestres_a_aggreger) # Récupére les RCFs de type Sx traduisant sa trajectoire rcfs_a_aggreger = {} for semestres_du_rang in semestres_tries.values(): if semestres_du_rang: rcf_id = get_rcf_from_semestres_aggreges( self.rcfs, semestres_du_rang ) if rcf_id: raise ValueError( "Il manque un RCF pour créer les RCRCFs dans cree_rcrcfs" ) rcfs_a_aggreger[rcf_id] = self.rcfs[rcf_id] # Ajout du RCRCF if rcf_id not in self.rcrcfs: self.rcrfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) rcrcf = self.rcrcfs[rcf_id] # Ajout des RCFs au RCRCF rcrcf.add_rcfs_a_aggreger(rcfs_a_aggreger) # Mémoire la trajectoire RCRCF suivie par l'étudiant nom_rcs = rcrcf.nom self.rcrcfs_suivis[etudid][nom_rcs] = rcrcf def get_rcs_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str ) -> dict[int, FormSemestre]: """Ensemble des semestres parcourus (trajectoire) par un étudiant dans le cadre d'un RCS de type Sx, iA ou iS et ayant pour semestre terminal `formsemestre_final`. Par ex: pour un RCS "3S", dont le formsemestre_terminal est un S3, regroupe le ou les S1 qu'il a suivi (1 ou 2 si redoublement) + le ou les S2 + le ou les S3. Les semestres parcourus sont antérieurs (en terme de date de fin) au formsemestre_terminal. Args: cursus: Dictionnaire {fid: Formsemestre} donnant l'ensemble des semestres dans lesquels l'étudiant a été inscrit formsemestre_final: le semestre final visé nom_rcs: Nom du RCS visé """ numero_semestre_terminal = formsemestre_final.semestre_id # semestres_significatifs = self.get_semestres_significatifs(etudid) semestres_significatifs = {} for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): semestres_significatifs = semestres_significatifs | semestres[f"S{i}"] if nom_rcs.startswith("S"): # les semestres numero_semestres_possibles = [numero_semestre_terminal] elif nom_rcs.endswith("A"): # les années numero_semestres_possibles = [ int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"] ] assert numero_semestre_terminal in numero_semestres_possibles else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal) numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1)) semestres_aggreges = {} for fid, semestre in semestres_significatifs.items(): # Semestres parmi ceux de n° possibles & qui lui sont antérieurs if ( semestre.semestre_id in numero_semestres_possibles and semestre.date_fin <= formsemestre_final.date_fin ): semestres_aggreges[fid] = semestre return semestres_aggreges