1
0
forked from ScoDoc/ScoDoc

Merge branch 'pe-but-v4' of https://scodoc.org/git/cleo/ScoDoc-PE into cleo

This commit is contained in:
Emmanuel Viennet 2024-02-27 21:15:38 +01:00
commit ce63b7f2f5
28 changed files with 3958 additions and 1897 deletions

View File

@ -187,7 +187,7 @@ def dept_etudiants(acronym: str):
] ]
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etats_civils]
@bp.route("/departement/id/<int:dept_id>/etudiants") @bp.route("/departement/id/<int:dept_id>/etudiants")
@ -200,7 +200,7 @@ def dept_etudiants_by_id(dept_id: int):
Retourne la liste des étudiants d'un département d'id donné. Retourne la liste des étudiants d'un département d'id donné.
""" """
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etats_civils]
@bp.route("/departement/<string:acronym>/formsemestres_ids") @bp.route("/departement/<string:acronym>/formsemestres_ids")

View File

@ -0,0 +1,65 @@
##############################################################################
#
# ScoDoc
#
# 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
#
##############################################################################
"""
Formulaire options génération table poursuite études (PE)
"""
from flask_wtf import FlaskForm
from wtforms import BooleanField, HiddenField, SubmitField
class ParametrageClasseurPE(FlaskForm):
"Formulaire paramétrage génération classeur PE"
# cohorte_restreinte = BooleanField(
# "Restreindre aux étudiants inscrits dans le semestre (sans interclassement de promotion) (à venir)"
# )
moyennes_tags = BooleanField(
"Générer les moyennes sur les tags de modules personnalisés (cf. programme de formation)",
default=True,
render_kw={"checked": ""},
)
moyennes_ue_res_sae = BooleanField(
"Générer les moyennes des ressources et des SAEs",
default=True,
render_kw={"checked": ""},
)
moyennes_ues_rcues = BooleanField(
"Générer les moyennes par RCUEs (compétences) et leurs synthèses HTML étudiant par étudiant",
default=True,
render_kw={"checked": ""},
)
min_max_moy = BooleanField("Afficher les colonnes min/max/moy")
# synthese_individuelle_etud = BooleanField(
# "Générer (suppose les RCUES)"
# )
publipostage = BooleanField(
"Nomme les moyennes pour publipostage",
# default=False,
# render_kw={"checked": ""},
)
submit = SubmitField("Générer les classeurs poursuites d'études")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

0
app/pe/moys/__init__.py Normal file
View File

View File

