# -*- 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 traités pour le jury" self.identites = {} # ex. ETUDINFO_DICT "Les cursus (semestres suivis, abandons, ...) des étudiants" self.cursus = {} "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 (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, 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 (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) 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) pe_tools.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") no_etud = 0 for no_etud, etudid in enumerate(self.etudiants_ids): """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_etudiants(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]]) ) 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_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é. Args: annee_diplome: Année de diplomation visée pour le jury Returns: Un dictionnaire `{etudid: Identite(etudid)}` """ 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 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é) * 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) 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) 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 """ identite = Identite.get_etud(etudid) """Le cursus global de l'étudiant (restreint aux semestres APC)""" semestres_etudiant = { 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 "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) 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]) # 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 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 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]) """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 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 dictionnaire de la forme {fid: FormSemestre(fid)} Remarque: 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) 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]["aggregats"][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: """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 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 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)}. La date prise en compte est celle marquant la **fin** des semestres. 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 {}