654 lines
26 KiB
Python
654 lines
26 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. c 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 pandas as pd
|
|
|
|
from app import ScoValueError
|
|
from app.models import FormSemestre, Identite, Formation
|
|
from app.pe import pe_comp, pe_affichage
|
|
from app.pe.rcss import pe_rcs
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_utils as scu
|
|
from app.comp.res_sem import load_formsemestre_results
|
|
|
|
|
|
class EtudiantsJuryPE:
|
|
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
|
|
|
|
def __init__(self, annee_diplome: int):
|
|
"""
|
|
Args:
|
|
annee_diplome: L'année de diplomation
|
|
"""
|
|
self.annee_diplome = annee_diplome
|
|
"""L'année du diplôme"""
|
|
|
|
self.identites: dict[int:Identite] = {} # ex. ETUDINFO_DICT
|
|
"""Les identités des étudiants traités pour le jury"""
|
|
|
|
self.cursus: dict[int:dict] = {}
|
|
"""Les cursus (semestres suivis, abandons) des étudiants"""
|
|
|
|
self.trajectoires: dict[int:dict] = {}
|
|
"""Les trajectoires (regroupement cohérents de semestres) suivis par les étudiants"""
|
|
|
|
self.semXs: dict[int:dict] = {}
|
|
"""Les semXs (RCS de type Sx) suivis par chaque étudiant"""
|
|
|
|
self.rcsemXs: dict[int:dict] = {}
|
|
"""Les RC de SemXs (RCS de type Sx, xA, xS) suivis par chaque étudiant"""
|
|
|
|
self.etudiants_diplomes = {}
|
|
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement
|
|
diplômés)"""
|
|
|
|
self.diplomes_ids = {}
|
|
"""Les etudids des étudiants diplômés"""
|
|
|
|
self.etudiants_ids = {}
|
|
"""Les etudids des étudiants dont il faut calculer les moyennes/classements
|
|
(même si d'éventuels abandons).
|
|
Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi
|
|
d'autres ayant été réorientés ou ayant abandonnés)"""
|
|
|
|
self.cosemestres: dict[int, FormSemestre] = None
|
|
"Les cosemestres donnant lieu à même année de diplome"
|
|
|
|
self.abandons = {}
|
|
"""Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)"""
|
|
self.abandons_ids = {}
|
|
"""Les etudids des étudiants redoublants/réorientés"""
|
|
|
|
def find_etudiants(self):
|
|
"""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``.
|
|
|
|
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
|
|
|
|
*Remarque* : ex: JuryPE.get_etudiants_in_jury()
|
|
"""
|
|
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome)
|
|
self.cosemestres = cosemestres
|
|
|
|
pe_affichage.pe_print(
|
|
f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés", info=True
|
|
)
|
|
|
|
pe_affichage.pe_print(
|
|
"2) Liste des étudiants dans les différents cosemestres", info=True
|
|
)
|
|
etudiants_ids = get_etudiants_dans_semestres(cosemestres)
|
|
pe_affichage.pe_print(
|
|
f" => {len(etudiants_ids)} étudiants trouvés dans les cosemestres",
|
|
info=True,
|
|
)
|
|
|
|
# Analyse des parcours étudiants pour déterminer leur année effective de diplome
|
|
# avec prise en compte des redoublements, des abandons, ....
|
|
pe_affichage.pe_print(
|
|
"3) Analyse des parcours individuels des étudiants", info=True
|
|
)
|
|
|
|
# Ajoute une liste d'étudiants
|
|
self.add_etudiants(etudiants_ids)
|
|
|
|
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
|
|
self.etudiants_diplomes = self.get_etudiants_diplomes()
|
|
self.diplomes_ids = set(self.etudiants_diplomes.keys())
|
|
self.etudiants_ids = set(self.identites.keys())
|
|
|
|
# Les abandons (pour debug)
|
|
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
|
|
# Les identités des étudiants ayant redoublés ou ayant abandonnés
|
|
|
|
self.abandons_ids = set(self.abandons)
|
|
# Les identifiants des étudiants ayant redoublés ou ayant abandonnés
|
|
|
|
# Synthèse
|
|
pe_affichage.pe_print(f"4) Bilan", info=True)
|
|
pe_affichage.pe_print(
|
|
f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}",
|
|
info=True,
|
|
)
|
|
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
|
|
assert nbre_abandons == len(self.abandons_ids)
|
|
|
|
pe_affichage.pe_print(
|
|
f"--> {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)"
|
|
)
|
|
|
|
def add_etudiants(self, etudiants_ids):
|
|
"""Ajoute une liste d'étudiants aux données du jury"""
|
|
nbre_etudiants_ajoutes = 0
|
|
for etudid in etudiants_ids:
|
|
if etudid not in self.identites:
|
|
nbre_etudiants_ajoutes += 1
|
|
|
|
# L'identité de l'étudiant
|
|
self.identites[etudid] = Identite.get_etud(etudid)
|
|
|
|
# Analyse son cursus
|
|
self.analyse_etat_etudiant(etudid, self.cosemestres)
|
|
|
|
# Analyse son parcours pour atteindre chaque semestre de la formation
|
|
self.structure_cursus_etudiant(etudid)
|
|
self.etudiants_ids = set(self.identites.keys())
|
|
return nbre_etudiants_ajoutes
|
|
|
|
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é.
|
|
|
|
|
|
Returns:
|
|
Un dictionnaire `{etudid: Identite(etudid)}`
|
|
"""
|
|
etudids = [
|
|
etudid
|
|
for etudid, cursus_etud in self.cursus.items()
|
|
if cursus_etud["diplome"] == self.annee_diplome
|
|
and cursus_etud["abandon"] is False
|
|
]
|
|
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
|
|
return etudiants
|
|
|
|
def get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]:
|
|
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
|
|
dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront
|
|
pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire.
|
|
|
|
Returns:
|
|
Un dictionnaire `{etudid: Identite(etudid)}`
|
|
"""
|
|
etudids = [
|
|
etudid
|
|
for etudid, cursus_etud in self.cursus.items()
|
|
if cursus_etud["diplome"] != self.annee_diplome
|
|
or cursus_etud["abandon"] is True
|
|
]
|
|
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 a démissionné, redoublé (autre année de diplôme)
|
|
ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré
|
|
en abandon si connaissant son dernier semestre (par ex. un S3) il n'est pas systématiquement
|
|
inscrit à l'un des S4, S5 ou S6 existants dans les cosemestres.
|
|
|
|
|
|
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)
|
|
formsemestres = identite.get_formsemestres()
|
|
|
|
semestres_etudiant = {
|
|
formsemestre.formsemestre_id: formsemestre
|
|
for formsemestre in formsemestres
|
|
if formsemestre.formation.is_apc()
|
|
}
|
|
|
|
# Le parcours final
|
|
parcour = formsemestres[0].etuds_inscriptions[etudid].parcour
|
|
if parcour:
|
|
libelle = parcour.libelle
|
|
else:
|
|
libelle = None
|
|
|
|
self.cursus[etudid] = {
|
|
"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
|
|
"parcours": libelle, # Le parcours final
|
|
"diplome": get_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
|
|
}
|
|
|
|
# Si l'étudiant est succeptible d'être diplomé
|
|
if self.cursus[etudid]["diplome"] == self.annee_diplome:
|
|
# Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
|
|
dernier_semes_etudiant = formsemestres[0]
|
|
res = load_formsemestre_results(dernier_semes_etudiant)
|
|
etud_etat = res.get_etud_etat(etudid)
|
|
if etud_etat == scu.DEMISSION:
|
|
self.cursus[etudid]["abandon"] = True
|
|
else:
|
|
# Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ?
|
|
self.cursus[etudid]["abandon"] = arret_de_formation(
|
|
identite, cosemestres
|
|
)
|
|
|
|
# Initialise ses trajectoires/SemX/RCSemX
|
|
self.trajectoires[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
|
|
self.semXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_SEMESTRES}
|
|
self.rcsemXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
|
|
|
|
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 interclassements par semestre dans la promo.
|
|
"""
|
|
semestres_significatifs = get_semestres_significatifs(
|
|
self.cursus[etudid]["formsemestres"], self.annee_diplome
|
|
)
|
|
|
|
# Tri des semestres par numéro de semestre
|
|
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
|
# les semestres de n°i de l'étudiant:
|
|
semestres_i = {
|
|
fid: sem_sig
|
|
for fid, sem_sig in semestres_significatifs.items()
|
|
if sem_sig.semestre_id == i
|
|
}
|
|
self.cursus[etudid][f"S{i}"] = semestres_i
|
|
|
|
def get_formsemestres_finals_des_rcs(self, nom_rcs: str) -> dict[int, FormSemestre]:
|
|
"""Pour un nom de RCS donné, ensemble des formsemestres finals possibles
|
|
pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3.
|
|
Les formsemestres finals obtenus 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:
|
|
nom_rcs: Le nom du RCS (parmi Sx, xA, xS)
|
|
|
|
Returns:
|
|
Un dictionnaire ``{fid: FormSemestre(fid)}``
|
|
"""
|
|
formsemestres_terminaux = {}
|
|
for trajectoire_aggr in self.cursus.values():
|
|
trajectoire = trajectoire_aggr[nom_rcs]
|
|
if trajectoire:
|
|
# Le semestre terminal de l'étudiant de l'aggrégat
|
|
fid = trajectoire.formsemestre_final.formsemestre_id
|
|
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
|
|
return formsemestres_terminaux
|
|
|
|
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
|
|
"""Partant d'un ensemble d'étudiants,
|
|
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
|
|
|
|
Args:
|
|
etudids: Liste d'étudid d'étudiants
|
|
"""
|
|
nbres_semestres = []
|
|
for etudid in etudids:
|
|
nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
|
|
if not nbres_semestres:
|
|
return 0
|
|
return max(nbres_semestres)
|
|
|
|
def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
|
|
"""Synthétise toutes les données administratives d'un groupe
|
|
d'étudiants fournis par les etudid dans un dataFrame
|
|
|
|
Args:
|
|
etudids: La liste des étudiants à prendre en compte
|
|
"""
|
|
|
|
etudids = list(etudids)
|
|
|
|
# Récupération des données des étudiants
|
|
administratif = {}
|
|
nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids)
|
|
|
|
for etudid in etudids:
|
|
etudiant = self.identites[etudid]
|
|
cursus = self.cursus[etudid]
|
|
formsemestres = cursus["formsemestres"]
|
|
parcours = cursus["parcours"]
|
|
if not parcours:
|
|
parcours = ""
|
|
if cursus["diplome"]:
|
|
diplome = cursus["diplome"]
|
|
else:
|
|
diplome = "indéterminé"
|
|
|
|
administratif[etudid] = {
|
|
"etudid": etudiant.id,
|
|
"INE": etudiant.code_ine or "",
|
|
"NIP": etudiant.code_nip or "",
|
|
"Nom": etudiant.nom,
|
|
"Prenom": etudiant.prenom,
|
|
"Civilite": etudiant.civilite_str,
|
|
"Age": pe_comp.calcul_age(etudiant.date_naissance),
|
|
"Parcours": parcours,
|
|
"Date entree": cursus["entree"],
|
|
"Date diplome": diplome,
|
|
"Nb semestres": len(formsemestres),
|
|
}
|
|
|
|
# Ajout des noms de semestres parcourus
|
|
etapes = etapes_du_cursus(formsemestres, nbre_semestres_max)
|
|
administratif[etudid] |= etapes
|
|
|
|
# Construction du dataframe
|
|
df = pd.DataFrame.from_dict(administratif, orient="index")
|
|
|
|
# Tri par nom/prénom
|
|
df.sort_values(by=["Nom", "Prenom"], inplace=True)
|
|
return df
|
|
|
|
|
|
def get_semestres_significatifs(formsemestres, annee_diplome):
|
|
"""Partant d'un ensemble de semestre, renvoie les semestres qui amèneraient les étudiants
|
|
à être diplômé à l'année visée, y compris s'ils n'avaient pas redoublé et seraient donc
|
|
diplômé plus tard.
|
|
|
|
De fait, supprime les semestres qui conduisent à une diplomation postérieure
|
|
à celle visée.
|
|
|
|
Args:
|
|
formsemestres: une liste de formsemestres
|
|
annee_diplome: l'année du diplôme visée
|
|
|
|
Returns:
|
|
Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres
|
|
amènent à une diplômation antérieur à celle de la diplômation visée par le jury
|
|
"""
|
|
# semestres_etudiant = self.cursus[etudid]["formsemestres"]
|
|
semestres_significatifs = {}
|
|
for fid in formsemestres:
|
|
semestre = formsemestres[fid]
|
|
if pe_comp.get_annee_diplome_semestre(semestre) <= annee_diplome:
|
|
semestres_significatifs[fid] = semestre
|
|
return semestres_significatifs
|
|
|
|
|
|
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``.
|
|
|
|
Args:
|
|
semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un
|
|
ensemble d'identifiant de semestres
|
|
|
|
Returns:
|
|
Un ensemble d``etudid``
|
|
"""
|
|
|
|
etudiants_ids = set()
|
|
for sem in semestres.values(): # pour chacun des semestres de la liste
|
|
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
|
|
|
|
pe_affichage.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 get_annee_diplome(etud: Identite) -> int | None:
|
|
"""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, ou None si aucun semestre
|
|
"""
|
|
formsemestres_apc = get_semestres_apc(etud)
|
|
|
|
if formsemestres_apc:
|
|
dates_possibles_diplome = []
|
|
# Années de diplômation prédites en fonction des semestres
|
|
# (d'une formation APC) d'un étudiant
|
|
for sem_base in formsemestres_apc:
|
|
annee = pe_comp.get_annee_diplome_semestre(sem_base)
|
|
if annee:
|
|
dates_possibles_diplome.append(annee)
|
|
if dates_possibles_diplome:
|
|
return max(dates_possibles_diplome)
|
|
|
|
return None
|
|
|
|
|
|
def get_semestres_apc(identite: Identite) -> list:
|
|
"""Liste des semestres d'un étudiant qui correspondent à une formation APC.
|
|
|
|
Args:
|
|
identite: L'identité d'un étudiant
|
|
|
|
Returns:
|
|
Liste de ``FormSemestre`` correspondant à une formation APC
|
|
"""
|
|
semestres = identite.get_formsemestres()
|
|
semestres_apc = []
|
|
for sem in semestres:
|
|
if sem.formation.is_apc():
|
|
semestres_apc.append(sem)
|
|
return semestres_apc
|
|
|
|
|
|
def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> bool:
|
|
"""Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir :
|
|
|
|
* 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'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 "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres
|
|
de rang/semestre_id supérieur (et donc de dates) au dernier semestre dans lequel il a été inscrit.
|
|
|
|
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:
|
|
etud: 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 pour le cas des étudiants à l'étranger
|
|
"""
|
|
# Les semestres APC de l'étudiant
|
|
semestres = get_semestres_apc(etud)
|
|
semestres_apc = {sem.semestre_id: sem for sem in semestres}
|
|
if not semestres_apc:
|
|
return True
|
|
|
|
# Le dernier semestre de l'étudiant
|
|
dernier_formsemestre = semestres[0]
|
|
rang_dernier_semestre = dernier_formsemestre.semestre_id
|
|
|
|
# Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
|
|
# sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
|
|
cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres)
|
|
|
|
cosemestres_superieurs = {}
|
|
for rang in cosemestres_tries_par_rang:
|
|
if rang > rang_dernier_semestre:
|
|
cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]
|
|
|
|
# Si pas d'autres cosemestres postérieurs
|
|
if not cosemestres_superieurs:
|
|
return False
|
|
|
|
# Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ?
|
|
etat_inscriptions = {rang: False for rang in cosemestres_superieurs}
|
|
for rang in etat_inscriptions:
|
|
for sem in cosemestres_superieurs[rang]:
|
|
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
|
|
if etud.etudid in etudiants_du_sem:
|
|
etat_inscriptions[rang] = True
|
|
|
|
# Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres
|
|
rangs = sorted(etat_inscriptions.keys())
|
|
if list(rangs) != list(range(min(rangs), max(rangs) + 1)):
|
|
difference = set(range(min(rangs), max(rangs) + 1)) - set(rangs)
|
|
affichage = ",".join([f"S{val}" for val in difference])
|
|
raise ScoValueError(
|
|
f"Il manque le(s) semestre(s) {affichage} au cursus de {etud.etat_civil} ({etud.etudid})."
|
|
)
|
|
|
|
# Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire
|
|
est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs)
|
|
if est_demissionnaire:
|
|
non_inscrit_a = [
|
|
rang for rang in etat_inscriptions if not etat_inscriptions[rang]
|
|
]
|
|
affichage = ", ".join([f"S{val}" for val in non_inscrit_a])
|
|
pe_affichage.pe_print(
|
|
f"--> ⛔ {etud.etat_civil} ({etud.etudid}), non inscrit dans {affichage} amenant à diplômation"
|
|
)
|
|
else:
|
|
pe_affichage.pe_print(f"--> ✅ {etud.etat_civil} ({etud.etudid})")
|
|
|
|
return est_demissionnaire
|
|
|
|
|
|
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
|
|
|
|
|
|
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.formsemestre_id})")
|
|
|
|
return " ".join(description)
|