@ -0,0 +1,342 @@
##############################################################################
#
# 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.models import Identite
from app.pe import pe_affichage
from app.pe.moys import pe_tabletags, pe_moy, pe_moytag, pe_sxtag
from app.pe.rcss import pe_rcs
import app.pe.pe_comp as pe_comp
from app.scodoc.sco_utils import ModuleType
class InterClassTag(pe_tabletags.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'), qu'il soit
de type SemX ou RCSemX,
en reportant les moyennes obtenues sur à la version tagguée
du RCS (de type SxTag ou RCSTag).
Sont ensuite calculés les classements (uniquement)
sur les étudiants diplômes.
Args:
nom_rcs: Le nom de l'aggrégat
type_interclassement: Le type d'interclassement (par UE ou par compétences)
etudiants_diplomes: L'identité des étudiants diplômés
rcss: Un dictionnaire {(nom_rcs, fid_final): RCS} donnant soit
les SemX soit les RCSemX recencés par le jury PE
rcstag: Un dictionnaire {(nom_rcs, fid_final): RCSTag} donnant
soit les SxTag (associés aux SemX)
soit les RCSTags (associés au RCSemX) calculés par le jury PE
suivis: Un dictionnaire associé à chaque étudiant son rcss
(de la forme ``{etudid: {nom_rcs: RCS_suivi}}``)
"""
def __init__(
self,
nom_rcs: str,
type_interclassement: str,
etudiants_diplomes: dict[int, Identite],
rcss: dict[(str, int) : pe_rcs.RCS],
rcstags: dict[(str, int) : pe_tabletags.TableTag],
suivis: dict[int:dict],
):
pe_tabletags.TableTag.__init__(self)
self.nom_rcs: str = nom_rcs
"""Le nom du RCS interclassé"""
# Le type d'interclassement
self.type = type_interclassement
pe_affichage.pe_print(
f"*** Interclassement par 🗂️{type_interclassement} pour le RCS ⏯️{nom_rcs}"
)
# Les informations sur les étudiants diplômés
self.etuds: list[Identite] = list(etudiants_diplomes.values())
"""Identités des étudiants diplômés"""
self.add_etuds(self.etuds)
self.diplomes_ids = set(etudiants_diplomes.keys())
"""Etudids des étudiants diplômés"""
# Les RCS de l'aggrégat (SemX ou RCSemX)
self.rcss: dict[(str, int), pe_rcs.RCS] = {}
"""Ensemble des SemX ou des RCSemX associés à l'aggrégat"""
for (nom, fid), rcs in rcss.items():
if nom == nom_rcs:
self.rcss[(nom, fid)] = rcss
# Les données tagguées
self.rcstags: dict[(str, int), pe_tabletags.TableTag] = {}
"""Ensemble des SxTag ou des RCSTags associés à l'aggrégat"""
for rcs_id in self.rcss:
self.rcstags[rcs_id] = rcstags[rcs_id]
# Les RCS (SemX ou RCSemX) suivis par les étudiants du jury,
# en ne gardant que ceux associés aux diplomés
self.suivis: dict[int, pe_rcs.RCS] = {}
"""Association entre chaque étudiant et le SxTag ou RCSTag à prendre
pour l'aggrégat"""
for etudid in self.diplomes_ids:
self.suivis[etudid] = suivis[etudid][nom_rcs]
# Les données sur les tags
self.tags_sorted = self._do_taglist()
"""Liste des tags (triés par ordre alphabétique)"""
aff = pe_affichage.repr_tags(self.tags_sorted)
pe_affichage.pe_print(f"--> Tags : {aff}")
# Les données sur les UEs (si SxTag) ou compétences (si RCSTag)
self.champs_sorted = self._do_ues_ou_competences_list()
"""Les champs (UEs ou compétences) de l'interclassement"""
if self.type == pe_moytag.CODE_MOY_UE:
pe_affichage.pe_print(
f"--> UEs : {pe_affichage.aff_UEs(self.champs_sorted)}"
)
else:
pe_affichage.pe_print(
f"--> Compétences : {pe_affichage.aff_competences(self.champs_sorted)}"
)
# Etudids triés
self.etudids_sorted = sorted(list(self.diplomes_ids))
self.nom = self.get_repr()
"""Représentation textuelle de l'interclassement"""
# Synthétise les moyennes/classements par tag
self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}
for tag in self.tags_sorted:
# Les moyennes tous modules confondus
notes_gen = self.compute_notes_matrice(tag)
# Les coefficients de la moyenne générale
coeffs = self.compute_coeffs_matrice(tag)
aff = pe_affichage.repr_profil_coeffs(coeffs, with_index=True)
pe_affichage.pe_print(f"--> Moyenne 👜{tag} avec coeffs: {aff} ")
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
self.type,
notes_gen,
coeffs, # limite les moyennes aux étudiants de la promo
)
def get_repr(self) -> str:
"""Une représentation textuelle"""
return f"{self.nom_rcs} par {self.type}"
def _do_taglist(self):
"""Synthétise les tags à partir des TableTags (SXTag ou RCSTag)
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
for rcstag in self.rcstags.values():
tags.extend(rcstag.tags_sorted)
return sorted(set(tags))
def compute_notes_matrice(self, tag) -> pd.DataFrame:
"""Construit la matrice de notes (etudids x champs) en
reportant les moyennes obtenues par les étudiants
aux semestres de l'aggrégat pour le tag visé.
Les champs peuvent être des acronymes d'UEs ou des compétences.
Args:
tag: Le tag visé
Return:
Le dataFrame (etudids x champs)
reportant les moyennes des étudiants aux champs
"""
# etudids_sorted: Les etudids des étudiants (diplômés) triés
# champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted)
for rcstag in self.rcstags.values():
# Charge les moyennes au tag d'un RCStag
if tag in rcstag.moyennes_tags:
moytag = rcstag.moyennes_tags[tag]
notes = moytag.matrice_notes_gen # dataframe etudids x ues
# Etudiants/Champs communs entre le RCSTag et les données interclassées
(
etudids_communs,
champs_communs,
) = pe_comp.find_index_and_columns_communs(df, notes)
# Injecte les notes par tag
df.loc[etudids_communs, champs_communs] = notes.loc[
etudids_communs, champs_communs
]
return df
def compute_coeffs_matrice(self, tag) -> pd.DataFrame:
"""Idem que compute_notes_matrices mais pour les coeffs
Args:
tag: Le tag visé
Return:
Le dataFrame (etudids x champs)
reportant les moyennes des étudiants aux champs
"""
# etudids_sorted: Les etudids des étudiants (diplômés) triés
# champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted)
for rcstag in self.rcstags.values():
if tag in rcstag.moyennes_tags:
# Charge les coeffs au tag d'un RCStag
coeffs: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_coeffs_moy_gen
# Etudiants/Champs communs entre le RCSTag et les données interclassées
(
etudids_communs,
champs_communs,
) = pe_comp.find_index_and_columns_communs(df, coeffs)
# Injecte les coeffs par tag
df.loc[etudids_communs, champs_communs] = coeffs.loc[
etudids_communs, champs_communs
]
return df
def _do_ues_ou_competences_list(self) -> list[str]:
"""Synthétise les champs (UEs ou compétences) sur lesquels
sont calculés les moyennes.
Returns:
Un dictionnaire {'acronyme_ue' : 'compétences'}
"""
dict_champs = []
for rcstag in self.rcstags.values():
if isinstance(rcstag, pe_sxtag.SxTag):
champs = rcstag.acronymes_sorted
else: # pe_rcstag.RCSTag
champs = rcstag.competences_sorted
dict_champs.extend(champs)
return sorted(set(dict_champs))
def has_tags(self):
"""Indique si l'interclassement a des tags (cas d'un
interclassement sur un S5 qui n'a pas eu lieu)
"""
return len(self.tags_sorted) > 0
def _un_rcstag_significatif(self, rcsstags: dict[(str, int):pe_tabletags]):
"""Renvoie un rcstag significatif (ayant des tags et des notes aux tags)
parmi le dictionnaire de rcsstags"""
for rcstag_id, rcstag in rcsstags.items():
moystags: pe_moytag.MoyennesTag = rcstag.moyennes_tags
for tag, moystag in moystags.items():
tags_tries = moystag.get_all_significant_tags()
if tags_tries:
return moystag
return None
def compute_df_synthese_moyennes_tag(
self, tag, aggregat=None, type_colonnes=False, options={"min_max_moy": True}
) -> pd.DataFrame:
"""Construit le dataframe retraçant pour les données des moyennes
pour affichage dans la synthèse du jury PE. (cf. to_df())
Args:
etudids_sorted: Les etudids des étudiants (diplômés) triés
champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
Return:
Le dataFrame (etudids x champs)
reportant les moyennes des étudiants aux champs
"""
if aggregat:
assert (
aggregat == self.nom_rcs
), "L'interclassement ciblé ne correspond pas à l'aggrégat visé"
etudids_sorted = sorted(list(self.diplomes_ids))
if not self.rcstags:
return None
# Partant d'un dataframe vierge
initialisation = False
df = pd.DataFrame()
# Pour chaque rcs (suivi) associe la liste des etudids l'ayant suivi
asso_rcs_etudids = {}
for etudid in etudids_sorted:
rcs = self.suivis[etudid]
if rcs:
if rcs.rcs_id not in asso_rcs_etudids:
asso_rcs_etudids[rcs.rcs_id] = []
asso_rcs_etudids[rcs.rcs_id].append(etudid)
for rcs_id, etudids in asso_rcs_etudids.items():
# Charge ses moyennes au RCSTag suivi
rcstag = self.rcstags[rcs_id] # Le SxTag ou RCSTag
# Charge la moyenne
if tag in rcstag.moyennes_tags:
moytag: pd.DataFrame = rcstag.moyennes_tags[tag]
df_moytag = moytag.to_df(
aggregat=aggregat, cohorte="Groupe", options=options
)
# Modif les colonnes au regard du 1er df_moytag significatif lu
if not initialisation:
df = pd.DataFrame(
np.nan, index=etudids_sorted, columns=df_moytag.columns
)
colonnes = list(df_moytag.columns)
for col in colonnes:
if col.endswith("rang"):
df[col] = df[col].astype(str)
initialisation = True
# Injecte les notes des étudiants
df.loc[etudids, :] = df_moytag.loc[etudids, :]
return df

128
app/pe/moys/pe_moy.py Normal file
View File

@ -0,0 +1,128 @@
import numpy as np
import pandas as pd
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
class Moyenne:
COLONNES = [
"note",
"classement",
"rang",
"min",
"max",
"moy",
"nb_etuds",
"nb_inscrits",
]
"""Colonnes du df"""
@classmethod
def get_colonnes_synthese(cls, with_min_max_moy):
if with_min_max_moy:
return ["note", "rang", "min", "max", "moy"]
else:
return ["note", "rang"]
def __init__(self, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
de notes :
* des "notes" : la Serie pandas des notes (float),
* des "classements" : la Serie pandas des classements (float),
* des "min" : la note minimum,
* des "max" : la note maximum,
* des "moy" : la moyenne,
* des "nb_inscrits" : le nombre d'étudiants ayant une note,
"""
self.notes = notes
"""Les notes"""
self.etudids = list(notes.index) # calcul à venir
"""Les id des étudiants"""
self.inscrits_ids = notes[notes.notnull()].index.to_list()
"""Les id des étudiants dont la note est non nulle"""
self.df: pd.DataFrame = self.comp_moy_et_stat(self.notes)
"""Le dataframe retraçant les moyennes/classements/statistiques"""
self.synthese = self.to_dict()
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
de notes (pouvant être une moyenne d'un tag à une UE ou une moyenne générale
d'un tag) dans un dictionnaire spécifique.
Partant des notes, sont calculés les classements (en ne tenant compte
que des notes non nulles).
Args:
notes: Une série de notes (avec des éventuels NaN)
Returns:
Un dictionnaire stockant les notes, les classements, le min,
le max, la moyenne, le nb de notes (donc d'inscrits)
"""
df = pd.DataFrame(
np.nan,
index=self.etudids,
columns=Moyenne.COLONNES,
)
# Supprime d'éventuelles chaines de caractères dans les notes
notes = pd.to_numeric(notes, errors="coerce")
df["note"] = notes
# Les nb d'étudiants & nb d'inscrits
df["nb_etuds"] = len(self.etudids)
df["nb_etuds"] = df["nb_etuds"].astype(int)
# Les étudiants dont la note n'est pas nulle
inscrits_ids = notes[notes.notnull()].index.to_list()
df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids)
# df["nb_inscrits"] = df["nb_inscrits"].astype(int)
# Le classement des inscrits
notes_non_nulles = notes[inscrits_ids]
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
df.loc[inscrits_ids, "classement"] = class_int
# df["classement"] = df["classement"].astype(int)
# Le rang (classement/nb_inscrit)
df["rang"] = df["rang"].astype(str)
df.loc[inscrits_ids, "rang"] = (
df.loc[inscrits_ids, "classement"].astype(int).astype(str)
+ "/"
+ df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str)
)
# Les stat (des inscrits)
df.loc[inscrits_ids, "min"] = notes.min()
df.loc[inscrits_ids, "max"] = notes.max()
df.loc[inscrits_ids, "moy"] = notes.mean()
return df
def get_df_synthese(self, with_min_max_moy=None):
"""Renvoie le df de synthese limité aux colonnes de synthese"""
colonnes_synthese = Moyenne.get_colonnes_synthese(
with_min_max_moy=with_min_max_moy
)
df = self.df[colonnes_synthese].copy()
df["rang"] = df["rang"].replace("nan", "")
return df
def to_dict(self) -> dict:
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)"""
synthese = {
"notes": self.df["note"],
"classements": self.df["classement"],
"min": self.df["min"].mean(),
"max": self.df["max"].mean(),
"moy": self.df["moy"].mean(),
"nb_inscrits": self.df["nb_inscrits"].mean(),
}
return synthese
def is_significatif(self) -> bool:
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
return self.synthese["nb_inscrits"] > 0

169
app/pe/moys/pe_moytag.py Normal file
View File

@ -0,0 +1,169 @@
import numpy as np
import pandas as pd
from app import comp
from app.comp.moy_sem import comp_ranks_series
from app.pe.moys import pe_moy
from app.scodoc.sco_utils import ModuleType
CODE_MOY_UE = "UEs"
CODE_MOY_COMPETENCES = "Compétences"
CHAMP_GENERAL = "Général" # Nom du champ de la moyenne générale
class MoyennesTag:
def __init__(
self,
tag: str,
type_moyenne: str,
matrice_notes_gen: pd.DataFrame, # etudids x colonnes
matrice_coeffs: pd.DataFrame, # etudids x colonnes
):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en différenciant les notes
obtenues aux UE et au général (toutes UEs confondues)
Args:
tag: Un tag
matrice_notes_gen: Les moyennes (etudid x acronymes_ues ou etudid x compétences)
aux différentes UEs ou compétences
# notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues)
"""
self.tag = tag
"""Le tag associé aux moyennes"""
self.type = type_moyenne
"""Le type de moyennes (par UEs ou par compétences)"""
# Les moyennes par UE/compétences (ressources/SAEs confondues)
self.matrice_notes_gen: pd.DataFrame = matrice_notes_gen
"""Les notes par UEs ou Compétences (DataFrame)"""
self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs
"""Les coeffs à appliquer pour le calcul des moyennes générales
(toutes UE ou compétences confondues). NaN si étudiant non inscrit"""
self.moyennes_gen: dict[int, pd.DataFrame] = {}
"""Dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs"""
self.etudids = self.matrice_notes_gen.index
"""Les étudids renseignés dans les moyennes"""
self.champs = self.matrice_notes_gen.columns
"""Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes"""
for col in self.champs: # if ue.type != UE_SPORT:
# Les moyennes tous modules confondus
notes = matrice_notes_gen[col]
self.moyennes_gen[col] = pe_moy.Moyenne(notes)
# Les moyennes générales (toutes UEs confondues)
self.notes_gen = pd.Series(np.nan, index=self.matrice_notes_gen.index)
if self.has_notes():
self.notes_gen = self.compute_moy_gen(
self.matrice_notes_gen, self.matrice_coeffs_moy_gen
)
self.moyenne_gen = pe_moy.Moyenne(self.notes_gen)
"""Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et modules confondus)"""
def has_notes(self):
"""Détermine si les moyennes (aux UEs ou aux compétences)
ont des notes
Returns:
True si la moytag a des notes, False sinon
"""
notes = self.matrice_notes_gen
nbre_nan = notes.isna().sum().sum()
nbre_notes_potentielles = len(notes.index) * len(notes.columns)
if nbre_nan == nbre_notes_potentielles:
return False
else:
return True
def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series:
"""Calcule la moyenne générale (toutes UE/compétences confondus)
pour le tag considéré, en pondérant les notes obtenues au UE
par les coeff (généralement les crédits ECTS).
Args:
moys: Les moyennes etudids x acronymes_ues/compétences
coeff: Les coeff etudids x ueids/compétences
"""
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
try:
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moys,
coeffs.fillna(0.0),
# formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
except TypeError as e:
raise TypeError(
"Pb dans le calcul de la moyenne toutes UEs/compétences confondues"
)
return moy_gen_tag
def to_df(
self, aggregat=None, cohorte=None, options={"min_max_moy": True}
) -> pd.DataFrame:
"""Renvoie le df synthétisant l'ensemble des données
connues
Adapte les intitulés des colonnes aux données fournies
(nom d'aggrégat, type de cohorte).
"""
if "min_max_moy" not in options or options["min_max_moy"]:
with_min_max_moy = True
else:
with_min_max_moy = False
etudids_sorted = sorted(self.etudids)
df = pd.DataFrame(index=etudids_sorted)
# Ajout des notes pour tous les champs
champs = list(self.champs)
for champ in champs:
df_champ = self.moyennes_gen[champ].get_df_synthese(
with_min_max_moy=with_min_max_moy
) # le dataframe
# Renomme les colonnes
cols = [
get_colonne_df(aggregat, self.tag, champ, cohorte, critere)
for critere in pe_moy.Moyenne.get_colonnes_synthese(
with_min_max_moy=with_min_max_moy
)
]
df_champ.columns = cols
df = df.join(df_champ)
# Ajoute la moy générale
df_moy_gen = self.moyenne_gen.get_df_synthese(with_min_max_moy=with_min_max_moy)
cols = [
get_colonne_df(aggregat, self.tag, CHAMP_GENERAL, cohorte, critere)
for critere in pe_moy.Moyenne.get_colonnes_synthese(
with_min_max_moy=with_min_max_moy
)
]
df_moy_gen.columns = cols
df = df.join(df_moy_gen)
return df
def get_colonne_df(aggregat, tag, champ, cohorte, critere):
"""Renvoie le tuple (aggregat, tag, champ, cohorte, critere)
utilisé pour désigner les colonnes du df"""
liste_champs = []
if aggregat != None:
liste_champs += [aggregat]
liste_champs += [tag, champ]
if cohorte != None:
liste_champs += [cohorte]
liste_champs += [critere]
return "|".join(liste_champs)

466
app/pe/moys/pe_rcstag.py Normal file
View File

@ -0,0 +1,466 @@
# -*- 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 Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.models import FormSemestre
from app.pe import pe_affichage
import pandas as pd
import numpy as np
from app.pe.rcss import pe_rcs, pe_rcsemx
import app.pe.moys.pe_sxtag as pe_sxtag
import app.pe.pe_comp as pe_comp
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc.sco_utils import ModuleType
class RCSemXTag(pe_tabletags.TableTag):
def __init__(
self,
rcsemx: pe_rcsemx.RCSemX,
sxstags: dict[(str, int) : pe_sxtag.SxTag],
semXs_suivis: dict[int, dict],
):
"""Calcule les moyennes par tag (orientées compétences)
d'un regroupement de SxTag
(RCRCF), pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au même semestre terminal.
Args:
rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal)
sxstags: Les données sur les SemX taggués
semXs_suivis: Les données indiquant quels SXTags sont à prendre en compte
pour chaque étudiant
"""
pe_tabletags.TableTag.__init__(self)
self.rcs_id: tuple(str, int) = rcsemx.rcs_id
"""Identifiant du RCSemXTag (identique au RCSemX sur lequel il s'appuie)"""
self.rcsemx: pe_rcsemx.RCSemX = rcsemx
"""Le regroupement RCSemX associé au RCSemXTag"""
self.semXs_suivis = semXs_suivis
"""Les semXs suivis par les étudiants"""
self.nom = self.get_repr()
"""Représentation textuelle du RSCtag"""
# Les données du semestre final
self.formsemestre_final: FormSemestre = rcsemx.formsemestre_final
"""Le semestre final"""
self.fid_final: int = rcsemx.formsemestre_final.formsemestre_id
"""Le fid du semestre final"""
# Affichage pour debug
pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")
# Les données aggrégés (RCRCF + SxTags)
self.semXs_aggreges: dict[(str, int) : pe_rcsemx.RCSemX] = rcsemx.semXs_aggreges
"""Les SemX aggrégés"""
self.sxstags_aggreges = {}
"""Les SxTag associés aux SemX aggrégés"""
try:
for rcf_id in self.semXs_aggreges:
self.sxstags_aggreges[rcf_id] = sxstags[rcf_id]
except:
raise ValueError("Semestres SxTag manquants")
self.sxtags_connus = sxstags # Tous les sxstags connus
# Les étudiants (etuds, états civils & etudis)
sems_dans_aggregat = rcsemx.aggregat
sxtag_final = self.sxstags_aggreges[(sems_dans_aggregat[-1], self.rcs_id[1])]
self.etuds = sxtag_final.etuds
"""Les étudiants (extraits du semestre final)"""
self.add_etuds(self.etuds)
self.etudids_sorted = sorted(self.etudids)
"""Les étudids triés"""
# Les compétences (extraites de tous les Sxtags)
self.acronymes_ues_to_competences = self._do_acronymes_to_competences()
"""L'association acronyme d'UEs -> compétence (extraites des SxTag aggrégés)"""
self.competences_sorted = sorted(
set(self.acronymes_ues_to_competences.values())
)
"""Compétences (triées par nom, extraites des SxTag aggrégés)"""
aff = pe_affichage.repr_comp_et_ues(self.acronymes_ues_to_competences)
pe_affichage.pe_print(f"--> Compétences : {', '.join(self.competences_sorted)}")
# Les tags
self.tags_sorted = self._do_taglist()
"""Tags extraits de tous les SxTag aggrégés"""
aff_tag = ["👜" + tag for tag in self.tags_sorted]
pe_affichage.pe_print(f"--> Tags : {', '.join(aff_tag)}")
# Les moyennes
self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
pe_affichage.pe_print(f"--> Moyennes du tag 👜{tag}")
# Traitement des inscriptions aux semX(tags)
# ******************************************
# Cube d'inscription (etudids_sorted x compétences_sorted x sxstags)
# indiquant quel sxtag est valide pour chaque étudiant
inscr_df, inscr_cube = self.compute_inscriptions_comps_cube(tag)
# Traitement des notes
# ********************
# Cube de notes (etudids_sorted x compétences_sorted x sxstags)
notes_df, notes_cube = self.compute_notes_comps_cube(tag)
# Calcule les moyennes sous forme d'un dataframe en les "aggrégant"
# compétence par compétence
moys_competences = self.compute_notes_competences(notes_cube, inscr_cube)
# Traitement des coeffs pour la moyenne générale
# ***********************************************
# Df des coeffs sur tous les SxTags aggrégés
coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube(tag)
# Synthèse des coefficients à prendre en compte pour la moyenne générale
matrice_coeffs_moy_gen = self.compute_coeffs_competences(
coeffs_cube, inscr_cube, notes_cube
)
# Affichage des coeffs
aff = pe_affichage.repr_profil_coeffs(
matrice_coeffs_moy_gen, with_index=True
)
pe_affichage.pe_print(f" > Moyenne calculée avec pour coeffs : {aff}")
# Mémorise les moyennes et les coeff associés
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
pe_moytag.CODE_MOY_COMPETENCES,
moys_competences,
matrice_coeffs_moy_gen,
)
def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
return self.rcs_id == other.sxtag_id
def get_repr(self, verbose=True) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
if verbose:
return f"{self.__class__.__name__} basé sur " + self.rcsemx.get_repr(
verbose=verbose
)
else:
return f"{self.__class__.__name__} {self.rcs_id}"
def compute_notes_comps_cube(self, tag):
"""Pour un tag donné, construit le cube de notes (etudid x competences x SxTag)
nécessaire au calcul des moyennes,
en remplaçant les données d'UE (obtenus du SxTag) par les compétences
Args:
tag: Le tag visé
"""
# etudids_sorted: list[int],
# competences_sorted: list[str],
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
notes_dfs = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
# Partant d'un dataframe vierge
notes_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
# Charge les notes du semestre tag (copie car changement de nom de colonnes à venir)
if tag in sxtag.moyennes_tags: # si le tag est présent dans le semestre
moys_tag = sxtag.moyennes_tags[tag]
notes = moys_tag.matrice_notes_gen.copy() # dataframe etudids x ues
# Traduction des acronymes d'UE en compétences
acronymes_ues_columns = notes.columns
acronymes_to_comps = [
self.acronymes_ues_to_competences[acro]
for acro in acronymes_ues_columns
]
notes.columns = acronymes_to_comps
# Les étudiants et les compétences communes
(
etudids_communs,
comp_communes,
) = pe_comp.find_index_and_columns_communs(notes_df, notes)
# Recopie des notes et des coeffs
notes_df.loc[etudids_communs, comp_communes] = notes.loc[
etudids_communs, comp_communes
]
# Supprime tout ce qui n'est pas numérique
# for col in notes_df.columns:
# notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce")
# Stocke les dfs
notes_dfs[sxtag_id] = notes_df
"""Réunit les notes sous forme d'un cube etudids x competences x semestres"""
sxtag_x_etudids_x_comps = [
notes_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
return notes_dfs, notes_etudids_x_comps_x_sxtag
def compute_coeffs_comps_cube(self, tag):
"""Pour un tag donné, construit
le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions
des étudiants aux UEs en fonction de leur parcours)
qui s'applique aux différents SxTag
en remplaçant les données d'UE (obtenus du SxTag) par les compétences
Args:
tag: Le tag visé
"""
# etudids_sorted: list[int],
# competences_sorted: list[str],
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
coeffs_dfs = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
# Partant d'un dataframe vierge
coeffs_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
if tag in sxtag.moyennes_tags:
moys_tag = sxtag.moyennes_tags[tag]
# Charge les notes et les coeffs du semestre tag
coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs
# Traduction des acronymes d'UE en compétences
acronymes_ues_columns = coeffs.columns
acronymes_to_comps = [
self.acronymes_ues_to_competences[acro]
for acro in acronymes_ues_columns
]
coeffs.columns = acronymes_to_comps
# Les étudiants et les compétences communes
etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(
coeffs_df, coeffs
)
# Recopie des notes et des coeffs
coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[
etudids_communs, comp_communes
]
# Stocke les dfs
coeffs_dfs[sxtag_id] = coeffs_df
"""Réunit les coeffs sous forme d'un cube etudids x competences x semestres"""
sxtag_x_etudids_x_comps = [
coeffs_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
return coeffs_dfs, coeffs_etudids_x_comps_x_sxtag
def compute_inscriptions_comps_cube(
self,
tag,
):
"""Pour un tag donné, construit
le cube etudid x competences x SxTag traduisant quels sxtags est à prendre
en compte pour chaque étudiant.
Contient des 0 et des 1 pour indiquer la prise en compte.
Args:
tag: Le tag visé
"""
# etudids_sorted: list[int],
# competences_sorted: list[str],
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
# Initialisation
inscriptions_dfs = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
# Partant d'un dataframe vierge
inscription_df = pd.DataFrame(
0, index=self.etudids_sorted, columns=self.competences_sorted
)
# Les étudiants dont les résultats au sxtag ont été calculés
etudids_sxtag = sxtag.etudids_sorted
# Les étudiants communs
etudids_communs = sorted(set(self.etudids_sorted) & set(etudids_sxtag))
# Acte l'inscription
inscription_df.loc[etudids_communs, :] = 1
# Stocke les dfs
inscriptions_dfs[sxtag_id] = inscription_df
"""Réunit les inscriptions sous forme d'un cube etudids x competences x semestres"""
sxtag_x_etudids_x_comps = [
inscriptions_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
inscriptions_etudids_x_comps_x_sxtag = np.stack(
sxtag_x_etudids_x_comps, axis=-1
)
return inscriptions_dfs, inscriptions_etudids_x_comps_x_sxtag
def _do_taglist(self) -> list[str]:
"""Synthétise les tags à partir des Sxtags aggrégés.
Returns:
Liste de tags triés par ordre alphabétique
"""
tags = []
for frmsem_id in self.sxstags_aggreges:
tags.extend(self.sxstags_aggreges[frmsem_id].tags_sorted)
return sorted(set(tags))
def _do_acronymes_to_competences(self) -> dict[str:str]:
"""Synthétise l'association complète {acronyme_ue: competences}
extraite de toutes les données/associations des SxTags
aggrégés.
Returns:
Un dictionnaire {'acronyme_ue' : 'compétences'}
"""
dict_competences = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
dict_competences |= sxtag.acronymes_ues_to_competences
return dict_competences
def compute_notes_competences(self, set_cube: np.array, inscriptions: np.array):
"""Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube).
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
par aggrégat de plusieurs semestres.
Args:
set_cube: notes moyennes aux compétences ndarray
(etuds x UEs|compétences x sxtags), des floats avec des NaN
inscriptions: inscrptions aux compétences ndarray
(etuds x UEs|compétences x sxtags), des 0 et des 1
Returns:
Un DataFrame avec pour columns les moyennes par tags,
et pour rows les etudid
"""
# etudids_sorted: liste des étudiants (dim. 0 du cube)
# competences_sorted: list (dim. 1 du cube)
nb_etuds, nb_comps, nb_semestres = set_cube.shape
# assert nb_etuds == len(etudids_sorted)
# assert nb_comps == len(competences_sorted)
# Applique le masque d'inscriptions
set_cube_significatif = set_cube * inscriptions
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube_significatif)
# Enlève les NaN du cube de notes pour les entrées manquantes
set_cube_no_nan = np.nan_to_num(set_cube_significatif, nan=0.0)
# Les moyennes par tag
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
# Le dataFrame des notes moyennes
etud_moy_tag_df = pd.DataFrame(
etud_moy_tag,
index=self.etudids_sorted, # les etudids
columns=self.competences_sorted, # les competences
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df
def compute_coeffs_competences(
self,
coeff_cube: np.array,
inscriptions: np.array,
set_cube: np.array,
):
"""Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences
confondues), en fonction des inscriptions.
Args:
coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres)
inscriptions: inscriptions aux UES|Compétences ndarray
(etuds x UEs|compétences x sxtags), des 0 ou des 1
set_cube: les notes
Returns:
Un DataFrame de coefficients (etudids_sorted x compétences_sorted)
"""
# etudids_sorted: liste des étudiants (dim. 0 du cube)
# competences_sorted: list (dim. 1 du cube)
nb_etuds, nb_comps, nb_semestres = inscriptions.shape
# assert nb_etuds == len(etudids_sorted)
# assert nb_comps == len(competences_sorted)
# Applique le masque des inscriptions aux coeffs et aux notes
coeffs_significatifs = coeff_cube * inscriptions
# Enlève les NaN du cube de notes pour les entrées manquantes
coeffs_cube_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0)
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Retire les coefficients associés à des données sans notes
coeffs_cube_no_nan = coeffs_cube_no_nan * mask
# Somme les coefficients (correspondant à des notes)
coeff_tag = np.sum(coeffs_cube_no_nan, axis=2)
# Le dataFrame des coeffs
coeffs_df = pd.DataFrame(
coeff_tag, index=self.etudids_sorted, columns=self.competences_sorted
)
# Remet à Nan les coeffs à 0
coeffs_df = coeffs_df.fillna(np.nan)
return coeffs_df

463
app/pe/moys/pe_ressemtag.py Normal file
View File

@ -0,0 +1,463 @@
# -*- pole: 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 Generfal 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 Fri Sep 9 09:15:05 2016
@author: barasc
"""
import pandas as pd
from app import ScoValueError
from app import comp
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, UniteEns
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc import sco_tag_module
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.sco_utils import *
class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
"""
Un ResSemBUTTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag.
Il s'appuie principalement sur un ResultatsSemestreBUT.
"""
def __init__(
self,
formsemestre: FormSemestre,
options={"moyennes_tags": True, "moyennes_ue_res_sae": False},
):
"""
Args:
formsemestre: le ``FormSemestre`` sur lequel il se base
options: Un dictionnaire d'options
"""
ResultatsSemestreBUT.__init__(self, formsemestre)
pe_tabletags.TableTag.__init__(self)
# Le nom du res_semestre taggué
self.nom = self.get_repr(verbose=True)
# Les étudiants (etuds, états civils & etudis) ajouté
self.add_etuds(self.etuds)
self.etudids_sorted = sorted(self.etudids)
"""Les etudids des étudiants du ResultatsSemestreBUT triés"""
pe_affichage.pe_print(
f"*** ResSemBUTTag du {self.nom} => {len(self.etudids_sorted)} étudiants"
)
# Les UEs (et les dispenses d'UE)
self.ues_standards: list[UniteEns] = [
ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD
]
"""Liste des UEs standards du ResultatsSemestreBUT"""
# Les parcours des étudiants à ce semestre
self.parcours = []
"""Parcours auxquels sont inscrits les étudiants"""
for etudid in self.etudids_sorted:
parcour = self.formsemestre.etuds_inscriptions[etudid].parcour
if parcour:
self.parcours += [parcour.libelle]
else:
self.parcours += [None]
# Les UEs en fonction des parcours
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
"""Inscription des étudiants aux UEs des parcours"""
# Les acronymes des UEs
self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards}
self.acronymes_sorted = sorted(self.ues_to_acronymes.values())
"""Les acronymes de UE triés par ordre alphabétique"""
# Les compétences associées aux UEs (définies par les acronymes)
self.acronymes_ues_to_competences = {}
"""Association acronyme d'UEs -> compétence"""
for ue in self.ues_standards:
assert ue.niveau_competence, ScoValueError(
"Des UEs ne sont pas rattachées à des compétences"
)
nom = ue.niveau_competence.competence.titre
self.acronymes_ues_to_competences[ue.acronyme] = nom
self.competences_sorted = sorted(
list(set(self.acronymes_ues_to_competences.values()))
)
"""Compétences triées par nom"""
aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")
# Les tags personnalisés et auto:
if "moyennes_tags" in options:
tags_dict = self._get_tags_dict(avec_moyennes_tags=options["moyennes_tags"])
else:
tags_dict = self._get_tags_dict()
pe_affichage.pe_print(
f"""--> {pe_affichage.aff_tags_par_categories(tags_dict)}"""
)
self._check_tags(tags_dict)
# Les coefficients pour le calcul de la moyenne générale, donnés par
# acronymes d'UE
self.matrice_coeffs_moy_gen = self._get_matrice_coeffs(
self.ues_inscr_parcours_df, self.ues_standards
)
"""DataFrame indiquant les coeffs des UEs par ordre alphabétique d'acronyme"""
profils_aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
pe_affichage.pe_print(
f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}"
)
# Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon)
self.capitalisations = self._get_capitalisations(self.ues_standards)
"""DataFrame indiquant les UEs capitalisables d'un étudiant (etudids x )"""
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
"""Moyennes par tags (personnalisés ou 'but')"""
for tag in tags_dict["personnalises"]:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
info_tag = tags_dict["personnalises"][tag]
# Les moyennes générales par UEs
moy_ues_tag = self.compute_moy_ues_tag(info_tag=info_tag, pole=None)
# Mémorise les moyennes
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
pe_moytag.CODE_MOY_UE,
moy_ues_tag,
self.matrice_coeffs_moy_gen,
)
# Ajoute les moyennes par UEs + la moyenne générale (but)
moy_gen = self.compute_moy_gen()
self.moyennes_tags["but"] = pe_moytag.MoyennesTag(
"but",
pe_moytag.CODE_MOY_UE,
moy_gen,
self.matrice_coeffs_moy_gen,
)
# Ajoute la moyenne générale par ressources
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
moy_res_gen = self.compute_moy_ues_tag(
info_tag=None, pole=ModuleType.RESSOURCE
)
self.moyennes_tags["ressources"] = pe_moytag.MoyennesTag(
"ressources",
pe_moytag.CODE_MOY_UE,
moy_res_gen,
self.matrice_coeffs_moy_gen,
)
# Ajoute la moyenne générale par saes
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
moy_saes_gen = self.compute_moy_ues_tag(info_tag=None, pole=ModuleType.SAE)
self.moyennes_tags["saes"] = pe_moytag.MoyennesTag(
"saes",
pe_moytag.CODE_MOY_UE,
moy_saes_gen,
self.matrice_coeffs_moy_gen,
)
# Tous les tags
self.tags_sorted = self.get_all_significant_tags()
"""Tags (personnalisés+compétences) par ordre alphabétique"""
def get_repr(self, verbose=False) -> str:
"""Nom affiché pour le semestre taggué, de la forme (par ex.):
* S1#69 si verbose est False
* S1 FI 2023 si verbose est True
"""
if not verbose:
return f"{self.formsemestre}#{self.formsemestre.formsemestre_id}"
else:
return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def _get_matrice_coeffs(
self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns]
) -> pd.DataFrame:
"""Renvoie un dataFrame donnant les coefficients à appliquer aux UEs
dans le calcul de la moyenne générale (toutes UEs confondues).
Prend en compte l'inscription des étudiants aux UEs en fonction de leur parcours
(cf. ues_inscr_parcours_df).
Args:
ues_inscr_parcours_df: Les inscriptions des étudiants aux UEs
ues_standards: Les UEs standards à prendre en compte
Returns:
Un dataFrame etudids x acronymes_UEs avec les coeffs des UEs
"""
matrice_coeffs_moy_gen = ues_inscr_parcours_df * [
ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé
]
matrice_coeffs_moy_gen.columns = [
self.ues_to_acronymes[ue.id] for ue in ues_standards
]
# Tri par etudids (dim 0) et par acronymes (dim 1)
matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index()
matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index(axis=1)
return matrice_coeffs_moy_gen
def _get_capitalisations(self, ues_standards) -> pd.DataFrame:
"""Renvoie un dataFrame résumant les UEs capitalisables par les
étudiants, d'après les décisions de jury (sous réserve qu'elles existent).
Args:
ues_standards: Liste des UEs standards (notamment autres que le sport)
Returns:
Un dataFrame etudids x acronymes_UEs dont les valeurs sont ``True`` si l'UE
est capitalisable, ``False`` sinon
"""
capitalisations = pd.DataFrame(
False, index=self.etudids_sorted, columns=self.acronymes_sorted
)
self.get_formsemestre_validations() # charge les validations
res_jury = self.validations
if res_jury:
for etud in self.etuds:
etudid = etud.etudid
decisions = res_jury.decisions_jury_ues.get(etudid, {})
for ue in ues_standards:
if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM:
capitalisations.loc[etudid, ue.acronyme] = True
# Tri par etudis et par accronyme d'UE
capitalisations = capitalisations.sort_index()
capitalisations = capitalisations.sort_index(axis=1)
return capitalisations
def compute_moy_ues_tag(
self, info_tag: dict[int, dict] = None, pole=None
) -> pd.DataFrame:
"""Calcule la moyenne par UE des étudiants pour un tag donné,
en ayant connaissance des informations sur le tag.
info_tag détermine les modules pris en compte :
* si non `None`, seuls les modules rattachés au tag sont pris en compte
* si `None`, tous les modules (quelque soit leur rattachement au tag) sont pris
en compte (sert au calcul de la moyenne générale par ressource ou SAE)
`pole` détermine les modules pris en compte :
* si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises
en compte (moyenne de ressources par UEs)
* si `pole` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte
* si `pole` vaut `None` (ou toute autre valeur),
tous les modules sont pris en compte (moyenne d'UEs)
Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag,
et pour chacun leur éventuel coefficient de **repondération**.
Returns:
Le dataframe des moyennes du tag par UE
"""
modimpls_sorted = self.formsemestre.modimpls_sorted
# Adaptation du mask de calcul des moyennes au tag visé
modimpls_mask = []
for modimpl in modimpls_sorted:
module = modimpl.module # Le module
mask = module.ue.type == sco_codes.UE_STANDARD # Est-ce une UE stantard ?
if pole == ModuleType.RESSOURCE:
mask &= module.module_type == ModuleType.RESSOURCE
elif pole == ModuleType.SAE:
mask &= module.module_type == ModuleType.SAE
modimpls_mask += [mask]
# Prise en compte du tag
if info_tag:
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
for i, modimpl in enumerate(modimpls_sorted):
if modimpl.moduleimpl_id not in info_tag:
modimpls_mask[i] = False
# Applique la pondération des coefficients
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
if info_tag:
for modimpl_id in info_tag:
ponderation = info_tag[modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.modimpl_inscr_df,
modimpl_coefs_ponderes_df,
modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
# Ne conserve que les UEs standards
colonnes = [ue.id for ue in self.ues_standards]
moyennes_ues_tag = moyennes_ues_tag[colonnes]
# Transforme les UEs en acronyme
acronymes = [self.ues_to_acronymes[ue.id] for ue in self.ues_standards]
moyennes_ues_tag.columns = acronymes
# Tri par etudids et par ordre alphabétique d'acronyme
moyennes_ues_tag = moyennes_ues_tag.sort_index()
moyennes_ues_tag = moyennes_ues_tag.sort_index(axis=1)
return moyennes_ues_tag
def compute_moy_gen(self):
"""Récupère les moyennes des UEs pour le calcul de la moyenne générale,
en associant à chaque UE.id son acronyme (toutes UEs confondues)
"""
df_ues = pd.DataFrame(
{ue.id: self.etud_moy_ue[ue.id] for ue in self.ues_standards},
index=self.etudids,
)
# Transforme les UEs en acronyme
colonnes = df_ues.columns
acronymes = [self.ues_to_acronymes[col] for col in colonnes]
df_ues.columns = acronymes
# Tri par ordre aphabétique de colonnes
df_ues.sort_index(axis=1)
return df_ues
def _get_tags_dict(self, avec_moyennes_tags=True):
"""Renvoie les tags personnalisés (déduits des modules du semestre)
et les tags automatiques ('but'), et toutes leurs informations,
dans un dictionnaire de la forme :
``{"personnalises": {tag: info_sur_le_tag},
"auto": {tag: {}}``
Returns:
Le dictionnaire structuré des tags ("personnalises" vs. "auto")
"""
dict_tags = {"personnalises": dict(), "auto": dict()}
if avec_moyennes_tags:
# Les tags perso (seulement si l'option d'utiliser les tags perso est choisie)
dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre(
self.formsemestre
)
# Les tags automatiques
# Déduit des compétences
# dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# noms_tags_comp = list(set(dict_ues_competences.values()))
# BUT
dict_tags["auto"] = {"but": {}, "ressources": {}, "saes": {}}
return dict_tags
def _check_tags(self, dict_tags):
"""Vérifie l'unicité des tags"""
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
noms_tags = noms_tags_perso + noms_tags_auto
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
if intersection:
liste_intersection = "\n".join(
[f"<li><code>{tag}</code></li>" for tag in intersection]
)
s = "s" if len(intersection) > 1 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>
contient le{s} tag{s} réservé{s} suivant :
<ul>
{liste_intersection}
</ul>
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(message)
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags renseignés dans le programme pédagogique &
associés aux modules du semestre,
en les associant aux modimpls qui les concernent (modimpl_id) et
au coeff de repondération fournie avec le tag (par défaut 1 si non indiquée)).
Le dictionnaire fournit est de la forme :
``{ tag : { modimplid: {"modimpl": ModImpl,
"ponderation": coeff_de_reponderation}
} }``
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
Return:
Un dictionnaire décrivant les tags
"""
synthese_tags = {}
# Instance des modules du semestre
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
modimpl_id = modimpl.id
# Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id)
# Traitement des tags recensés, chacun pouvant étant de la forme
# "mathématiques", "théorie", "pe:0", "maths:2"
for tag in tags:
# Extraction du nom du tag et du coeff de pondération
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
# Ajout d'une clé pour le tag
if tagname not in synthese_tags:
synthese_tags[tagname] = {}
# Ajout du module (modimpl) au tagname considéré
synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
}
return synthese_tags

406
app/pe/moys/pe_sxtag.py Normal file
View File

@ -0,0 +1,406 @@
# -*- 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 Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.pe import pe_affichage, pe_comp
import app.pe.moys.pe_ressemtag as pe_ressemtag
import pandas as pd
import numpy as np
from app.pe.moys import pe_moytag, pe_tabletags
import app.pe.rcss.pe_trajectoires as pe_trajectoires
from app.scodoc.sco_utils import ModuleType
class SxTag(pe_tabletags.TableTag):
def __init__(
self,
sxtag_id: (str, int),
semx: pe_trajectoires.SemX,
ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
):
"""Calcule les moyennes/classements par tag d'un semestre de type 'Sx'
(par ex. 'S1', 'S2', ...) représentés par acronyme d'UE.
Il représente :
* pour les étudiants *non redoublants* : moyennes/classements
du semestre suivi
* pour les étudiants *redoublants* : une fusion des moyennes/classements
dans les (2) 'Sx' qu'il a suivi, en exploitant les informations de capitalisation :
meilleure moyenne entre l'UE capitalisée et l'UE refaite (la notion de meilleure
s'appliquant à la moyenne d'UE)
Un SxTag (regroupant potentiellement plusieurs semestres) est identifié
par un tuple ``(Sx, fid)`` :
* ``x`` est le rang (semestre_id) du semestre
* ``fid`` le formsemestre_id du semestre final (le plus récent) du regroupement.
Les **tags**, les **UE** et les inscriptions aux UEs (pour les étudiants)
considérés sont uniquement ceux du semestre final.
Args:
sxtag_id: L'identifiant de SxTag
ressembuttags: Un dictionnaire de la forme `{fid: ResSemBUTTag(fid)}` donnant
les semestres à regrouper et les résultats/moyennes par tag des
semestres
"""
pe_tabletags.TableTag.__init__(self)
assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags
self.sxtag_id: (str, int) = sxtag_id
"""Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)"""
assert (
len(self.sxtag_id) == 2
and isinstance(self.sxtag_id[0], str)
and isinstance(self.sxtag_id[1], int)
), "Format de l'identifiant du SxTag non respecté"
self.agregat = sxtag_id[0]
"""Nom de l'aggrégat du RCS"""
self.semx = semx
"""Le SemX sur lequel il s'appuie"""
assert semx.rcs_id == sxtag_id, "Problème de correspondance SxTag/SemX"
# Les resultats des semestres taggués à prendre en compte dans le SemX
self.ressembuttags = {
fid: ressembuttags[fid] for fid in semx.semestres_aggreges
}
"""Les ResSemBUTTags à regrouper dans le SxTag"""
# Les données du semestre final
self.fid_final = sxtag_id[1]
self.ressembuttag_final = ressembuttags[self.fid_final]
"""Le ResSemBUTTag final"""
# Ajoute les etudids et les états civils
self.etuds = self.ressembuttag_final.etuds
"""Les étudiants (extraits du ReSemBUTTag final)"""
self.add_etuds(self.etuds)
self.etudids_sorted = sorted(self.etudids)
"""Les etudids triés"""
# Affichage
pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")
# Les tags
self.tags_sorted = self.ressembuttag_final.tags_sorted
"""Tags (extraits du ReSemBUTTag final)"""
aff_tag = pe_affichage.repr_tags(self.tags_sorted)
pe_affichage.pe_print(f"--> Tags : {aff_tag}")
# Les UE données par leur acronyme
self.acronymes_sorted = self.ressembuttag_final.acronymes_sorted
"""Les acronymes des UEs (extraits du ResSemBUTTag final)"""
# L'association UE-compétences extraites du dernier semestre
self.acronymes_ues_to_competences = (
self.ressembuttag_final.acronymes_ues_to_competences
)
"""L'association acronyme d'UEs -> compétence"""
self.competences_sorted = sorted(self.acronymes_ues_to_competences.values())
"""Les compétences triées par nom"""
aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")
# Les coeffs pour la moyenne générale (traduisant également l'inscription
# des étudiants aux UEs) (etudids_sorted x acronymes_ues_sorted)
self.matrice_coeffs_moy_gen = self.ressembuttag_final.matrice_coeffs_moy_gen
"""La matrice des coeffs pour la moyenne générale"""
aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
pe_affichage.pe_print(
f"--> Moyenne générale calculée avec pour coeffs d'UEs : {aff}"
)
# Masque des inscriptions et des capitalisations
self.masque_df = None
"""Le DataFrame traduisant les capitalisations des différents semestres"""
self.masque_df, masque_cube = compute_masques_capitalisation_cube(
self.etudids_sorted,
self.acronymes_sorted,
self.ressembuttags,
self.fid_final,
)
pe_affichage.aff_capitalisations(
self.etuds,
self.ressembuttags,
self.fid_final,
self.acronymes_sorted,
self.masque_df,
)
# Les moyennes par tag
self.moyennes_tags: dict[str, pd.DataFrame] = {}
"""Moyennes aux UEs (identifiées par leur acronyme) des différents tags"""
if self.tags_sorted:
pe_affichage.pe_print("--> Calcul des moyennes par tags :")
for tag in self.tags_sorted:
pe_affichage.pe_print(f" > MoyTag 👜{tag}")
# Masque des inscriptions aux UEs (extraits de la matrice de coefficients)
inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy())
# Moyennes (tous modules confondus)
if not self.has_notes_tag(tag):
pe_affichage.pe_print(
f" --> Semestre (final) actuellement sans notes"
)
matrice_moys_ues = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
)
else:
# Moyennes tous modules confondus
### Cube de note etudids x UEs tous modules confondus
notes_df_gen, notes_cube_gen = self.compute_notes_ues_cube(tag)
# DataFrame des moyennes (tous modules confondus)
matrice_moys_ues = self.compute_notes_ues(
notes_cube_gen, masque_cube, inscr_mask
)
# Mémorise les infos pour la moyenne au tag
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
pe_moytag.CODE_MOY_UE,
matrice_moys_ues,
self.matrice_coeffs_moy_gen,
)
# Affichage de debug
aff = pe_affichage.repr_profil_coeffs(
self.matrice_coeffs_moy_gen, with_index=True
)
pe_affichage.pe_print(f" > Moyenne générale calculée avec : {aff}")
def has_notes_tag(self, tag):
"""Détermine si le SxTag, pour un tag donné, est en cours d'évaluation.
Si oui, n'a pas (encore) de notes dans le resformsemestre final.
Args:
tag: Le tag visé
Returns:
True si a des notes, False sinon
"""
moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag]
return moy_tag_dernier_sem.has_notes()
def __eq__(self, other):
"""Egalité de 2 SxTag sur la base de leur identifiant"""
return self.sxtag_id == other.sxtag_id
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
if verbose:
return f"SXTag basé sur {self.semx.get_repr()}"
else:
# affichage = [str(fid) for fid in self.ressembuttags]
return f"SXTag {self.agregat}#{self.fid_final}"
def compute_notes_ues_cube(self, tag) -> (pd.DataFrame, np.array):
"""Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé)
nécessaire au calcul des moyennes du tag pour le RCS Sx.
(Renvoie également le dataframe associé pour debug).
Args:
tag: Le tag considéré (personalisé ou "but")
"""
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
# etudids_sorted = etudids_sorted
# acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
semestres_id = list(self.ressembuttags.keys())
dfs = {}
for frmsem_id in semestres_id:
# Partant d'un dataframe vierge
df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
)
# Charge les notes du semestre tag
sem_tag = self.ressembuttags[frmsem_id]
moys_tag = sem_tag.moyennes_tags[tag]
notes = moys_tag.matrice_notes_gen # dataframe etudids x ues
# les étudiants et les acronymes communs
etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
df, notes
)
# Recopie
df.loc[etudids_communs, acronymes_communs] = notes.loc[
etudids_communs, acronymes_communs
]
# Supprime tout ce qui n'est pas numérique
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube etudids x ues x semestres"""
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
return dfs, etudids_x_ues_x_semestres
def compute_notes_ues(
self,
set_cube: np.array,
masque_cube: np.array,
inscr_mask: np.array,
) -> pd.DataFrame:
"""Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
par UE) obtenue par un étudiant à un semestre.
Args:
set_cube: notes moyennes aux modules ndarray
(semestre_ids x etudids x UEs), des floats avec des NaN
masque_cube: masque indiquant si la note doit être prise en compte ndarray
(semestre_ids x etudids x UEs), des 1.0 ou des 0.0
inscr_mask: masque etudids x UE traduisant les inscriptions des
étudiants aux UE (du semestre terminal)
Returns:
Un DataFrame avec pour columns les moyennes par ues,
et pour rows les etudid
"""
# etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid
# acronymes_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme
nb_etuds, nb_ues, nb_semestres = set_cube.shape
nb_etuds_mask, nb_ues_mask = inscr_mask.shape
# assert nb_etuds == len(self.etudids_sorted)
# assert nb_ues == len(self.acronymes_sorted)
# assert nb_etuds == nb_etuds_mask
# assert nb_ues == nb_ues_mask
# Entrées à garder dans le cube en fonction du masque d'inscription aux UEs du parcours
inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1)
set_cube = set_cube * inscr_mask_3D
# Entrées à garder en fonction des UEs capitalisées ou non
set_cube = set_cube * masque_cube
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0
set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)
# Les moyennes par ues
# TODO: Pour l'instant un max sans prise en compte des UE capitalisées
etud_moy = np.max(set_cube_no_nan, axis=2)
# Fix les max non calculé -1 -> NaN
etud_moy[etud_moy < 0] = np.NaN
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy,
index=self.etudids_sorted, # les etudids
columns=self.acronymes_sorted, # les acronymes d'UEs
)
etud_moy_tag_df = etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df
def compute_masques_capitalisation_cube(
etudids_sorted: list[int],
acronymes_sorted: list[str],
ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
formsemestre_id_final: int,
) -> (pd.DataFrame, np.array):
"""Construit le cube traduisant les masques des UEs à prendre en compte dans le calcul
des moyennes, en utilisant le dataFrame de capitalisations de chaque ResSemBUTTag
Ces masques contiennent : 1 si la note doit être prise en compte, 0 sinon
Le masque des UEs à prendre en compte correspondant au semestre final (identifié par
son formsemestre_id_final) est systématiquement à 1 (puisque les résultats
de ce semestre doivent systématiquement
être pris en compte notamment pour les étudiants non redoublant).
Args:
etudids_sorted: La liste des etudids triés par ordre croissant (dim 0)
acronymes_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1)
ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus)
formsemestre_id_final: L'identifiant du formsemestre_id_final
"""
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
# etudids_sorted = etudids_sorted
# acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
semestres_id = list(ressembuttags.keys())
dfs = {}
for frmsem_id in semestres_id:
# Partant d'un dataframe contenant des 1.0
if frmsem_id == formsemestre_id_final:
df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_sorted)
else: # semestres redoublés
df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_sorted)
# Traitement des capitalisations : remplace les infos de capitalisations par les coeff 1 ou 0
capitalisations = ressembuttags[frmsem_id].capitalisations
capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0)
# Met à 0 les coeffs des UEs non capitalisées pour les étudiants
# inscrits dans les 2 semestres: 1.0*False => 0.0
etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
df, capitalisations
)
df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[
etudids_communs, acronymes_communs
]
# Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube etudids x ues x semestres"""
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
return dfs, etudids_x_ues_x_semestres

