forked from ScoDoc/ScoDoc
650 lines
27 KiB
Python
650 lines
27 KiB
Python
# -*- 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 pandas as pd
|
|
|
|
from app.models import FormSemestre, Identite
|
|
from app.pe import pe_comp, pe_affichage
|
|
|
|
|
|
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 = {}
|
|
"""Les trajectoires/chemins de semestres suivis par les étudiants
|
|
pour atteindre un aggrégat donné
|
|
(par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)"""
|
|
|
|
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, 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``. XXX TODO voir si on garde formation_id qui n'est pas utilisé ici
|
|
|
|
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
|
|
|
|
|
|
formation_id: L'identifiant de la formation (inutilisé)
|
|
|
|
*Remarque* : ex: JuryPE.get_etudiants_in_jury()
|
|
"""
|
|
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)
|
|
self.cosemestres = cosemestres
|
|
|
|
pe_affichage.pe_print(
|
|
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés"
|
|
)
|
|
|
|
pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres")
|
|
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
|
|
pe_affichage.pe_print(
|
|
f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres"
|
|
)
|
|
|
|
# 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")
|
|
|
|
for etudid in self.etudiants_ids:
|
|
self.identites[etudid] = Identite.get_etud(etudid)
|
|
|
|
# Analyse son cursus
|
|
self.analyse_etat_etudiant(etudid, cosemestres)
|
|
|
|
# Analyse son parcours pour atteindre chaque semestre de la formation
|
|
self.structure_cursus_etudiant(etudid)
|
|
|
|
# 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())
|
|
|
|
self.formsemestres_jury_ids = self.get_formsemestres()
|
|
"""Les formsemestres (des étudiants) dont il faut calculer les moyennes"""
|
|
|
|
# Les abandons (pour debug)
|
|
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
|
|
self.abandons_ids = set(self.abandons)
|
|
|
|
# Synthèse
|
|
pe_affichage.pe_print(
|
|
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
|
|
)
|
|
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 non considérés (redoublement, réorientation, abandon"
|
|
)
|
|
pe_affichage.pe_print(
|
|
f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne"
|
|
)
|
|
# pe_affichage.pe_print(
|
|
# " => quelques étudiants futurs diplômés : "
|
|
# + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
|
|
# )
|
|
# pe_affichage.pe_print(
|
|
# " => semestres dont il faut calculer les moyennes : "
|
|
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
|
|
# )
|
|
|
|
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 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)
|
|
formsemestres = identite.get_formsemestres()
|
|
|
|
semestres_etudiant = {
|
|
formsemestre.formsemestre_id: formsemestre
|
|
for formsemestre in formsemestres
|
|
if formsemestre.formation.is_apc()
|
|
}
|
|
|
|
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
|
|
"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
|
|
}
|
|
|
|
# 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 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é)
|
|
|
|
Args:
|
|
etudid: L'identifiant d'un étudiant
|
|
|
|
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"]
|
|
semestres_significatifs = {}
|
|
for fid in semestres_etudiant:
|
|
semestre = semestres_etudiant[fid]
|
|
if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome:
|
|
semestres_significatifs[fid] = semestre
|
|
return semestres_significatifs
|
|
|
|
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_comp.TOUS_LES_SEMESTRES:
|
|
i = int(nom_sem[1]) # le n° du semestre
|
|
# 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][nom_sem] = semestres_i
|
|
|
|
def get_trajectoire(
|
|
self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str
|
|
):
|
|
"""Ensemble des semestres parcourus par
|
|
un étudiant pour l'amener à un semestre terminal.
|
|
|
|
Si nom_aggregat est de type "Si", limite les semestres à ceux de numéro i.
|
|
Par ex: si formsemestre_terminal est un S3 et nom_agrregat "S3", ne prend en compte que les
|
|
semestres 3.
|
|
|
|
Si nom_aggregat est de type "iA" ou "iS" (incluant plusieurs numéros de semestres), prend en
|
|
compte les dit numéros de semestres.
|
|
|
|
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é).
|
|
|
|
Les semestres parcourus sont antérieurs (en terme de date de fin)
|
|
au formsemestre_terminal.
|
|
|
|
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)
|
|
|
|
if nom_aggregat.startswith("S"): # les semestres
|
|
numero_semestres_possibles = [numero_semestre_terminal]
|
|
elif nom_aggregat.endswith("A"): # les années
|
|
numero_semestres_possibles = [
|
|
int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["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
|
|
|
|
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 trajectoire_aggr in self.trajectoires.values():
|
|
trajectoire = trajectoire_aggr[aggregat]
|
|
if trajectoire:
|
|
# 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_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 (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 :
|
|
|
|
* 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(pe_comp.AGGREGAT_DIPLOMANT)
|
|
return semestres
|
|
if 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(elmt)
|
|
semestres = semestres | semestres_elmt
|
|
return semestres
|
|
if (
|
|
isinstance(semestres_recherches, str)
|
|
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_comp.PARCOURS[semestres_recherches]["aggregat"]
|
|
)
|
|
return semestres
|
|
if (
|
|
isinstance(semestres_recherches, str)
|
|
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
|
|
# dans le jury (diplômé et redoublants non diplômé)
|
|
nom_sem = semestres_recherches
|
|
semestres = {}
|
|
for etudid in self.etudiants_ids:
|
|
if self.cursus[etudid][nom_sem]:
|
|
semestres = semestres | self.cursus[etudid][nom_sem]
|
|
return semestres
|
|
|
|
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids")
|
|
|
|
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"]
|
|
|
|
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),
|
|
"Date entree": cursus["entree"],
|
|
"Date diplome": diplome,
|
|
"Nb 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")
|
|
|
|
# Tri par nom/prénom
|
|
df.sort_values(by=["Nom", "Prenom"], inplace=True)
|
|
return df
|
|
|
|
|
|
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
|
|
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:
|
|
"""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
|
|
"""
|
|
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 corresponde à 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: 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 (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 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
|
|
TODO:: A reprendre si BUT avec semestres décalés
|
|
"""
|
|
|
|
# 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
|
|
|
|
# Son dernier semestre APC en date
|
|
dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc)
|
|
numero_dernier_formsemestre = dernier_formsemestre.semestre_id
|
|
|
|
# Les numéro de semestres possible dans lesquels il pourrait s'incrire
|
|
# 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_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_comp.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 (
|
|
fid != dernier_formsemestre.formsemestre_id
|
|
and sem.semestre_id in numeros_possibles
|
|
and sem.date_debut.year >= dernier_formsemestre.date_debut.year
|
|
):
|
|
# 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_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
|
|
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire
|
|
de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``.
|
|
|
|
Args:
|
|
semestres: Un dictionnaire de semestres
|
|
|
|
Return:
|
|
Le FormSemestre du semestre le plus récent
|
|
"""
|
|
if semestres:
|
|
fid_dernier_semestre = list(semestres.keys())[0]
|
|
dernier_semestre: FormSemestre = semestres[fid_dernier_semestre]
|
|
for fid in semestres:
|
|
if semestres[fid].date_fin > dernier_semestre.date_fin:
|
|
dernier_semestre = semestres[fid]
|
|
return dernier_semestre
|
|
return None
|