Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
8 changed files with 255 additions and 211 deletions
Showing only changes of commit f4c1d00046 - Show all commits

View File

@ -1,3 +1,11 @@
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""Affichages, debug
"""
from app import log from app import log
PE_DEBUG = 0 PE_DEBUG = 0
@ -15,5 +23,3 @@ else:
SANS_NOTE = "-" SANS_NOTE = "-"
NOM_STAT_GROUPE = "statistiques du groupe" NOM_STAT_GROUPE = "statistiques du groupe"
NOM_STAT_PROMO = "statistiques de la promo" NOM_STAT_PROMO = "statistiques de la promo"

View File

@ -52,7 +52,6 @@ from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
# Generated LaTeX files are encoded as: # Generated LaTeX files are encoded as:
PE_LATEX_ENCODING = "utf-8" PE_LATEX_ENCODING = "utf-8"
@ -98,7 +97,8 @@ def calcul_age(born: datetime.date) -> int:
return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
def remove_accents(input_unicode_str): # Nota: scu.suppress_accents fait la même chose mais renvoie un str et non un bytes
def remove_accents(input_unicode_str: str) -> bytes:
"""Supprime les accents d'une chaine unicode""" """Supprime les accents d'une chaine unicode"""
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
only_ascii = nfkd_form.encode("ASCII", "ignore") only_ascii = nfkd_form.encode("ASCII", "ignore")
@ -133,15 +133,15 @@ def escape_for_latex(s):
# ---------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------
def list_directory_filenames(path): def list_directory_filenames(path: str) -> list[str]:
"""List of regular filenames in a directory (recursive) """List of regular filenames (paths) in a directory (recursive)
Excludes files and directories begining with . Excludes files and directories begining with .
""" """
R = [] paths = []
for root, dirs, files in os.walk(path, topdown=True): for root, dirs, files in os.walk(path, topdown=True):
dirs[:] = [d for d in dirs if d[0] != "."] dirs[:] = [d for d in dirs if d[0] != "."]
R += [os.path.join(root, fn) for fn in files if fn[0] != "."] paths += [os.path.join(root, fn) for fn in files if fn[0] != "."]
return R return paths
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
@ -195,13 +195,15 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
def get_annee_diplome_semestre( def get_annee_diplome_semestre(
sem_base: FormSemestre | dict, nbre_sem_formation: int = 6 sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
) -> int: ) -> int:
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres) """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT
et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle à 6 semestres) et connaissant le numéro du semestre, ses dates de début et de fin du
sera remis le diplôme BUT des étudiants qui y sont scolarisés semestre, prédit l'année à laquelle sera remis le diplôme BUT des étudiants qui y
(en supposant qu'il n'y ait pas de redoublement à venir). 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) **Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4,
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire. 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 : Par exemple :
@ -235,21 +237,22 @@ def get_annee_diplome_semestre(
if ( if (
1 <= sem_id <= nbre_sem_formation 1 <= sem_id <= nbre_sem_formation
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
nbreSemRestant = ( nb_sem_restants = (
nbre_sem_formation - sem_id nbre_sem_formation - sem_id
) # nombre de semestres restant avant diplome ) # nombre de semestres restant avant diplome
nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome nb_annees_restantes = (
# Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé nb_sem_restants // 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 # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
delta = annee_fin - annee_debut delta = annee_fin - annee_debut
decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 decalage = nb_sem_restants % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
increment = decalage * (1 - delta) increment = decalage * (1 - delta)
return annee_fin + nbreAnRestant + increment return annee_fin + nb_annees_restantes + increment
def get_cosemestres_diplomants( def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
annee_diplome: int
) -> dict[int, FormSemestre]:
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``. """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``.
**Définition** : Un co-semestre est un semestre : **Définition** : Un co-semestre est un semestre :
@ -264,13 +267,13 @@ def get_cosemestres_diplomants(
Returns: Returns:
Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres
""" """
tousLesSems = ( tous_les_sems = (
sco_formsemestre.do_formsemestre_list() sco_formsemestre.do_formsemestre_list()
) # tous les semestres memorisés dans scodoc ) # tous les semestres memorisés dans scodoc
cosemestres_fids = { cosemestres_fids = {
sem["id"] sem["id"]
for sem in tousLesSems for sem in tous_les_sems
if get_annee_diplome_semestre(sem) == annee_diplome if get_annee_diplome_semestre(sem) == annee_diplome
} }
@ -281,5 +284,3 @@ def get_cosemestres_diplomants(
cosemestres[fid] = cosem cosemestres[fid] = cosem
return cosemestres return cosemestres

View File

@ -37,13 +37,13 @@ Created on 17/01/2024
""" """
import pandas as pd import pandas as pd
import app.pe.pe_rcs
from app.models import FormSemestre, Identite, Formation from app.models import FormSemestre, Identite, Formation
from app.pe import pe_comp, pe_affichage from app.pe import pe_comp, pe_affichage
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
class EtudiantsJuryPE: class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
@ -123,20 +123,15 @@ class EtudiantsJuryPE:
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris # Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
self.etudiants_diplomes = self.get_etudiants_diplomes() self.etudiants_diplomes = self.get_etudiants_diplomes()
"""Les identités des étudiants diplômés"""
self.diplomes_ids = set(self.etudiants_diplomes.keys()) self.diplomes_ids = set(self.etudiants_diplomes.keys())
"""Les identifiants des étudiants diplômés"""
self.etudiants_ids = set(self.identites.keys()) self.etudiants_ids = set(self.identites.keys())
"""Les identifiants des étudiants (diplômés, redoublants ou ayant abandonnés) à traiter"""
# Les abandons (pour debug) # Les abandons (pour debug)
self.abandons = self.get_etudiants_redoublants_ou_reorientes() self.abandons = self.get_etudiants_redoublants_ou_reorientes()
"""Les identités des étudiants ayant redoublés ou ayant abandonnés""" # Les identités des étudiants ayant redoublés ou ayant abandonnés
self.abandons_ids = set(self.abandons) self.abandons_ids = set(self.abandons)
"""Les identifiants des étudiants ayant redoublés ou ayant abandonnés""" # Les identifiants des étudiants ayant redoublés ou ayant abandonnés
# Synthèse # Synthèse
pe_affichage.pe_print( pe_affichage.pe_print(
@ -288,18 +283,20 @@ class EtudiantsJuryPE:
} }
self.cursus[etudid][f"S{i}"] = semestres_i self.cursus[etudid][f"S{i}"] = semestres_i
def get_formsemestres_terminaux_aggregat(
self, aggregat: str
def get_formsemestres_terminaux_aggregat(self, aggregat: str): ) -> dict[int, FormSemestre]:
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat """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). (pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3).
Ces formsemestres traduisent : 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 * les différents parcours des étudiants liés par exemple au choix de modalité
ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA. (par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année : formsemestre_id du S3 FI et du S3 UFA.
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du * les éventuelles situations de redoublement (par ex pour 1 étudiant ayant
S3 (1ère session) et du S3 (2ème session) 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: Args:
aggregat: L'aggrégat aggregat: L'aggrégat
@ -316,7 +313,6 @@ class EtudiantsJuryPE:
formsemestres_terminaux[fid] = trajectoire.formsemestre_final formsemestres_terminaux[fid] = trajectoire.formsemestre_final
return formsemestres_terminaux return formsemestres_terminaux
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int: def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
"""Partant d'un ensemble d'étudiants, """Partant d'un ensemble d'étudiants,
nombre de semestres (étapes) maximum suivis par les étudiants du jury. nombre de semestres (étapes) maximum suivis par les étudiants du jury.
@ -407,7 +403,7 @@ def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
return etudiants_ids return etudiants_ids
def get_annee_diplome(etud: Identite) -> int: def get_annee_diplome(etud: Identite) -> int | None:
"""L'année de diplôme prévue d'un étudiant en fonction de ses semestres """L'année de diplôme prévue d'un étudiant en fonction de ses semestres
d'inscription (pour un BUT). d'inscription (pour un BUT).
@ -415,13 +411,14 @@ def get_annee_diplome(etud: Identite) -> int:
identite: L'identité d'un étudiant identite: L'identité d'un étudiant
Returns: Returns:
L'année prévue de sa diplômation L'année prévue de sa diplômation, ou None si aucun semestre
""" """
formsemestres_apc = get_semestres_apc(etud) formsemestres_apc = get_semestres_apc(etud)
if formsemestres_apc: if formsemestres_apc:
dates_possibles_diplome = [] dates_possibles_diplome = []
"""Années de diplômation prédites en fonction des semestres (d'une formation APC) d'un étudiant""" # Années de diplômation prédites en fonction des semestres
# (d'une formation APC) d'un étudiant
for sem_base in formsemestres_apc: for sem_base in formsemestres_apc:
annee = pe_comp.get_annee_diplome_semestre(sem_base) annee = pe_comp.get_annee_diplome_semestre(sem_base)
if annee: if annee:
@ -452,23 +449,26 @@ def get_semestres_apc(identite: Identite) -> list:
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: 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é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 * d'une réorientation à l'initiative du jury de semestre ou d'une démission
utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD, (on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
mais pas nécessaire ici) 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). * 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 Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
(semestres conduisant à la même année de diplômation) connu dans Scodoc. 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 Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
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 l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
arrêté. A moins qu'il ne soit parti à l'étranger et , pas de notes. 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 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 **Attention** : Cela suppose que toutes les instances d'un semestre donné
accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme (par ex: toutes les instances de S6 accueillant un étudiant soient créées ; sinon les
ayant abandonnés) é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 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 Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et

View File

@ -1,16 +1,59 @@
from app.comp import moy_sem ##############################################################################
#
# 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 pandas as pd
import numpy as np
from app.pe.pe_tabletags import TableTag, MoyenneTag from app.pe.pe_tabletags import TableTag, MoyenneTag
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_rcs import RCS, RCSsJuryPE from app.pe.pe_rcs import RCS, RCSsJuryPE
from app.pe.pe_rcstag import RCSTag from app.pe.pe_rcstag import RCSTag
import pandas as pd
import numpy as np
class RCSInterclasseTag(TableTag): class RCSInterclasseTag(TableTag):
# ------------------------------------------------------------------------------------------------------------------- """
Interclasse l'ensemble des étudiants diplômés à une année
donnée (celle du jury), pour un RCS 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
formsemestre)
* calculant le classement sur les étudiants diplômes
"""
def __init__( def __init__(
self, self,
nom_rcs: str, nom_rcs: str,
@ -18,15 +61,6 @@ class RCSInterclasseTag(TableTag):
rcss_jury_pe: RCSsJuryPE, rcss_jury_pe: RCSsJuryPE,
rcss_tags: dict[tuple, RCSTag], rcss_tags: dict[tuple, RCSTag],
): ):
"""
Interclasse l'ensemble des étudiants diplômés à une année
donnée (celle du jury), pour un RCS 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
"""
TableTag.__init__(self) TableTag.__init__(self)
self.nom_rcs = nom_rcs self.nom_rcs = nom_rcs
@ -43,7 +77,8 @@ class RCSInterclasseTag(TableTag):
for etudid in self.diplomes_ids for etudid in self.diplomes_ids
} }
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat # Les trajectoires (et leur version tagguées), en ne gardant que
# celles associées à l'aggrégat
self.rcss: dict[int, RCS] = {} self.rcss: dict[int, RCS] = {}
"""Ensemble des trajectoires associées à l'aggrégat""" """Ensemble des trajectoires associées à l'aggrégat"""
for trajectoire_id in rcss_jury_pe.rcss: for trajectoire_id in rcss_jury_pe.rcss:
@ -54,9 +89,7 @@ class RCSInterclasseTag(TableTag):
self.trajectoires_taggues: dict[int, RCS] = {} self.trajectoires_taggues: dict[int, RCS] = {}
"""Ensemble des trajectoires tagguées associées à l'aggrégat""" """Ensemble des trajectoires tagguées associées à l'aggrégat"""
for trajectoire_id in self.rcss: for trajectoire_id in self.rcss:
self.trajectoires_taggues[trajectoire_id] = rcss_tags[ self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
trajectoire_id
]
# Les trajectoires suivies par les étudiants du jury, en ne gardant que # Les trajectoires suivies par les étudiants du jury, en ne gardant que
# celles associées aux diplomés # celles associées aux diplomés

View File

@ -47,22 +47,18 @@ import os
from zipfile import ZipFile from zipfile import ZipFile
import numpy as np import numpy as np
import pandas as pd
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_etudiant import * from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
from app.pe.pe_rcs import * from app.pe.pe_rcs import * # TODO A éviter
import app.pe.pe_comp as pe_comp from app.pe.pe_rcstag import RCSTag
from app.pe.pe_semtag import SemestreTag from app.pe.pe_semtag import SemestreTag
from app.pe.pe_interclasstag import RCSInterclasseTag from app.pe.pe_interclasstag import RCSInterclasseTag
from app.pe.pe_rcstag import RCSTag
import app.pe.pe_affichage as pe_affichage
import pandas as pd
class JuryPE(object): class JuryPE(object):
def __init__(self, diplome):
""" """
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
d'une année de diplôme. De ce semestre est déduit : d'une année de diplôme. De ce semestre est déduit :
@ -72,6 +68,8 @@ class JuryPE(object):
Args: Args:
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
""" """
def __init__(self, diplome):
self.diplome = diplome self.diplome = diplome
"L'année du diplome" "L'année du diplome"
@ -161,9 +159,7 @@ class JuryPE(object):
self.rcss.cree_rcss(self.etudiants) self.rcss.cree_rcss(self.etudiants)
# Génère les moyennes par tags des trajectoires # Génère les moyennes par tags des trajectoires
pe_affichage.pe_print( pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
"*** Calcule les moyennes par tag des RCS possibles"
)
self.rcss_tags = compute_trajectoires_tag( self.rcss_tags = compute_trajectoires_tag(
self.rcss, self.etudiants, self.sems_tags self.rcss, self.etudiants, self.sems_tags
) )
@ -381,14 +377,13 @@ class JuryPE(object):
champ = (descr, "", "note") champ = (descr, "", "note")
notes_traj = moy_traj.get_notes() notes_traj = moy_traj.get_notes()
donnees.loc[etudids_communs, champ] = notes_traj.loc[ donnees.loc[etudids_communs, champ] = notes_traj.loc[
etudids_communs] etudids_communs
]
# Les rangs # Les rangs
champ = (descr, NOM_STAT_GROUPE, "class.") champ = (descr, NOM_STAT_GROUPE, "class.")
rangs = moy_traj.get_rangs_inscrits() rangs = moy_traj.get_rangs_inscrits()
donnees.loc[etudids_communs, champ] = rangs.loc[ donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs]
etudids_communs
]
# Les mins # Les mins
champ = (descr, NOM_STAT_GROUPE, "min") champ = (descr, NOM_STAT_GROUPE, "min")
@ -487,9 +482,7 @@ class JuryPE(object):
# La trajectoire de l'étudiant sur l'aggrégat # La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.rcss.suivi[etudid][aggregat] trajectoire = self.rcss.suivi[etudid][aggregat]
if trajectoire: if trajectoire:
trajectoire_tagguee = self.rcss_tags[ trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id]
trajectoire.rcs_id
]
if tag in trajectoire_tagguee.moyennes_tags: if tag in trajectoire_tagguee.moyennes_tags:
# L'interclassement # L'interclassement
interclass = self.interclassements_taggues[aggregat] interclass = self.interclassements_taggues[aggregat]
@ -512,6 +505,7 @@ class JuryPE(object):
df.sort_values(by=[("", "", "tag")], inplace=True) df.sort_values(by=[("", "", "tag")], inplace=True)
return df return df
def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
"""Ayant connaissance des étudiants dont il faut calculer les moyennes pour """Ayant connaissance des étudiants dont il faut calculer les moyennes pour
le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres
@ -534,6 +528,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
semestres = semestres | etudiants.cursus[etudid][cle] semestres = semestres | etudiants.cursus[etudid][cle]
return semestres return semestres
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. """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 Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
@ -548,7 +543,7 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
Un dictionnaire {fid: SemestreTag(fid)} Un dictionnaire {fid: SemestreTag(fid)}
""" """
"""Création des semestres taggués, de type 'S1', 'S2', ...""" # Création des semestres taggués, de type 'S1', 'S2', ...
pe_affichage.pe_print("*** Création des semestres taggués") pe_affichage.pe_print("*** Création des semestres taggués")
formsemestres = get_formsemestres_etudiants(etudiants) formsemestres = get_formsemestres_etudiants(etudiants)

View File

@ -1,5 +1,15 @@
##############################################################################
# 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 import app.pe.pe_comp as pe_comp
import app.pe.pe_affichage as pe_affichage
from app.models import FormSemestre from app.models import FormSemestre
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
@ -61,16 +71,16 @@ TYPES_RCS = {
} }
"""Dictionnaire détaillant les différents regroupements cohérents """Dictionnaire détaillant les différents regroupements cohérents
de semestres (RCS), en leur attribuant un nom et en détaillant de semestres (RCS), en leur attribuant un nom et en détaillant
le nom des semestres qu'ils regroupement et l'affichage qui en sera fait le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
dans les tableurs de synthèse""" dans les tableurs de synthèse.
"""
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS.keys() if not cle.startswith("S")] 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_RCS = list(TYPES_RCS.keys())
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS.keys() if cle.startswith("S")] TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
class RCS: class RCS:
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
"""Modélise un ensemble de semestres d'étudiants """Modélise un ensemble de semestres d'étudiants
associé à un type de regroupement cohérent de semestres associé à un type de regroupement cohérent de semestres
donné (par ex: 'S2', '3S', '2A'). donné (par ex: 'S2', '3S', '2A').
@ -92,6 +102,8 @@ class RCS:
nom_rcs: Un nom du RCS (par ex: '5S') nom_rcs: Un nom du RCS (par ex: '5S')
semestre_final: Le semestre final du RCS semestre_final: Le semestre final du RCS
""" """
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
self.nom = nom_rcs self.nom = nom_rcs
"""Nom du RCS""" """Nom du RCS"""
@ -121,14 +133,14 @@ class RCS:
semestre = self.semestres_aggreges[fid] semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}({fid})") noms.append(f"S{semestre.semestre_id}({fid})")
noms = sorted(noms) noms = sorted(noms)
repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" title = f"""{self.nom} ({
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
if verbose and noms: if verbose and noms:
repr += " - " + "+".join(noms) title += " - " + "+".join(noms)
return repr return title
class RCSsJuryPE: class RCSsJuryPE:
def __init__(self, annee_diplome: int):
"""Classe centralisant toutes les regroupements cohérents de """Classe centralisant toutes les regroupements cohérents de
semestres (RCS) des étudiants à prendre en compte dans un jury PE semestres (RCS) des étudiants à prendre en compte dans un jury PE
@ -136,6 +148,7 @@ class RCSsJuryPE:
annee_diplome: L'année de diplomation annee_diplome: L'année de diplomation
""" """
def __init__(self, annee_diplome: int):
self.annee_diplome = annee_diplome self.annee_diplome = annee_diplome
"""Année de diplômation""" """Année de diplômation"""
@ -155,7 +168,8 @@ class RCSsJuryPE:
""" """
for nom_rcs 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)""" # 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"] noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
nom_semestre_terminal = noms_semestre_de_aggregat[-1] nom_semestre_terminal = noms_semestre_de_aggregat[-1]
@ -167,14 +181,14 @@ class RCSsJuryPE:
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
} }
"""Le formsemestre terminal (dernier en date) associé au # Le formsemestre terminal (dernier en date) associé au
semestre marquant la fin de l'aggrégat # semestre marquant la fin de l'aggrégat
(par ex: son dernier S3 en date)""" # (par ex: son dernier S3 en date)
semestres = etudiants.cursus[etudid][nom_semestre_terminal] semestres = etudiants.cursus[etudid][nom_semestre_terminal]
if semestres: if semestres:
formsemestre_final = get_dernier_semestre_en_date(semestres) formsemestre_final = get_dernier_semestre_en_date(semestres)
"""Ajout ou récupération de la trajectoire""" # Ajout ou récupération de la trajectoire
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id) trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
if trajectoire_id not in self.rcss: if trajectoire_id not in self.rcss:
trajectoire = RCS(nom_rcs, formsemestre_final) trajectoire = RCS(nom_rcs, formsemestre_final)
@ -182,21 +196,22 @@ class RCSsJuryPE:
else: else:
trajectoire = self.rcss[trajectoire_id] trajectoire = self.rcss[trajectoire_id]
"""La liste des semestres de l'étudiant à prendre en compte # La liste des semestres de l'étudiant à prendre en compte
pour cette trajectoire""" # pour cette trajectoire
semestres_a_aggreger = get_rcs_etudiant( semestres_a_aggreger = get_rcs_etudiant(
etudiants.cursus[etudid], formsemestre_final, nom_rcs etudiants.cursus[etudid], formsemestre_final, nom_rcs
) )
"""Ajout des semestres à la trajectoire""" # Ajout des semestres à la trajectoire
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger) trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
"""Mémoire la trajectoire suivie par l'étudiant""" # Mémoire la trajectoire suivie par l'étudiant
self.suivi[etudid][nom_rcs] = trajectoire self.suivi[etudid][nom_rcs] = trajectoire
def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str def get_rcs_etudiant(
): semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
) -> dict[int, FormSemestre]:
"""Ensemble des semestres parcourus par un étudiant, connaissant """Ensemble des semestres parcourus par un étudiant, connaissant
les semestres de son cursus, les semestres de son cursus,
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`. dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
@ -247,6 +262,7 @@ def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: Form
semestres_aggreges[fid] = semestre semestres_aggreges[fid] = semestre
return semestres_aggreges return semestres_aggreges
def get_descr_rcs(nom_rcs: str) -> str: def get_descr_rcs(nom_rcs: str) -> str:
"""Renvoie la description pour les tableurs de synthèse """Renvoie la description pour les tableurs de synthèse
Excel d'un nom de RCS""" Excel d'un nom de RCS"""

View File

@ -35,33 +35,30 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc @author: barasc
""" """
import numpy as np import pandas as pd
import app.pe.pe_etudiant import app.pe.pe_etudiant
from app import db, log, ScoValueError from app import db, ScoValueError
from app.comp import res_sem, moy_ue, moy_sem from app import comp
from app.comp.moy_sem import comp_ranks_series
from app.comp.res_compat import NotesTableCompat
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre from app.models import FormSemestre
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, MoyenneTag
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag
import pandas as pd
class SemestreTag(TableTag): class SemestreTag(TableTag):
def __init__(self, formsemestre_id: int):
""" """
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag. accès aux moyennes par tag.
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
"""
def __init__(self, formsemestre_id: int):
"""
Args: Args:
nom: Nom à donner au SemestreTag
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
""" """
TableTag.__init__(self) TableTag.__init__(self)
@ -103,27 +100,27 @@ class SemestreTag(TableTag):
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
noms_tags_comp = list(set(dict_ues_competences.values())) noms_tags_comp = list(set(dict_ues_competences.values()))
noms_tags_auto = ["but"] + noms_tags_comp noms_tags_auto = ["but"] + noms_tags_comp
self.tags = ( self.tags = noms_tags_perso + noms_tags_auto
noms_tags_perso + noms_tags_auto
)
"""Tags du semestre taggué""" """Tags du semestre taggué"""
## Vérifie l'unicité des tags ## Vérifie l'unicité des tags
if len(set(self.tags)) != len(self.tags): if len(set(self.tags)) != len(self.tags):
intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
liste_intersection = "\n".join([f"<li><code>{tag}</code></li>" for tag in intersection]) liste_intersection = "\n".join(
message = f"""Erreur dans le module PE : Un des tags saisis dans votre programme de formation [f"<li><code>{tag}</code></li>" for tag in intersection]
fait parti des tags réservés. En particulier, )
s = "s" if len(intersection) > 0 else ""
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
programme de formation fait parti des tags réservés. En particulier,
votre semestre <em>{self.formsemestre.titre_annee()}</em> votre semestre <em>{self.formsemestre.titre_annee()}</em>
contient le(s) tag(s) réservé(s) suivant : contient le{s} tag{s} réservé{s} suivant :
<ul> <ul>
{liste_intersection} {liste_intersection}
</ul> </ul>
Modifiez votre programme de formation pour le(s) supprimer. Il(s) sera(ont) automatiquement à vos documents de poursuites d'études. Modifiez votre programme de formation pour le{s} supprimer.
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
""" """
raise ScoValueError( raise ScoValueError(message)
message
)
# Calcul des moyennes & les classements de chaque étudiant à chaque tag # Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {} self.moyennes_tags = {}
@ -174,25 +171,25 @@ class SemestreTag(TableTag):
La série des moyennes La série des moyennes
""" """
"""Adaptation du mask de calcul des moyennes au tag visé""" # Adaptation du mask de calcul des moyennes au tag visé
modimpls_mask = [ modimpls_mask = [
modimpl.module.ue.type != UE_SPORT modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
] ]
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag""" # Désactive tous les modules qui ne sont pas pris en compte pour ce tag
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted): for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
if modimpl.moduleimpl_id not in tags_infos[tag]: if modimpl.moduleimpl_id not in tags_infos[tag]:
modimpls_mask[i] = False modimpls_mask[i] = False
"""Applique la pondération des coefficients""" # Applique la pondération des coefficients
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy() modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in tags_infos[tag]: for modimpl_id in tags_infos[tag]:
ponderation = tags_infos[tag][modimpl_id]["ponderation"] ponderation = tags_infos[tag][modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)""" # Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
moyennes_ues_tag = moy_ue.compute_ue_moys_apc( moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
self.sem_cube, self.sem_cube,
self.etuds, self.etuds,
self.formsemestre.modimpls_sorted, self.formsemestre.modimpls_sorted,
@ -203,13 +200,13 @@ class SemestreTag(TableTag):
block=self.formsemestre.block_moyennes, block=self.formsemestre.block_moyennes,
) )
"""Les ects""" # Les ects
ects = self.ues_inscr_parcours_df.fillna(0.0) * [ ects = self.ues_inscr_parcours_df.fillna(0.0) * [
ue.ects for ue in self.ues if ue.type != UE_SPORT 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)""" # 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( moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moyennes_ues_tag, moyennes_ues_tag,
ects, ects,
formation_id=self.formsemestre.formation_id, formation_id=self.formsemestre.formation_id,
@ -224,11 +221,6 @@ def get_moduleimpl(modimpl_id) -> dict:
modimpl = db.session.get(ModuleImpl, modimpl_id) modimpl = db.session.get(ModuleImpl, modimpl_id)
if modimpl: if modimpl:
return modimpl return modimpl
if SemestreTag.DEBUG:
log(
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
% (modimpl_id)
)
return None return None
@ -260,27 +252,26 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
""" """
synthese_tags = {} synthese_tags = {}
"""Instance des modules du semestre""" # Instance des modules du semestre
modimpls = formsemestre.modimpls_sorted modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls: for modimpl in modimpls:
modimpl_id = modimpl.id modimpl_id = modimpl.id
"""Liste des tags pour le module concerné""" # Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id) tags = sco_tag_module.module_tag_list(modimpl.module.id)
"""Traitement des tags recensés, chacun pouvant étant de la forme # Traitement des tags recensés, chacun pouvant étant de la forme
"mathématiques", "théorie", "pe:0", "maths:2" # "mathématiques", "théorie", "pe:0", "maths:2"
"""
for tag in tags: for tag in tags:
"""Extraction du nom du tag et du coeff de pondération""" # Extraction du nom du tag et du coeff de pondération
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag) (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
"""Ajout d'une clé pour le tag""" # Ajout d'une clé pour le tag
if tagname not in synthese_tags: if tagname not in synthese_tags:
synthese_tags[tagname] = {} synthese_tags[tagname] = {}
"""Ajout du module (modimpl) au tagname considéré""" # Ajout du module (modimpl) au tagname considéré
synthese_tags[tagname][modimpl_id] = { synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module "modimpl": modimpl, # les données sur le module
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre # "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
@ -298,6 +289,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
"""Partant d'un formsemestre, extrait le nom des compétences associés """Partant d'un formsemestre, extrait le nom des compétences associés
à (ou aux) parcours des étudiants du formsemestre. à (ou aux) parcours des étudiants du formsemestre.
Ignore les UEs non associées à un niveau de compétence.
Args: Args:
formsemestre: Un FormSemestre formsemestre: Un FormSemestre
@ -310,8 +303,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
noms_competences = {} noms_competences = {}
for ue in nt.ues: for ue in nt.ues:
if ue.type != UE_SPORT: if ue.niveau_competence and ue.type != UE_SPORT:
ordre = ue.niveau_competence.ordre # ?? inutilisé ordre = ue.niveau_competence.ordre
nom = ue.niveau_competence.competence.titre nom = ue.niveau_competence.competence.titre
noms_competences[ue.ue_id] = f"comp. {nom}" noms_competences[ue.ue_id] = f"comp. {nom}"
return noms_competences return noms_competences

View File

@ -870,7 +870,7 @@ def stripquotes(s):
return s return s
def suppress_accents(s): def suppress_accents(s: str) -> str:
"remove accents and suppress non ascii characters from string s" "remove accents and suppress non ascii characters from string s"
if isinstance(s, str): if isinstance(s, str):
return ( return (