203
app/pe/moys/pe_tabletags.py Normal file
View File

@ -0,0 +1,203 @@
# -*- pole: 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 Thu Sep 8 09:36:33 2016
@author: barasc
"""
import pandas as pd
from app.models import Identite
from app.pe.moys import pe_moytag
TAGS_RESERVES = ["but"]
CHAMPS_ADMINISTRATIFS = ["Civilité", "Nom", "Prénom"]
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
# Les étudiants
# self.etuds: list[Identite] = None # A venir
"""Les étudiants"""
# self.etudids: list[int] = {}
"""Les etudids"""
def add_etuds(self, etuds: list[Identite]):
"""Mémorise les informations sur les étudiants
Args:
etuds: la liste des identités de l'étudiant
"""
# self.etuds = etuds
self.etudids = list({etud.etudid for etud in etuds})
def get_all_significant_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique,
extraite des clés du dictionnaire ``moyennes_tags``, en ne
considérant que les moyennes ayant des notes.
Returns:
Liste de tags triés par ordre alphabétique
"""
tags = []
tag: str = ""
moytag: pe_moytag.MoyennesTag = None
for tag, moytag in self.moyennes_tags.items():
if moytag.has_notes():
tags.append(tag)
return sorted(tags)
def to_df(
self,
administratif=True,
aggregat=None,
tags_cibles=None,
cohorte=None,
options={"min_max_moy": True},
) -> pd.DataFrame:
"""Renvoie un dataframe listant toutes les données
des moyennes/classements/nb_inscrits/min/max/moy
des étudiants aux différents tags.
tags_cibles limitent le dataframe aux tags indiqués
type_colonnes indiquent si les colonnes doivent être passées en multiindex
Args:
administratif: Indique si les données administratives sont incluses
aggregat: l'aggrégat représenté
tags_cibles: la liste des tags ciblés
cohorte: la cohorte représentée
Returns:
Le dataframe complet de synthèse
"""
if not self.is_significatif():
return None
# Les tags visés
tags_tries = self.get_all_significant_tags()
if not tags_cibles:
tags_cibles = tags_tries
tags_cibles = sorted(tags_cibles)
# Les tags visés avec des notes
# Les étudiants visés
if administratif:
df = df_administratif(self.etuds, aggregat=aggregat, cohorte=cohorte)
else:
df = pd.DataFrame(index=self.etudids)
# Ajout des données par tags
for tag in tags_cibles:
if tag in self.moyennes_tags:
moy_tag_df = self.moyennes_tags[tag].to_df(
aggregat=aggregat, cohorte=cohorte, options=options
)
df = df.join(moy_tag_df)
# Tri par nom, prénom
if administratif:
colonnes_tries = [
_get_champ_administratif(champ, aggregat=aggregat, cohorte=cohorte)
for champ in CHAMPS_ADMINISTRATIFS[1:]
] # Nom + Prénom
df = df.sort_values(by=colonnes_tries)
return df
def has_etuds(self):
"""Indique si un tabletag contient des étudiants"""
return len(self.etuds) > 0
def is_significatif(self):
"""Indique si une tabletag a des données"""
# A des étudiants
if not self.etuds:
return False
# A des tags avec des notes
tags_tries = self.get_all_significant_tags()
if not tags_tries:
return False
return True
def _get_champ_administratif(champ, aggregat=None, cohorte=None):
"""Pour un champ donné, renvoie l'index (ou le multindex)
à intégrer au dataframe"""
liste = []
if aggregat != None:
liste += [aggregat]
liste += ["Administratif", "Identité"]
if cohorte != None:
liste += [champ]
liste += [champ]
return "|".join(liste)
def df_administratif(
etuds: list[Identite], aggregat=None, cohorte=None
) -> pd.DataFrame:
"""Renvoie un dataframe donnant les données administratives
des étudiants du TableTag
Args:
etuds: Identité des étudiants générant les données administratives
"""
identites = {etud.etudid: etud for etud in etuds}
donnees = {}
etud: Identite = None
for etudid, etud in identites.items():
data = {
CHAMPS_ADMINISTRATIFS[0]: etud.civilite_str,
CHAMPS_ADMINISTRATIFS[1]: etud.nom,
CHAMPS_ADMINISTRATIFS[2]: etud.prenom_str,
}
donnees[etudid] = {
_get_champ_administratif(champ, aggregat, cohorte): data[champ]
for champ in CHAMPS_ADMINISTRATIFS
}
colonnes = [
_get_champ_administratif(champ, aggregat, cohorte)
for champ in CHAMPS_ADMINISTRATIFS
]
df = pd.DataFrame.from_dict(donnees, orient="index", columns=colonnes)
df = df.sort_values(by=colonnes[1:])
return df

View File

@ -8,6 +8,7 @@
from flask import g from flask import g
from app import log from app import log
from app.pe.rcss import pe_rcs
PE_DEBUG = False PE_DEBUG = False
@ -20,16 +21,18 @@ def pe_start_log() -> list[str]:
return g.scodoc_pe_log return g.scodoc_pe_log
def pe_print(*a): def pe_print(*a, **cles):
"Log (or print in PE_DEBUG mode) and store in g" "Log (or print in PE_DEBUG mode) and store in g"
if PE_DEBUG:
msg = " ".join(a)
print(msg)
else:
lines = getattr(g, "scodoc_pe_log") lines = getattr(g, "scodoc_pe_log")
if lines is None: if lines is None:
lines = pe_start_log() lines = pe_start_log()
msg = " ".join(a) msg = " ".join(a)
lines.append(msg) lines.append(msg)
if PE_DEBUG: if "info" in cles:
print(msg)
else:
log(msg) log(msg)
@ -40,5 +43,192 @@ def pe_get_log() -> str:
# Affichage dans le tableur pe en cas d'absence de notes # Affichage dans le tableur pe en cas d'absence de notes
SANS_NOTE = "-" SANS_NOTE = "-"
NOM_STAT_GROUPE = "statistiques du groupe"
NOM_STAT_PROMO = "statistiques de la promo"
def repr_profil_coeffs(matrice_coeffs_moy_gen, with_index=False):
"""Affiche les différents types de coefficients (appelés profil)
d'une matrice_coeffs_moy_gen (pour debug)
"""
# Les profils des coeffs d'UE (pour debug)
profils = []
index_a_profils = {}
for i in matrice_coeffs_moy_gen.index:
val = matrice_coeffs_moy_gen.loc[i].fillna("-")
val = " | ".join([str(v) for v in val])
if val not in profils:
profils += [val]
index_a_profils[val] = [str(i)]
else:
index_a_profils[val] += [str(i)]
# L'affichage
if len(profils) > 1:
if with_index:
elmts = [
" " * 10
+ prof
+ " (par ex. "
+ ", ".join(index_a_profils[prof][:10])
+ ")"
for prof in profils
]
else:
elmts = [" " * 10 + prof for prof in profils]
profils_aff = "\n" + "\n".join(elmts)
else:
profils_aff = "\n".join(profils)
return profils_aff
def repr_asso_ue_comp(acronymes_ues_to_competences):
"""Représentation textuelle de l'association UE -> Compétences
fournies dans acronymes_ues_to_competences
"""
champs = acronymes_ues_to_competences.keys()
champs = sorted(champs)
aff_comp = []
for acro in champs:
aff_comp += [f"📍{acro} (∈ 💡{acronymes_ues_to_competences[acro]})"]
return ", ".join(aff_comp)
def aff_UEs(champs):
"""Représentation textuelle des UEs fournies dans `champs`"""
champs_tries = sorted(champs)
aff_comp = []
for comp in champs_tries:
aff_comp += ["📍" + comp]
return ", ".join(aff_comp)
def aff_competences(champs):
"""Affiche les compétences"""
champs_tries = sorted(champs)
aff_comp = []
for comp in champs_tries:
aff_comp += ["💡" + comp]
return ", ".join(aff_comp)
def repr_tags(tags):
"""Affiche les tags"""
tags_tries = sorted(tags)
aff_tag = ["👜" + tag for tag in tags_tries]
return ", ".join(aff_tag)
def aff_tags_par_categories(dict_tags):
"""Etant donné un dictionnaire de tags, triés
par catégorie (ici "personnalisés" ou "auto")
représentation textuelle des tags
"""
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
if noms_tags_perso:
aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso])
aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto])
return f"Tags du programme de formation : {aff_tags_perso} + Automatiques : {aff_tags_auto}"
else:
aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto])
return f"Tags automatiques {aff_tags_auto} (aucun tag personnalisé)"
# Affichage
def aff_trajectoires_suivies_par_etudiants(etudiants):
"""Affiche les trajectoires (regroupement de (form)semestres)
amenant un étudiant du S1 à un semestre final"""
# Affichage pour debug
etudiants_ids = etudiants.etudiants_ids
jeunes = list(enumerate(etudiants_ids))
for no_etud, etudid in jeunes:
etat = "" if etudid in etudiants.abandons_ids else ""
pe_print(f"--> {etat} {etudiants.identites[etudid].nomprenom} (#{etudid}) :")
trajectoires = etudiants.trajectoires[etudid]
for nom_rcs, rcs in trajectoires.items():
if rcs:
pe_print(f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}")
def aff_semXs_suivis_par_etudiants(etudiants):
"""Affiche les SemX (regroupement de semestres de type Sx)
amenant un étudiant à valider un Sx"""
etudiants_ids = etudiants.etudiants_ids
jeunes = list(enumerate(etudiants_ids))
for no_etud, etudid in jeunes:
etat = "" if etudid in etudiants.abandons_ids else ""
pe_print(f"--> {etat} {etudiants.identites[etudid].nomprenom} :")
for nom_rcs, rcs in etudiants.semXs[etudid].items():
if rcs:
pe_print(f" > SemX ⏯️{nom_rcs}: {rcs.get_repr()}")
vides = []
for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES:
les_semX_suivis = []
for no_etud, etudid in jeunes:
if etudiants.semXs[etudid][nom_rcs]:
les_semX_suivis.append(etudiants.semXs[etudid][nom_rcs])
if not les_semX_suivis:
vides += [nom_rcs]
vides = sorted(list(set(vides)))
pe_print(f"⚠️ SemX sans données : {', '.join(vides)}")
def aff_capitalisations(etuds, ressembuttags, fid_final, acronymes_sorted, masque_df):
"""Affichage des capitalisations du sxtag pour debug"""
aff_cap = []
for etud in etuds:
cap = []
for frmsem_id in ressembuttags:
if frmsem_id != fid_final:
for accr in acronymes_sorted:
if masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0:
cap += [accr]
if cap:
aff_cap += [f" > {etud.nomprenom} : {', '.join(cap)}"]
if aff_cap:
pe_print(f"--> ⚠️ Capitalisations :")
pe_print("\n".join(aff_cap))
def repr_comp_et_ues(acronymes_ues_to_competences):
"""Affichage pour debug"""
aff_comp = []
competences_sorted = sorted(acronymes_ues_to_competences.keys())
for comp in competences_sorted:
liste = []
for acro in acronymes_ues_to_competences:
if acronymes_ues_to_competences[acro] == comp:
liste += ["📍" + acro]
aff_comp += [f" 💡{comp} (⇔ {', '.join(liste)})"]
return "\n".join(aff_comp)
def aff_rcsemxs_suivis_par_etudiants(etudiants):
"""Affiche les RCSemX (regroupement de SemX)
amenant un étudiant du S1 à un Sx"""
etudiants_ids = etudiants.etudiants_ids
jeunes = list(enumerate(etudiants_ids))
for no_etud, etudid in jeunes:
etat = "" if etudid in etudiants.abandons_ids else ""
pe_print(f"-> {etat} {etudiants.identites[etudid].nomprenom} :")
for nom_rcs, rcs in etudiants.rcsemXs[etudid].items():
if rcs:
pe_print(f" > RCSemX ⏯️{nom_rcs}: {rcs.get_repr()}")
vides = []
for nom_rcs in pe_rcs.TOUS_LES_RCS:
les_rcssemX_suivis = []
for no_etud, etudid in jeunes:
if etudiants.rcsemXs[etudid][nom_rcs]:
les_rcssemX_suivis.append(etudiants.rcsemXs[etudid][nom_rcs])
if not les_rcssemX_suivis:
vides += [nom_rcs]
vides = sorted(list(set(vides)))
pe_print(f"⚠️ RCSemX vides : {', '.join(vides)}")

View File

@ -41,13 +41,13 @@ import datetime
import re import re
import unicodedata import unicodedata
import pandas as pd
from flask import g from flask import g
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models import FormSemestre from app.models import FormSemestre
from app.pe.pe_rcs import TYPES_RCS from app.pe.rcss.pe_rcs import TYPES_RCS
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
@ -284,3 +284,56 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
cosemestres[fid] = cosem cosemestres[fid] = cosem
return cosemestres return cosemestres
def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]):
"""Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un
dictionnaire {rang: [liste des semestres du dit rang]}"""
cosemestres_tries = {}
for sem in cosemestres.values():
cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(
sem.semestre_id, []
) + [sem]
return cosemestres_tries
def find_index_and_columns_communs(
df1: pd.DataFrame, df2: pd.DataFrame
) -> (list, list):
"""Partant de 2 DataFrames ``df1`` et ``df2``, renvoie les indices de lignes
et de colonnes, communes aux 2 dataframes
Args:
df1: Un dataFrame
df2: Un dataFrame
Returns:
Le tuple formé par la liste des indices de lignes communs et la liste des indices
de colonnes communes entre les 2 dataFrames
"""
indices1 = df1.index
indices2 = df2.index
indices_communs = list(df1.index.intersection(df2.index))
colonnes1 = df1.columns
colonnes2 = df2.columns
colonnes_communes = list(set(colonnes1) & set(colonnes2))
return indices_communs, colonnes_communes
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

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # Gestion scolarite IUT
# #
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2024 Emmanuel Viennet. c All rights reserved.
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -37,8 +37,10 @@ Created on 17/01/2024
""" """
import pandas as pd import pandas as pd
from app import ScoValueError
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.pe.rcss import pe_rcs
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
@ -55,16 +57,20 @@ class EtudiantsJuryPE:
self.annee_diplome = annee_diplome self.annee_diplome = annee_diplome
"""L'année du diplôme""" """L'année du diplôme"""
self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT self.identites: dict[int:Identite] = {} # ex. ETUDINFO_DICT
"Les identités des étudiants traités pour le jury" """Les identités des étudiants traités pour le jury"""
self.cursus: dict[int, dict] = {} self.cursus: dict[int:dict] = {}
"Les cursus (semestres suivis, abandons) des étudiants" """Les cursus (semestres suivis, abandons) des étudiants"""
self.trajectoires = {} self.trajectoires: dict[int:dict] = {}
"""Les trajectoires/chemins de semestres suivis par les étudiants """Les trajectoires (regroupement cohérents 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.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 = {} self.etudiants_diplomes = {}
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement """Les identités des étudiants à considérer au jury (ceux qui seront effectivement
@ -99,27 +105,26 @@ class EtudiantsJuryPE:
self.cosemestres = cosemestres self.cosemestres = cosemestres
pe_affichage.pe_print( pe_affichage.pe_print(
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés" f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés", info=True
) )
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( pe_affichage.pe_print(
f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres" "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 # Analyse des parcours étudiants pour déterminer leur année effective de diplome
# avec prise en compte des redoublements, des abandons, .... # avec prise en compte des redoublements, des abandons, ....
pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants") pe_affichage.pe_print(
"3) Analyse des parcours individuels des étudiants", info=True
)
for etudid in self.etudiants_ids: # Ajoute une liste d'étudiants
self.identites[etudid] = Identite.get_etud(etudid) self.add_etudiants(etudiants_ids)
# 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 # 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()
@ -134,23 +139,35 @@ class EtudiantsJuryPE:
# 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(f"4) Bilan", info=True)
pe_affichage.pe_print( pe_affichage.pe_print(
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}",
info=True,
) )
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
assert nbre_abandons == len(self.abandons_ids) assert nbre_abandons == len(self.abandons_ids)
pe_affichage.pe_print( pe_affichage.pe_print(
f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon" f"--> {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)"
) )
# pe_affichage.pe_print(
# " => quelques étudiants futurs diplômés : " def add_etudiants(self, etudiants_ids):
# + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]]) """Ajoute une liste d'étudiants aux données du jury"""
# ) nbre_etudiants_ajoutes = 0
# pe_affichage.pe_print( for etudid in etudiants_ids:
# " => semestres dont il faut calculer les moyennes : " if etudid not in self.identites:
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) 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]: def get_etudiants_diplomes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
@ -198,8 +215,11 @@ class EtudiantsJuryPE:
* à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
avec son nom, prénom, etc... avec son nom, prénom, etc...
* à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme)
route (cf. clé abandon) 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: Args:
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
@ -217,11 +237,19 @@ class EtudiantsJuryPE:
if formsemestre.formation.is_apc() 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] = { self.cursus[etudid] = {
"etudid": etudid, # les infos sur l'étudiant "etudid": etudid, # les infos sur l'étudiant
"etat_civil": identite.etat_civil, # Ajout à la table jury "etat_civil": identite.etat_civil, # Ajout à la table jury
"nom": identite.nom, "nom": identite.nom,
"entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT "entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT
"parcours": libelle, # Le parcours final
"diplome": get_annee_diplome( "diplome": get_annee_diplome(
identite identite
), # Le date prévisionnelle de son diplôme ), # Le date prévisionnelle de son diplôme
@ -232,35 +260,24 @@ class EtudiantsJuryPE:
"abandon": False, # va être traité en dessous "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 ? # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
dernier_semes_etudiant = formsemestres[0] dernier_semes_etudiant = formsemestres[0]
res = load_formsemestre_results(dernier_semes_etudiant) res = load_formsemestre_results(dernier_semes_etudiant)
etud_etat = res.get_etud_etat(etudid) etud_etat = res.get_etud_etat(etudid)
if etud_etat == scu.DEMISSION: if etud_etat == scu.DEMISSION:
self.cursus[etudid]["abandon"] |= True self.cursus[etudid]["abandon"] = True
else: else:
# Est-il réorienté ou a-t-il arrêté volontairement sa formation ? # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ?
self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres) self.cursus[etudid]["abandon"] = arret_de_formation(
identite, cosemestres
)
def get_semestres_significatifs(self, etudid: int): # Initialise ses trajectoires/SemX/RCSemX
"""Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé self.trajectoires[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
l'année visée (supprime les semestres qui conduisent à une diplomation self.semXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_SEMESTRES}
postérieure à celle du jury visé) self.rcsemXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
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): def structure_cursus_etudiant(self, etudid: int):
"""Structure les informations sur les semestres suivis par un """Structure les informations sur les semestres suivis par un
@ -269,9 +286,11 @@ class EtudiantsJuryPE:
Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke : 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). 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. Ce semestre influera les interclassements par semestre dans la promo.
""" """
semestres_significatifs = self.get_semestres_significatifs(etudid) semestres_significatifs = get_semestres_significatifs(
self.cursus[etudid]["formsemestres"], self.annee_diplome
)
# Tri des semestres par numéro de semestre # Tri des semestres par numéro de semestre
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
@ -283,12 +302,10 @@ class EtudiantsJuryPE:
} }
self.cursus[etudid][f"S{i}"] = semestres_i self.cursus[etudid][f"S{i}"] = semestres_i
def get_formsemestres_terminaux_aggregat( def get_formsemestres_finals_des_rcs(self, nom_rcs: str) -> dict[int, FormSemestre]:
self, aggregat: str """Pour un nom de RCS donné, ensemble des formsemestres finals possibles
) -> dict[int, FormSemestre]: pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3.
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat Les formsemestres finals obtenus traduisent :
(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é * 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 (par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
@ -299,14 +316,14 @@ class EtudiantsJuryPE:
renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session) renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)
Args: Args:
aggregat: L'aggrégat nom_rcs: Le nom du RCS (parmi Sx, xA, xS)
Returns: Returns:
Un dictionnaire ``{fid: FormSemestre(fid)}`` Un dictionnaire ``{fid: FormSemestre(fid)}``
""" """
formsemestres_terminaux = {} formsemestres_terminaux = {}
for trajectoire_aggr in self.trajectoires.values(): for trajectoire_aggr in self.cursus.values():
trajectoire = trajectoire_aggr[aggregat] trajectoire = trajectoire_aggr[nom_rcs]
if trajectoire: if trajectoire:
# Le semestre terminal de l'étudiant de l'aggrégat # Le semestre terminal de l'étudiant de l'aggrégat
fid = trajectoire.formsemestre_final.formsemestre_id fid = trajectoire.formsemestre_final.formsemestre_id
@ -345,7 +362,9 @@ class EtudiantsJuryPE:
etudiant = self.identites[etudid] etudiant = self.identites[etudid]
cursus = self.cursus[etudid] cursus = self.cursus[etudid]
formsemestres = cursus["formsemestres"] formsemestres = cursus["formsemestres"]
parcours = cursus["parcours"]
if not parcours:
parcours = ""
if cursus["diplome"]: if cursus["diplome"]:
diplome = cursus["diplome"] diplome = cursus["diplome"]
else: else:
@ -359,6 +378,7 @@ class EtudiantsJuryPE:
"Prenom": etudiant.prenom, "Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str, "Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance), "Age": pe_comp.calcul_age(etudiant.date_naissance),
"Parcours": parcours,
"Date entree": cursus["entree"], "Date entree": cursus["entree"],
"Date diplome": diplome, "Date diplome": diplome,
"Nb semestres": len(formsemestres), "Nb semestres": len(formsemestres),
@ -376,13 +396,35 @@ class EtudiantsJuryPE:
return df 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: def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
"""Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)
inscrits à l'un des semestres de la liste de ``semestres``. 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: Args:
semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un
ensemble d'identifiant de semestres ensemble d'identifiant de semestres
@ -430,7 +472,7 @@ def get_annee_diplome(etud: Identite) -> int | None:
def get_semestres_apc(identite: Identite) -> list: def get_semestres_apc(identite: Identite) -> list:
"""Liste des semestres d'un étudiant qui corresponde à une formation APC. """Liste des semestres d'un étudiant qui correspondent à une formation APC.
Args: Args:
identite: L'identité d'un étudiant identite: L'identité d'un étudiant
@ -446,8 +488,8 @@ def get_semestres_apc(identite: Identite) -> list:
return semestres_apc return semestres_apc
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: def arret_de_formation(etud: Identite, cosemestres: dict[int, 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 (volontairement ou non). Il peut s'agir :
* d'une réorientation à l'initiative du jury de semestre ou d'une démission * 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 (on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
@ -458,7 +500,8 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas 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) dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
connu dans Scodoc. 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), 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'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
@ -485,7 +528,6 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? 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 pour le cas des étudiants à l'étranger
TODO:: A reprendre si BUT avec semestres décalés
""" """
# Les semestres APC de l'étudiant # Les semestres APC de l'étudiant
semestres = get_semestres_apc(etud) semestres = get_semestres_apc(etud)
@ -493,61 +535,54 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
if not semestres_apc: if not semestres_apc:
return True return True
# Son dernier semestre APC en date # Le dernier semestre de l'étudiant
dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) dernier_formsemestre = semestres[0]
numero_dernier_formsemestre = dernier_formsemestre.semestre_id rang_dernier_semestre = dernier_formsemestre.semestre_id
# Les numéro de semestres possible dans lesquels il pourrait s'incrire # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
if numero_dernier_formsemestre % 2 == 1: cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres)
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 ? cosemestres_superieurs = {}
formsestres_superieurs_possibles = [] for rang in cosemestres_tries_par_rang:
for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits if rang > rang_dernier_semestre:
if ( cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]
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
# Si pas d'autres cosemestres postérieurs
if not cosemestres_superieurs:
return False 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
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: # Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire rangs = sorted(etat_inscriptions.keys())
de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``. 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})."
)
Args: # Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire
semestres: Un dictionnaire de semestres 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: return est_demissionnaire
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
def etapes_du_cursus( def etapes_du_cursus(
@ -613,6 +648,6 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
f"{semestre.date_debut.year}-{semestre.date_fin.year}", f"{semestre.date_debut.year}-{semestre.date_fin.year}",
] ]
if avec_fid: if avec_fid:
description.append(f"({semestre.formsemestre_id})") description.append(f"(#{semestre.formsemestre_id})")
return " ".join(description) return " ".join(description)

View File

@ -1,160 +0,0 @@
##############################################################################
#
# 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_etudiant import EtudiantsJuryPE
from app.pe.pe_rcs import RCS, RCSsJuryPE
from app.pe.pe_rcstag import RCSTag
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__(
self,
nom_rcs: str,
etudiants: EtudiantsJuryPE,
rcss_jury_pe: RCSsJuryPE,
rcss_tags: dict[tuple, RCSTag],
):
TableTag.__init__(self)
self.nom_rcs = nom_rcs
"""Le nom du RCS interclassé"""
self.nom = self.get_repr()
"""Les étudiants diplômés et leurs rcss""" # TODO
self.diplomes_ids = etudiants.etudiants_diplomes
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
# pour les exports sous forme de dataFrame
self.etudiants = {
etudid: etudiants.identites[etudid].etat_civil
for etudid in self.diplomes_ids
}
# Les trajectoires (et leur version tagguées), en ne gardant que
# celles associées à l'aggrégat
self.rcss: dict[int, RCS] = {}
"""Ensemble des trajectoires associées à l'aggrégat"""
for trajectoire_id in rcss_jury_pe.rcss:
trajectoire = rcss_jury_pe.rcss[trajectoire_id]
if trajectoire_id[0] == nom_rcs:
self.rcss[trajectoire_id] = trajectoire
self.trajectoires_taggues: dict[int, RCS] = {}
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
for trajectoire_id in self.rcss:
self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
# celles associées aux diplomés
self.suivi: dict[int, RCS] = {}
"""Association entre chaque étudiant et la trajectoire tagguée à prendre en
compte pour l'aggrégat"""
for etudid in self.diplomes_ids:
self.suivi[etudid] = rcss_jury_pe.suivi[etudid][nom_rcs]
self.tags_sorted = self.do_taglist()
"""Liste des tags (triés par ordre alphabétique)"""
# Construit la matrice de notes
self.notes = self.compute_notes_matrice()
"""Matrice des notes de l'aggrégat"""
# Synthétise les moyennes/classements par tag
self.moyennes_tags: dict[str, MoyenneTag] = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
# Est significatif ? (aka a-t-il des tags et des notes)
self.significatif = len(self.tags_sorted) > 0
def get_repr(self) -> str:
"""Une représentation textuelle"""
return f"Aggrégat {self.nom_rcs}"
def do_taglist(self):
"""Synthétise les tags à partir des trajectoires_tagguées
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
for trajectoire in self.trajectoires_taggues.values():
tags.extend(trajectoire.tags_sorted)
return sorted(set(tags))
def compute_notes_matrice(self):
"""Construit la matrice de notes (etudid x tags)
retraçant les moyennes obtenues par les étudiants dans les semestres associés à
l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat).
"""
# nb_tags = len(self.tags_sorted) unused ?
# nb_etudiants = len(self.diplomes_ids)
# Index de la matrice (etudids -> dim 0, tags -> dim 1)
etudids = list(self.diplomes_ids)
tags = self.tags_sorted
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
for trajectoire in self.trajectoires_taggues.values():
# Charge les moyennes par tag de la trajectoire tagguée
notes = trajectoire.notes
# Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées
etudids_communs = df.index.intersection(notes.index)
tags_communs = df.columns.intersection(notes.columns)
# Injecte les notes par tag
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
return df

File diff suppressed because it is too large Load Diff

View File

@ -1,269 +0,0 @@
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on 01-2024
@author: barasc
"""
import app.pe.pe_comp as pe_comp
from app.models import FormSemestre
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
TYPES_RCS = {
"S1": {
"aggregat": ["S1"],
"descr": "Semestre 1 (S1)",
},
"S2": {
"aggregat": ["S2"],
"descr": "Semestre 2 (S2)",
},
"1A": {
"aggregat": ["S1", "S2"],
"descr": "BUT1 (S1+S2)",
},
"S3": {
"aggregat": ["S3"],
"descr": "Semestre 3 (S3)",
},
"S4": {
"aggregat": ["S4"],
"descr": "Semestre 4 (S4)",
},
"2A": {
"aggregat": ["S3", "S4"],
"descr": "BUT2 (S3+S4)",
},
"3S": {
"aggregat": ["S1", "S2", "S3"],
"descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)",
},
"4S": {
"aggregat": ["S1", "S2", "S3", "S4"],
"descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)",
},
"S5": {
"aggregat": ["S5"],
"descr": "Semestre 5 (S5)",
},
"S6": {
"aggregat": ["S6"],
"descr": "Semestre 6 (S6)",
},
"3A": {
"aggregat": ["S5", "S6"],
"descr": "3ème année (S5+S6)",
},
"5S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
"descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)",
},
"6S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
},
}
"""Dictionnaire détaillant les différents regroupements cohérents
de semestres (RCS), en leur attribuant un nom et en détaillant
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
dans les tableurs de synthèse.
"""
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
TOUS_LES_RCS = list(TYPES_RCS.keys())
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
class RCS:
"""Modélise un ensemble de semestres d'étudiants
associé à un type de regroupement cohérent de semestres
donné (par ex: 'S2', '3S', '2A').
Si le RCS est un semestre de type Si, stocke le (ou les)
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
Args:
nom_rcs: Un nom du RCS (par ex: '5S')
semestre_final: Le semestre final du RCS
"""
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
self.nom = nom_rcs
"""Nom du RCS"""
self.formsemestre_final = semestre_final
"""FormSemestre terminal du RCS"""
self.rcs_id = (nom_rcs, semestre_final.formsemestre_id)
"""Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)"""
self.semestres_aggreges = {}
"""Semestres regroupés dans le RCS"""
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
"""Ajout de semestres aux semestres à regrouper
Args:
semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter
"""
self.semestres_aggreges = self.semestres_aggreges | semestres
def get_repr(self, verbose=True) -> str:
"""Représentation textuelle d'un RCS
basé sur ses semestres aggrégés"""
noms = []
for fid in self.semestres_aggreges:
semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}({fid})")
noms = sorted(noms)
title = f"""{self.nom} ({
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
if verbose and noms:
title += " - " + "+".join(noms)
return title
class RCSsJuryPE:
"""Classe centralisant toutes les regroupements cohérents de
semestres (RCS) des étudiants à prendre en compte dans un jury PE
Args:
annee_diplome: L'année de diplomation
"""
def __init__(self, annee_diplome: int):
self.annee_diplome = annee_diplome
"""Année de diplômation"""
self.rcss: dict[tuple:RCS] = {}
"""Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCS}"""
self.suivi: dict[int:str] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
son RCS : {etudid: {nom_RCS: RCS}}"""
def cree_rcss(self, etudiants: EtudiantsJuryPE):
"""Créé tous les RCS, au regard du cursus des étudiants
analysés + les mémorise dans les données de l'étudiant
Args:
etudiants: Les étudiants à prendre en compte dans le Jury PE
"""
for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM:
# L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre
# terminal (par ex: S3) et son numéro (par ex: 3)
noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
for etudid in etudiants.cursus:
if etudid not in self.suivi:
self.suivi[etudid] = {
aggregat: None
for aggregat in pe_comp.TOUS_LES_SEMESTRES
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
}
# Le formsemestre terminal (dernier en date) associé au
# semestre marquant la fin de l'aggrégat
# (par ex: son dernier S3 en date)
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
if semestres:
formsemestre_final = get_dernier_semestre_en_date(semestres)
# Ajout ou récupération de la trajectoire
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
if trajectoire_id not in self.rcss:
trajectoire = RCS(nom_rcs, formsemestre_final)
self.rcss[trajectoire_id] = trajectoire
else:
trajectoire = self.rcss[trajectoire_id]
# La liste des semestres de l'étudiant à prendre en compte
# pour cette trajectoire
semestres_a_aggreger = get_rcs_etudiant(
etudiants.cursus[etudid], formsemestre_final, nom_rcs
)
# Ajout des semestres à la trajectoire
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
# Mémoire la trajectoire suivie par l'étudiant
self.suivi[etudid][nom_rcs] = trajectoire
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
les semestres de son cursus,
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
Si le RCS 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 le RCS 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:
cursus: Dictionnaire {fid: FormSemestre(fid)} donnant l'ensemble des semestres
dans lesquels l'étudiant a été inscrit
formsemestre_final: le semestre final visé
nom_rcs: Nom du RCS visé
"""
numero_semestre_terminal = formsemestre_final.semestre_id
# semestres_significatifs = self.get_semestres_significatifs(etudid)
semestres_significatifs = {}
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
if nom_rcs.startswith("S"): # les semestres
numero_semestres_possibles = [numero_semestre_terminal]
elif nom_rcs.endswith("A"): # les années
numero_semestres_possibles = [
int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"]
]
assert numero_semestre_terminal in numero_semestres_possibles
else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal)
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
semestres_aggreges = {}
for fid, semestre in semestres_significatifs.items():
# Semestres parmi ceux de n° possibles & qui lui sont antérieurs
if (
semestre.semestre_id in numero_semestres_possibles
and semestre.date_fin <= formsemestre_final.date_fin
):
semestres_aggreges[fid] = semestre
return semestres_aggreges
def get_descr_rcs(nom_rcs: str) -> str:
"""Renvoie la description pour les tableurs de synthèse
Excel d'un nom de RCS"""
return TYPES_RCS[nom_rcs]["descr"]

289
app/pe/pe_rcss_jury.py Normal file
View File

@ -0,0 +1,289 @@
import app.pe.pe_comp
from app.pe.rcss import pe_rcs, pe_trajectoires, pe_rcsemx
import app.pe.pe_etudiant as pe_etudiant
import app.pe.pe_comp as pe_comp
from app.models import FormSemestre
from app.pe import pe_affichage
class RCSsJuryPE:
"""Classe centralisant tous les regroupements cohérents de
semestres (RCS) des étudiants à prendre en compte dans un jury PE
Args:
annee_diplome: L'année de diplomation
"""
def __init__(self, annee_diplome: int, etudiants: pe_etudiant.EtudiantsJuryPE):
self.annee_diplome = annee_diplome
"""Année de diplômation"""
self.etudiants = etudiants
"""Les étudiants recensés"""
self.trajectoires: dict[tuple(int, str) : pe_trajectoires.Trajectoire] = {}
"""Ensemble des trajectoires recensées (regroupement de (form)semestres BUT)"""
self.trajectoires_suivies: dict[int:dict] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
sa Trajectoire : {etudid: {nom_RCS: Trajectoire}}"""
self.semXs: dict[tuple(int, str) : pe_trajectoires.SemX] = {}
"""Ensemble des SemX recensés (regroupement de (form)semestre BUT de rang x) :
{(nom_RCS, fid_terminal): SemX}"""
self.semXs_suivis: dict[int:dict] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque RCS de type Sx,
son SemX : {etudid: {nom_RCS_de_type_Sx: SemX}}"""
self.rcsemxs: dict[tuple(int, str) : pe_rcsemx.RCSemX] = {}
"""Ensemble des RCSemX (regroupement de SemX donnant les résultats aux sems de rang x)
recensés : {(nom_RCS, fid_terminal): RCSemX}"""
self.rcsemxs_suivis: dict[int:str] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
son RCSemX : {etudid: {nom_RCS: RCSemX}}"""
def cree_trajectoires(self):
"""Créé toutes les trajectoires, au regard du cursus des étudiants
analysés + les mémorise dans les données de l'étudiant
Args:
etudiants: Les étudiants à prendre en compte dans le Jury PE
"""
tous_les_aggregats = pe_rcs.TOUS_LES_RCS
for etudid in self.etudiants.cursus:
self.trajectoires_suivies[etudid] = self.etudiants.trajectoires[etudid]
for nom_rcs in tous_les_aggregats:
# 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_semestres = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"]
nom_semestre_final = noms_semestres[-1]
for etudid in self.etudiants.cursus:
# Le (ou les) semestre(s) marquant la fin du cursus de l'étudiant
sems_final = self.etudiants.cursus[etudid][nom_semestre_final]
if sems_final:
# Le formsemestre final (dernier en date) de l'étudiant,
# marquant la fin de son aggrégat (par ex: son dernier S3 en date)
formsemestre_final = app.pe.pe_comp.get_dernier_semestre_en_date(
sems_final
)
# Ajout (si nécessaire) et récupération du RCS associé
rcs_id = (nom_rcs, formsemestre_final.formsemestre_id)
if rcs_id not in self.trajectoires:
self.trajectoires[rcs_id] = pe_trajectoires.Trajectoire(
nom_rcs, formsemestre_final
)
rcs = self.trajectoires[rcs_id]
# La liste des semestres de l'étudiant à prendre en compte
# pour cette trajectoire
semestres_a_aggreger = get_rcs_etudiant(
self.etudiants.cursus[etudid], formsemestre_final, nom_rcs
)
# Ajout des semestres au RCS
rcs.add_semestres(semestres_a_aggreger)
# Mémorise le RCS suivi par l'étudiant
self.trajectoires_suivies[etudid][nom_rcs] = rcs
self.etudiants.trajectoires[etudid][nom_rcs] = rcs
def cree_semxs(self):
"""Créé les SemXs (trajectoires/combinaisons de semestre de même rang x),
en ne conservant dans les trajectoires que les regroupements
de type Sx"""
self.semXs = {}
for rcs_id, trajectoire in self.trajectoires.items():
if trajectoire.nom in pe_rcs.TOUS_LES_SEMESTRES:
self.semXs[rcs_id] = pe_trajectoires.SemX(trajectoire)
# L'association (pour chaque étudiant entre chaque Sx et le SemX associé)
self.semXs_suivis = {}
for etudid in self.etudiants.trajectoires:
self.semXs_suivis[etudid] = {
agregat: None for agregat in pe_rcs.TOUS_LES_SEMESTRES
}
for agregat in pe_rcs.TOUS_LES_SEMESTRES:
trajectoire = self.etudiants.trajectoires[etudid][agregat]
if trajectoire:
rcs_id = trajectoire.rcs_id
semX = self.semXs[rcs_id]
self.semXs_suivis[etudid][agregat] = semX
self.etudiants.semXs[etudid][agregat] = semX
def cree_rcsemxs(self, options={"moyennes_ues_rcues": True}):
"""Créé tous les RCSemXs, au regard du cursus des étudiants
analysés (trajectoires traduisant son parcours dans les
différents semestres) + les mémorise dans les données de l'étudiant
"""
self.rcsemxs_suivis = {}
self.rcsemxs = {}
if "moyennes_ues_rcues" in options and options["moyennes_ues_rcues"] == False:
# Pas de RCSemX généré
pe_affichage.pe_print("⚠️ Pas de RCSemX générés")
return
# Pour tous les étudiants du jury
pas_de_semestres = []
for etudid in self.trajectoires_suivies:
self.rcsemxs_suivis[etudid] = {
nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
}
# Pour chaque aggréggat de type xA ou Sx ou xS
for agregat in pe_rcs.TOUS_LES_RCS:
trajectoire = self.trajectoires_suivies[etudid][agregat]
if not trajectoire:
self.rcsemxs_suivis[etudid][agregat] = None
else:
# Identifiant de la trajectoire => donnera ceux du RCSemX
tid = trajectoire.rcs_id
# Ajout du RCSemX
if tid not in self.rcsemxs:
self.rcsemxs[tid] = pe_rcsemx.RCSemX(
trajectoire.nom, trajectoire.formsemestre_final
)
# Récupére les SemX (RC de type Sx) associés aux semestres de son cursus
# Par ex: dans S1+S2+S1+S2+S3 => les 2 S1 devient le SemX('S1'), les 2 S2 le SemX('S2'), etc..
# Les Sx pris en compte dans l'aggrégat
noms_sems_aggregat = pe_rcs.TYPES_RCS[agregat]["aggregat"]
semxs_a_aggreger = {}
for Sx in noms_sems_aggregat:
semestres_etudiants = self.etudiants.cursus[etudid][Sx]
if not semestres_etudiants:
pas_de_semestres += [
f"{Sx} pour {self.etudiants.identites[etudid].nomprenom}"
]
else:
semx_id = get_semx_from_semestres_aggreges(
self.semXs, semestres_etudiants
)
if not semx_id:
raise (
"Il manque un SemX pour créer les RCSemX dans cree_rcsemxs"
)
# Les SemX à ajouter au RCSemX
semxs_a_aggreger[semx_id] = self.semXs[semx_id]
# Ajout des SemX à ceux à aggréger dans le RCSemX
rcsemx = self.rcsemxs[tid]
rcsemx.add_semXs(semxs_a_aggreger)
# Mémoire du RCSemX aux informations de suivi de l'étudiant
self.rcsemxs_suivis[etudid][agregat] = rcsemx
self.etudiants.rcsemXs[etudid][agregat] = rcsemx
# Affichage des étudiants pour lesquels il manque un semestre
pas_de_semestres = sorted(set(pas_de_semestres))
if pas_de_semestres:
pe_affichage.pe_print("⚠️ Semestres manquants :")
pe_affichage.pe_print(
"\n".join([" " * 10 + psd for psd in pas_de_semestres])
)
def get_rcs_etudiant(
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
) -> dict[int, FormSemestre]:
"""Ensemble des semestres parcourus (trajectoire)
par un étudiant dans le cadre
d'un RCS de type Sx, iA ou iS et ayant pour semestre terminal `formsemestre_final`.
Par ex: pour un RCS "3S", dont le formsemestre_terminal est un S3, regroupe
le ou les S1 qu'il a suivi (1 ou 2 si redoublement) + le ou les S2 + le ou les S3.
Les semestres parcourus sont antérieurs (en terme de date de fin)
au formsemestre_terminal.
Args:
cursus: Dictionnaire {fid: Formsemestre} donnant l'ensemble des semestres
dans lesquels l'étudiant a été inscrit
formsemestre_final: le semestre final visé
nom_rcs: Nom du RCS visé
"""
numero_semestre_terminal = formsemestre_final.semestre_id
# semestres_significatifs = self.get_semestres_significatifs(etudid)
semestres_significatifs = {}
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
if nom_rcs.startswith("S"): # les semestres
numero_semestres_possibles = [numero_semestre_terminal]
elif nom_rcs.endswith("A"): # les années
numero_semestres_possibles = [
int(sem[-1]) for sem in pe_rcs.TYPES_RCS[nom_rcs]["aggregat"]
]
assert numero_semestre_terminal in numero_semestres_possibles
else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal)
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
semestres_aggreges = {}
for fid, semestre in semestres_significatifs.items():
# Semestres parmi ceux de n° possibles & qui lui sont antérieurs
if (
semestre.semestre_id in numero_semestres_possibles
and semestre.date_fin <= formsemestre_final.date_fin
):
semestres_aggreges[fid] = semestre
return semestres_aggreges
def get_semx_from_semestres_aggreges(
semXs: dict[(str, int) : pe_trajectoires.SemX],
semestres_a_aggreger: dict[(str, int):FormSemestre],
) -> (str, int):
"""Partant d'un dictionnaire de SemX (de la forme
``{ (nom_rcs, fid): SemX }, et connaissant une liste
de (form)semestres suivis, renvoie l'identifiant
(nom_rcs, fid) du SemX qui lui correspond.
Le SemX qui correspond est tel que :
* le semestre final du SemX correspond au dernier semestre en date des
semestres_a_aggreger
* le rang du SemX est le même que celui des semestres_aggreges
* les semestres_a_aggreger (plus large, car contenant plusieurs
parcours), matchent avec les semestres aggrégés
par le SemX
Returns:
rcf_id: L'identifiant du RCF trouvé
"""
assert semestres_a_aggreger, "Pas de semestres à aggréger"
rangs_a_aggreger = [sem.semestre_id for fid, sem in semestres_a_aggreger.items()]
assert (
len(set(rangs_a_aggreger)) == 1
), "Tous les sem à aggréger doivent être de même rang"
# Le dernier semestre des semestres à regrouper
dernier_sem_a_aggreger = pe_comp.get_dernier_semestre_en_date(semestres_a_aggreger)
semxs_ids = [] # Au cas où il y ait plusieurs solutions
for semx_id, semx in semXs.items():
# Même semestre final ?
if semx.get_formsemestre_id_final() == dernier_sem_a_aggreger.formsemestre_id:
# Les fids
fids_a_aggreger = set(semestres_a_aggreger.keys())
# Ceux du semx
fids_semx = set(semx.semestres_aggreges.keys())
if fids_a_aggreger.issubset(
fids_semx
): # tous les semestres du semx correspond à des sems de la trajectoire
semxs_ids += [semx_id]
if len(semxs_ids) == 0:
return None # rien trouvé
elif len(semxs_ids) == 1:
return semxs_ids[0]
else:
raise "Plusieurs solutions :)"

View File

@ -1,217 +0,0 @@
# -*- 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 Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.comp.res_sem import load_formsemestre_results
from app.pe.pe_semtag import SemestreTag
import pandas as pd
import numpy as np
from app.pe.pe_rcs import RCS
from app.pe.pe_tabletags import TableTag, MoyenneTag
class RCSTag(TableTag):
def __init__(
self, rcs: RCS, semestres_taggues: dict[int, SemestreTag]
):
"""Calcule les moyennes par tag d'une combinaison de semestres
(RCS), pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au semestre terminal.
Args:
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
semestres_taggues: Les données sur les semestres taggués
"""
TableTag.__init__(self)
self.rcs_id = rcs.rcs_id
"""Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
self.rcs = rcs
"""RCS associé au RCS taggué"""
self.nom = self.get_repr()
"""Représentation textuelle du RCS taggué"""
self.formsemestre_terminal = rcs.formsemestre_final
"""Le formsemestre terminal"""
# Les résultats du formsemestre terminal
nt = load_formsemestre_results(self.formsemestre_terminal)
self.semestres_aggreges = rcs.semestres_aggreges
"""Les semestres aggrégés"""
self.semestres_tags_aggreges = {}
"""Les semestres tags associés aux semestres aggrégés"""
for frmsem_id in self.semestres_aggreges:
try:
self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id]
except:
raise ValueError("Semestres taggués manquants")
"""Les étudiants (état civil + cursus connu)"""
self.etuds = nt.etuds
# assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ?
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.tags_sorted = self.do_taglist()
"""Tags extraits de tous les semestres"""
self.notes_cube = self.compute_notes_cube()
"""Cube de notes"""
etudids = list(self.etudiants.keys())
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe"""
self.moyennes_tags: dict[str, MoyenneTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
return self.rcs_id == other.rcs_id
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
return self.rcs.get_repr(verbose=verbose)
def compute_notes_cube(self):
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
nécessaire au calcul des moyennes de l'aggrégat
"""
# nb_tags = len(self.tags_sorted)
# nb_etudiants = len(self.etuds)
# nb_semestres = len(self.semestres_tags_aggreges)
# Index du cube (etudids -> dim 0, tags -> dim 1)
etudids = [etud.etudid for etud in self.etuds]
tags = self.tags_sorted
semestres_id = list(self.semestres_tags_aggreges.keys())
dfs = {}
for frmsem_id in semestres_id:
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
# Charge les notes du semestre tag
notes = self.semestres_tags_aggreges[frmsem_id].notes
# Les étudiants & les tags commun au dataframe final et aux notes du semestre)
etudids_communs = df.index.intersection(notes.index)
tags_communs = df.columns.intersection(notes.columns)
# Injecte les notes par tag
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
# Supprime tout ce qui n'est pas numérique
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube etdids x tags x semestres"""
semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs]
etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1)
return etudids_x_tags_x_semestres
def do_taglist(self):
"""Synthétise les tags à partir des semestres (taggués) aggrégés
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
for frmsem_id in self.semestres_tags_aggreges:
tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted)
return sorted(set(tags))
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
"""Calcul de la moyenne par tag sur plusieurs semestres.
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
par aggrégat de plusieurs semestres.
Args:
set_cube: notes moyennes aux modules ndarray
(etuds x modimpls x UEs), des floats avec des NaN
etudids: liste des étudiants (dim. 0 du cube)
tags: liste des tags (dim. 1 du cube)
Returns:
Un DataFrame avec pour columns les moyennes par tags,
et pour rows les etudid
"""
nb_etuds, nb_tags, nb_semestres = set_cube.shape
assert nb_etuds == len(etudids)
assert nb_tags == len(tags)
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube pour les entrées manquantes
set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0)
# Les moyennes par tag
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy_tag,
index=etudids, # les etudids
columns=tags, # les tags
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df

View File

@ -1,310 +0,0 @@
# -*- 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 Fri Sep 9 09:15:05 2016
@author: barasc
"""
import pandas as pd
import app.pe.pe_etudiant
from app import db, ScoValueError
from app import comp
from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre
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.codes_cursus import UE_SPORT
class SemestreTag(TableTag):
"""
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag.
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
"""
def __init__(self, formsemestre_id: int):
"""
Args:
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
"""
TableTag.__init__(self)
# Le semestre
self.formsemestre_id = formsemestre_id
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Le nom du semestre taggué
self.nom = self.get_repr()
# Les résultats du semestre
self.nt = load_formsemestre_results(self.formsemestre)
# Les étudiants
self.etuds = self.nt.etuds
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
# Les notes, les modules implémentés triés, les étudiants, les coeffs,
# récupérés notamment de py:mod:`res_but`
self.sem_cube = self.nt.sem_cube
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
# Les inscriptions au module et les dispenses d'UE
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
self.ues = self.nt.ues
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
self.dispense_ues = self.nt.dispense_ues
# Les tags :
## Saisis par l'utilisateur
tags_personnalises = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
noms_tags_perso = list(set(tags_personnalises.keys()))
## Déduit des compétences
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
noms_tags_comp = list(set(dict_ues_competences.values()))
noms_tags_auto = ["but"] + noms_tags_comp
self.tags = noms_tags_perso + noms_tags_auto
"""Tags du semestre taggué"""
## Vérifie l'unicité des tags
if len(set(self.tags)) != len(self.tags):
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
liste_intersection = "\n".join(
[f"<li><code>{tag}</code></li>" for tag in intersection]
)
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>
contient le{s} tag{s} réservé{s} suivant :
<ul>
{liste_intersection}
</ul>
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(message)
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in tags_personnalises:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
# Ajoute les moyennes générales de BUT pour le semestre considéré
moy_gen_but = self.nt.etud_moy_gen
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
# Ajoute les moyennes par compétence
for ue_id, competence in dict_ues_competences.items():
if competence not in self.moyennes_tags:
moy_ue = self.nt.etud_moy_ue[ue_id]
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
self.tags_sorted = self.get_all_tags()
"""Tags (personnalisés+compétences) par ordre alphabétique"""
# Synthétise l'ensemble des moyennes dans un dataframe
self.notes = self.df_notes()
"""Dataframe synthétique des notes par tag"""
pe_affichage.pe_print(
f" => Traitement des tags {', '.join(self.tags_sorted)}"
)
def get_repr(self):
"""Nom affiché pour le semestre taggué"""
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series:
"""Calcule la moyenne des étudiants pour le tag indiqué,
pour ce SemestreTag, en ayant connaissance des informations sur
les tags (dictionnaire donnant les coeff de repondération)
Sont pris en compte les modules implémentés associés au tag,
avec leur éventuel coefficient de **repondération**, en utilisant les notes
chargées pour ce SemestreTag.
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Returns:
La série des moyennes
"""
# Adaptation du mask de calcul des moyennes au tag visé
modimpls_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
if modimpl.moduleimpl_id not in tags_infos[tag]:
modimpls_mask[i] = False
# Applique la pondération des coefficients
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in tags_infos[tag]:
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.modimpl_inscr_df,
modimpl_coefs_ponderes_df,
modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
# Les ects
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
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)
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moyennes_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
return moy_gen_tag
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
modimpl = db.session.get(ModuleImpl, modimpl_id)
if modimpl:
return modimpl
return None
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
le module de modimpl_id
"""
# ré-écrit
modimpl = get_moduleimpl(modimpl_id) # le module
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
if ue_status is None:
return None
return ue_status["moy"]
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags renseignés dans le programme pédagogique &
associés aux modules du semestre,
en les associant aux modimpls qui les concernent (modimpl_id) et
aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
Return:
Un dictionnaire de tags
"""
synthese_tags = {}
# Instance des modules du semestre
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
modimpl_id = modimpl.id
# Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id)
# Traitement des tags recensés, chacun pouvant étant de la forme
# "mathématiques", "théorie", "pe:0", "maths:2"
for tag in tags:
# Extraction du nom du tag et du coeff de pondération
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
# Ajout d'une clé pour le tag
if tagname not in synthese_tags:
synthese_tags[tagname] = {}
# Ajout du module (modimpl) au tagname considéré
synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
# "ue_code": modimpl.module.ue.ue_code,
# "ue_acronyme": modimpl.module.ue.acronyme,
}
return synthese_tags
def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
"""Partant d'un formsemestre, extrait le nom des compétences associés
à (ou aux) parcours des étudiants du formsemestre.
Ignore les UEs non associées à un niveau de compétence.
Args:
formsemestre: Un FormSemestre
Returns:
Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences
en les raccrochant à leur ue
"""
# Les résultats du semestre
nt = load_formsemestre_results(formsemestre)
noms_competences = {}
for ue in nt.ues:
if ue.niveau_competence and ue.type != UE_SPORT:
# ?? inutilisé ordre = ue.niveau_competence.ordre
nom = ue.niveau_competence.competence.titre
noms_competences[ue.ue_id] = f"comp. {nom}"
return noms_competences

View File

@ -1,263 +0,0 @@
# -*- 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 Thu Sep 8 09:36:33 2016
@author: barasc
"""
import datetime
import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
from app.pe.pe_affichage import SANS_NOTE
from app.scodoc import sco_utils as scu
import pandas as pd
TAGS_RESERVES = ["but"]
class MoyenneTag:
def __init__(self, tag: str, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant un dictionnaire :
``
{
"notes": la Serie pandas des notes (float),
"classements": la Serie pandas des classements (float),
"min": la note minimum,
"max": la note maximum,
"moy": la moyenne,
"nb_inscrits": le nombre d'étudiants ayant une note,
}
``
Args:
tag: Un tag
note: Une série de notes (moyenne) sous forme d'un pd.Series()
"""
self.tag = tag
"""Le tag associé à la moyenne"""
self.etudids = list(notes.index) # calcul à venir
"""Les id des étudiants"""
self.inscrits_ids = notes[notes.notnull()].index.to_list()
"""Les id des étudiants dont la moyenne est non nulle"""
self.df: pd.DataFrame = self.comp_moy_et_stat(notes)
"""Le dataframe retraçant les moyennes/classements/statistiques"""
self.synthese = self.to_dict()
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
def __eq__(self, other):
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
return self.tag == other.tag
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
de notes (souvent une moyenne par tag) dans un dictionnaire spécifique.
Partant des notes, sont calculés les classements (en ne tenant compte
que des notes non nulles).
Args:
notes: Une série de notes (avec des éventuels NaN)
Returns:
Un dictionnaire stockant les notes, les classements, le min,
le max, la moyenne, le nb de notes (donc d'inscrits)
"""
df = pd.DataFrame(
np.nan,
index=self.etudids,
columns=[
"note",
"classement",
"rang",
"min",
"max",
"moy",
"nb_etuds",
"nb_inscrits",
],
)
# Supprime d'éventuelles chaines de caractères dans les notes
notes = pd.to_numeric(notes, errors="coerce")
df["note"] = notes
# Les nb d'étudiants & nb d'inscrits
df["nb_etuds"] = len(self.etudids)
df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids)
# Le classement des inscrits
notes_non_nulles = notes[self.inscrits_ids]
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
df.loc[self.inscrits_ids, "classement"] = class_int
# Le rang (classement/nb_inscrit)
df["rang"] = df["rang"].astype(str)
df.loc[self.inscrits_ids, "rang"] = (
df.loc[self.inscrits_ids, "classement"].astype(int).astype(str)
+ "/"
+ df.loc[self.inscrits_ids, "nb_inscrits"].astype(int).astype(str)
)
# Les stat (des inscrits)
df.loc[self.inscrits_ids, "min"] = notes.min()
df.loc[self.inscrits_ids, "max"] = notes.max()
df.loc[self.inscrits_ids, "moy"] = notes.mean()
return df
def to_dict(self) -> dict:
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques"""
synthese = {
"notes": self.df["note"],
"classements": self.df["classement"],
"min": self.df["min"].mean(),
"max": self.df["max"].mean(),
"moy": self.df["moy"].mean(),
"nb_inscrits": self.df["nb_inscrits"].mean(),
}
return synthese
def get_notes(self):
"""Série des notes, arrondies à 2 chiffres après la virgule"""
return self.df["note"].round(2)
def get_rangs_inscrits(self) -> pd.Series:
"""Série des rangs classement/nbre_inscrit"""
return self.df["rang"]
def get_min(self) -> pd.Series:
"""Série des min"""
return self.df["min"].round(2)
def get_max(self) -> pd.Series:
"""Série des max"""
return self.df["max"].round(2)
def get_moy(self) -> pd.Series:
"""Série des moy"""
return self.df["moy"].round(2)
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.df["note"].loc[etudid], 2)
def get_min_for_df(self) -> float:
"""Min renseigné pour affichage dans un df"""
return round(self.synthese["min"], 2)
def get_max_for_df(self) -> float:
"""Max renseigné pour affichage dans un df"""
return round(self.synthese["max"], 2)
def get_moy_for_df(self) -> float:
"""Moyenne renseignée pour affichage dans un df"""
return round(self.synthese["moy"], 2)
def get_class_for_df(self, etudid: int) -> str:
"""Classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
classement = self.df["rang"].loc[etudid]
if not pd.isna(classement):
return classement
else:
return pe_affichage.SANS_NOTE
def is_significatif(self) -> bool:
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
return self.synthese["nb_inscrits"] > 0
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
pass
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique,
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
possible).
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(list(self.moyennes_tags.keys()))
def df_moyennes_et_classements(self) -> pd.DataFrame:
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags.
Est utilisé pour afficher le détail d'un tableau taggué
(semestres, trajectoires ou aggrégat)
Returns:
Le dataframe des notes et des classements
"""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
tags_tries = self.get_all_tags()
for tag in tags_tries:
moy_tag = self.moyennes_tags[tag]
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
return df
def df_notes(self) -> pd.DataFrame | None:
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
Returns:
Un dataframe etudids x tag (avec tag par ordre alphabétique)
"""
tags_tries = self.get_all_tags()
if tags_tries:
dict_series = {}
for tag in tags_tries:
# Les moyennes associés au tag
moy_tag = self.moyennes_tags[tag]
dict_series[tag] = moy_tag.synthese["notes"]
df = pd.DataFrame(dict_series)
return df

View File

@ -38,6 +38,7 @@
from flask import flash, g, redirect, render_template, request, send_file, url_for from flask import flash, g, redirect, render_template, request, send_file, url_for
from app.decorators import permission_required, scodoc from app.decorators import permission_required, scodoc
from app.forms.pe.pe_sem_recap import ParametrageClasseurPE
from app.models import FormSemestre from app.models import FormSemestre
from app.pe import pe_comp from app.pe import pe_comp
from app.pe import pe_jury from app.pe import pe_jury
@ -73,17 +74,27 @@ def pe_view_sem_recap(formsemestre_id: int):
# Cosemestres diplomants # Cosemestres diplomants
cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome) cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome)
form = ParametrageClasseurPE()
cosemestres_tries = pe_comp.tri_semestres_par_rang(cosemestres)
affichage_cosemestres_tries = {
rang: ", ".join([sem.titre_annee() for sem in cosemestres_tries[rang]])
for rang in cosemestres_tries
}
if request.method == "GET": if request.method == "GET":
return render_template( return render_template(
"pe/pe_view_sem_recap.j2", "pe/pe_view_sem_recap.j2",
annee_diplome=annee_diplome, annee_diplome=annee_diplome,
form=form,
formsemestre=formsemestre, formsemestre=formsemestre,
sco=ScoData(formsemestre=formsemestre), sco=ScoData(formsemestre=formsemestre),
cosemestres=cosemestres, cosemestres=affichage_cosemestres_tries,
rangs_tries=sorted(affichage_cosemestres_tries.keys()),
) )
# request.method == "POST" # request.method == "POST"
jury = pe_jury.JuryPE(annee_diplome) if form.validate_on_submit():
jury = pe_jury.JuryPE(annee_diplome, formsemestre_id, options=form.data)
if not jury.diplomes_ids: if not jury.diplomes_ids:
flash("aucun étudiant à considérer !") flash("aucun étudiant à considérer !")
return redirect( return redirect(
@ -102,3 +113,11 @@ def pe_view_sem_recap(formsemestre_id: int):
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"), download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
as_attachment=True, as_attachment=True,
) )
return redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)

0
app/pe/rcss/__init__.py Normal file
View File

131
app/pe/rcss/pe_rcs.py Normal file
View File

@ -0,0 +1,131 @@
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on 01-2024
@author: barasc
"""
from app.models import FormSemestre
TYPES_RCS = {
"S1": {
"aggregat": ["S1"],
"descr": "Semestre 1 (S1)",
},
"S2": {
"aggregat": ["S2"],
"descr": "Semestre 2 (S2)",
},
"1A": {
"aggregat": ["S1", "S2"],
"descr": "BUT1 (S1+S2)",
},
"S3": {
"aggregat": ["S3"],
"descr": "Semestre 3 (S3)",
},
"S4": {
"aggregat": ["S4"],
"descr": "Semestre 4 (S4)",
},
"2A": {
"aggregat": ["S3", "S4"],
"descr": "BUT2 (S3+S4)",
},
"3S": {
"aggregat": ["S1", "S2", "S3"],
"descr": "Moyenne du S1 au S3 (S1+S2+S3)",
},
"4S": {
"aggregat": ["S1", "S2", "S3", "S4"],
"descr": "Moyenne du S1 au S4 (S1+S2+S3+S4)",
},
"S5": {
"aggregat": ["S5"],
"descr": "Semestre 5 (S5)",
},
"S6": {
"aggregat": ["S6"],
"descr": "Semestre 6 (S6)",
},
"3A": {
"aggregat": ["S5", "S6"],
"descr": "BUT3 (S5+S6)",
},
"5S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
"descr": "Moyenne du S1 au S5 (S1+S2+S3+S4+S5)",
},
"6S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
},
}
"""Dictionnaire détaillant les différents regroupements cohérents
de semestres (RCS), en leur attribuant un nom et en détaillant
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
dans les tableurs de synthèse.
"""
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
TOUS_LES_RCS = list(TYPES_RCS.keys())
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
def get_descr_rcs(nom_rcs: str) -> str:
"""Renvoie la description pour les tableurs de synthèse
Excel d'un nom de RCS"""
return TYPES_RCS[nom_rcs]["descr"]
class RCS:
"""Modélise un regroupement cohérent de semestres,
tous se terminant par un (form)semestre final.
"""
def __init__(self, nom: str, semestre_final: FormSemestre):
self.nom: str = nom
"""Nom du RCS"""
assert self.nom in TOUS_LES_RCS, "Le nom d'un RCS doit être un aggrégat"
self.aggregat: list[str] = TYPES_RCS[nom]["aggregat"]
"""Aggrégat (liste des nom des semestres aggrégés)"""
self.formsemestre_final: FormSemestre = semestre_final
"""(Form)Semestre final du RCS"""
self.rang_final = self.formsemestre_final.semestre_id
"""Rang du formsemestre final"""
self.rcs_id: (str, int) = (nom, semestre_final.formsemestre_id)
"""Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)"""
self.fid_final: int = self.formsemestre_final.formsemestre_id
"""Identifiant du (Form)Semestre final"""
def get_formsemestre_id_final(self) -> int:
"""Renvoie l'identifiant du formsemestre final du RCS
Returns:
L'id du formsemestre final (marquant la fin) du RCS
"""
return self.formsemestre_final.formsemestre_id
def __str__(self):
"""Représentation textuelle d'un RCS"""
return f"{self.nom}[#{self.formsemestre_final.formsemestre_id}{self.formsemestre_final.date_fin.year}]"
def get_repr(self, verbose=True):
"""Représentation textuelle d'un RCS"""
return self.__str__()
def __eq__(self, other):
"""Egalité de RCS"""
return (
self.nom == other.nom
and self.formsemestre_final == other.formsemestre_final
)

59
app/pe/rcss/pe_rcsemx.py Normal file
View File

@ -0,0 +1,59 @@
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on 01-2024
@author: barasc
"""
from app.models import FormSemestre
from app.pe.moys import pe_sxtag
from app.pe.rcss import pe_rcs, pe_trajectoires
class RCSemX(pe_rcs.RCS):
"""Modélise un regroupement cohérent de SemX (en même regroupant
des semestres Sx combinés pour former les résultats des étudiants
au semestre de rang x) dans le but de synthétiser les résultats
du S1 jusqu'au semestre final ciblé par le RCSemX (dépendant de l'aggrégat
visé).
Par ex: Si l'aggrégat du RCSemX est '3S' (=S1+S2+S3),
regroupement le SemX du S1 + le SemX du S2 + le SemX du S3 (chacun
incluant des infos sur les redoublements).
Args:
nom: Un nom du RCS (par ex: '5S')
semestre_final: Le semestre final du RCS
"""
def __init__(self, nom: str, semestre_final: FormSemestre):
pe_rcs.RCS.__init__(self, nom, semestre_final)
self.semXs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {}
"""Les semX à aggréger"""
def add_semXs(self, semXs: dict[(str, int) : pe_trajectoires.SemX]):
"""Ajoute des semXs aux semXs à regrouper dans le RCSemX
Args:
semXs: Dictionnaire ``{(str,fid): RCF}`` à ajouter
"""
self.semXs_aggreges = self.semXs_aggreges | semXs
def get_repr(self, verbose=True) -> str:
"""Représentation textuelle d'un RCSF
basé sur ses RCF aggrégés"""
title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}"""
if verbose:
noms = []
for semx_id, semx in self.semXs_aggreges.items():
noms.append(semx.get_repr(verbose=False))
if noms:
title += " <<" + "+".join(noms) + ">>"
else:
title += " <<vide>>"
return title

View File

@ -0,0 +1,87 @@
from app.models import FormSemestre
import app.pe.rcss.pe_rcs as pe_rcs
class Trajectoire(pe_rcs.RCS):
"""Regroupement Cohérent de Semestres ciblant un type d'aggrégat (par ex.
'S2', '3S', '1A') et un semestre final, et dont les données regroupées
sont des **FormSemestres** suivis par les étudiants.
Une *Trajectoire* traduit la succession de semestres
qu'ont pu suivre des étudiants pour aller d'un semestre S1 jusqu'au semestre final
de l'aggrégat.
Une *Trajectoire* peut être :
* un RCS de semestre de type "Sx" (cf. classe "SemX"), qui stocke les
formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx
(en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants)
* un RCS de type iS ou iA (par ex, 3A=S1+S2+S3), qui identifie
les formsemestres que des étudiants ont suivis pour les amener jusqu'au semestre
terminal du RCS. Par ex: si le RCS est un 3S:
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
Args:
nom: Un nom du RCS (par ex: '5S')
semestre_final: Le formsemestre final du RCS
"""
def __init__(self, nom: str, semestre_final: FormSemestre):
pe_rcs.RCS.__init__(self, nom, semestre_final)
self.semestres_aggreges: dict[int:FormSemestre] = {}
"""Formsemestres regroupés dans le RCS"""
def add_semestres(self, semestres: dict[int:FormSemestre]):
"""Ajout de semestres aux semestres à regrouper
Args:
semestres: Dictionnaire ``{fid: Formsemestre)``
"""
for sem in semestres.values():
assert isinstance(
sem, FormSemestre
), "Les données aggrégées d'une Trajectoire doivent être des FormSemestres"
self.semestres_aggreges = self.semestres_aggreges | semestres
def get_repr(self, verbose=True) -> str:
"""Représentation textuelle d'un RCS
basé sur ses semestres aggrégés"""
title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}"""
if verbose:
noms = []
for fid in self.semestres_aggreges:
semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}#{fid}")
noms = sorted(noms)
if noms:
title += " <" + "+".join(noms) + ">"
else:
title += " <vide>"
return title
class SemX(Trajectoire):
"""Trajectoire (regroupement cohérent de (form)semestres
dans laquelle tous les semestres regroupés sont de même rang `x`.
Les SemX stocke les
formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx
(en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants).
Ils servent à calculer les SemXTag (moyennes par tag des RCS de type `Sx`).
"""
def __init__(self, trajectoire: Trajectoire):
Trajectoire.__init__(self, trajectoire.nom, trajectoire.formsemestre_final)
semestres_aggreges = trajectoire.semestres_aggreges
for sem in semestres_aggreges.values():
assert (
sem.semestre_id == trajectoire.rang_final
), "Tous les semestres aggrégés d'un SemX doivent être de même rang"
self.semestres_aggreges = trajectoire.semestres_aggreges

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PE de {{ nom }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"/>
<style>
.w3-badge{
background-color:#000;
color:#fff;
display:inline-block;
padding-left:8px;
padding-right:8px;
text-align:center;
border-radius:50%;
font-size: 70%;
}
.w3-red{background-color: darkred;}
.w3-blue{background-color: darkblue;}
ul.legend li {list-style: none; }
</style>
</head>
<body>
<main class="container">
<h1>Résultats PE de {{prenom}} {{nom}} <span style="font-size: 80%">({{ parcours }})</span></h1>
<h2>Légende</h2>
<ul class="legend">
<li><span class="w3-badge w3-red">../..</span>&nbsp;Classement par groupe</li>
<li><span class="w3-badge w3-blue">../..</span>&nbsp;Classement par promo</li>
</ul>
{% for tag in tags %}
<h2>Tag <code>👜 {{ tag }}</code></h2>
<table class="striped">
<!-- Entêtes/Colonnes -->
<thead>
<tr>
<th rowspan="2"></th>
{% for col in colonnes_html %}
<th colspan="2" data-theme="dark">{{ col }}</th>
{% endfor %}
<th colspan="2" data-theme="dark">Général</th>
</tr>
<tr>
{% for col in colonnes_html %}
<th>Note</th><th>Class.</th>
{% endfor %}
<th>Note</th><th>Class.</th>
</tr>
</thead>
<tbody>
{% for aggregat in moyennes[tag] %}
<tr>
<td>{{ aggregat }}</td>
{% for comp in moyennes[tag][aggregat] %}
<td>{{ moyennes[tag][aggregat][comp]["note"] }}</td>
<td><span class="w3-badge w3-red">{{ moyennes[tag][aggregat][comp]["rang_groupe"] }}</span>
<span class="w3-badge w3-blue">{{ moyennes[tag][aggregat][comp]["rang_promo"] }}</span></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</main>main>
</body>
</html>

View File

@ -1,4 +1,5 @@
{% extends "sco_page.j2" %} {% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block styles %} {% block styles %}
{{super()}} {{super()}}
@ -30,7 +31,7 @@
<p> <p>
Cette fonction génère un ensemble de feuilles de calcul (xlsx) Cette fonction génère un ensemble de feuilles de calcul (xlsx)
permettant d'éditer des avis de poursuites d'études pour les étudiants permettant d'éditer des avis de poursuites d'études pour les étudiants
de BUT diplômés. de BUT diplômés. Les calculs sous-jacents peuvent prendre un peu de temps (1 à 3 minutes).
<br> <br>
De nombreux aspects sont paramétrables: De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" <a href="https://scodoc.org/AvisPoursuiteEtudes"
@ -42,34 +43,19 @@
<h3>Avis de poursuites d'études de la promo {{ annee_diplome }}</h3> <h3>Avis de poursuites d'études de la promo {{ annee_diplome }}</h3>
<div class="help">
Seront (a minima) pris en compte les étudiants ayant été inscrits aux semestres suivants : Seront pris en compte les étudiants ayant (au moins) été inscrits à l'un des semestres suivants :
<ul> <ul>
{% for fid in cosemestres %} {% for rang in rangs_tries %}
<li> <li>
{{ cosemestres[fid].titre_annee() }} <strong>Semestre {{rang}}</strong> : {{ cosemestres[rang] }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div>
<div> <h3>Options</h3>
<progress id="pe_progress" style="visibility: hidden"></progress> {{ wtf.quick_form(form) }}
<br>
<button onclick="submitPEGeneration()">Générer les documents de la promo {{ annee_diplome }}</button>
</div>
<form method="post" id="pe_generation" style="visibility: hidden">
<input type="submit"
onclick="submitPEGeneration()" value=""/>
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}">
</form>
<script>
function submitPEGeneration() {
// document.getElementById("pe_progress").style.visibility = 'visible';
document.getElementById("pe_generation").submit(); //attach an id to your form
}
</script>
{% endblock app_content %} {% endblock app_content %}

View File

@ -61,11 +61,11 @@ class DevConfig(Config):
DEBUG = True DEBUG = True
TESTING = False TESTING = False
SQLALCHEMY_DATABASE_URI = ( SQLALCHEMY_DATABASE_URI = (
os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV" os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC"
) )
SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
# pour le avoir url_for dans le shell: # pour le avoir url_for dans le shell:
# SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "localhost" SERVER_NAME = "http://localhost:8080"
class TestConfig(DevConfig): class TestConfig(DevConfig):