Compare commits

...

13 Commits

28 changed files with 586 additions and 358 deletions

View File

@ -393,7 +393,7 @@ class BulletinBUT:
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
@ -408,7 +408,7 @@ class BulletinBUT:
}
if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = {
"injustifie": nbabs - nbabsjust,
"injustifie": nbabsnj,
"total": nbabs,
"metrique": {
"H.": "Heure(s)",
@ -525,7 +525,7 @@ class BulletinBUT:
d["demission"] = ""
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, _ = sco_bulletins.etud_descr_situation_semestre(
@ -540,9 +540,9 @@ class BulletinBUT:
d.update(infos)
# --- Rangs
d["rang_nt"] = (
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
)
d[
"rang_nt"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -241,7 +241,7 @@ def bulletin_but_xml_compat(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------

View File

@ -875,7 +875,7 @@ class FormSemestre(db.Model):
def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non just, nb abs justifiées, nb abs total)
Utilise un cache.
"""
from app.scodoc import sco_assiduites

View File

@ -119,7 +119,7 @@ class RCSemXTag(pe_tabletags.TableTag):
)
"""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)}")
pe_affichage.pe_print(f"--> Compétences : {aff}")
# Les tags
self.tags_sorted = self._do_taglist()
@ -134,28 +134,24 @@ class RCSemXTag(pe_tabletags.TableTag):
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)
# Cubes d'inscription (etudids_sorted x compétences_sorted x sxstags),
# de notes et de coeffs pour la moyenne générale
# en "aggrégant" les données des sxstags, compétence par compétence
(
inscr_df,
inscr_cube,
notes_df,
notes_cube,
coeffs_df,
coeffs_cube,
) = self.compute_cubes(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
# Calcule les moyennes, et synthétise les coeffs
(
moys_competences,
matrice_coeffs_moy_gen,
) = self.compute_notes_et_coeffs_competences(
notes_cube, coeffs_cube, inscr_cube
)
# Affichage des coeffs
@ -186,10 +182,17 @@ class RCSemXTag(pe_tabletags.TableTag):
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
def compute_cubes(self, tag):
"""Pour un tag donné, construit les cubes de :
* d'inscriptions aux compétences (etudid x competences x SxTag)
* de notes (etudid x competences x SxTag)
* de coeffs (etudid x competences x SxTag)
nécessaire au calcul des moyennes, en :
* transformant les données des UEs en données de compétences (changement de noms)
* fusionnant les données d'un même semestre, lorsque plusieurs UEs traitent d'une même compétence (cas des RCSx = Sx)
* aggrégeant les données de compétences sur plusieurs semestres (cas des RCSx = xA ou xS)
Args:
tag: Le tag visé
@ -197,144 +200,75 @@ class RCSemXTag(pe_tabletags.TableTag):
# etudids_sorted: list[int],
# competences_sorted: list[str],
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
inscriptions_dfs = {}
notes_dfs = {}
coeffs_dfs = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
# Partant d'un dataframe vierge
# Partant de dataframes vierges
inscription_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
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)
coeffs_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
# Charge les données 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
# Les inscr, les notes, les coeffs
acro_ues_inscr_parcours = sxtag.acro_ues_inscr_parcours
notes = moys_tag.matrice_notes_gen
coeffs = moys_tag.matrice_coeffs_moy_gen # les coeffs
# Traduction des acronymes d'UE en compétences
# comp_to_ues = pe_comp.asso_comp_to_accronymes(self.acronymes_ues_to_competences)
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
for acronyme in acronymes_ues_columns:
# La compétence visée
competence = self.acronymes_ues_to_competences[acronyme] # La comp
# Les étudiants et les compétences communes
(
etudids_communs,
comp_communes,
) = pe_comp.find_index_and_columns_communs(notes_df, notes)
# Les étud inscrits à la comp reportés dans l'inscription au RCSemX
comp_inscr = acro_ues_inscr_parcours[
acro_ues_inscr_parcours.notnull()
].index
etudids_communs = list(
inscription_df.index.intersection(comp_inscr)
)
inscription_df.loc[
etudids_communs, competence
] = acro_ues_inscr_parcours.loc[etudids_communs, acronyme]
# Recopie des notes et des coeffs
notes_df.loc[etudids_communs, comp_communes] = notes.loc[
etudids_communs, comp_communes
# Les étud ayant une note à l'acronyme de la comp (donc à la comp)
etuds_avec_notes = notes[notes[acronyme].notnull()].index
etudids_communs = list(
notes_df.index.intersection(etuds_avec_notes)
)
notes_df.loc[etudids_communs, competence] = notes.loc[
etudids_communs, acronyme
]
# Les coeffs
etuds_avec_coeffs = coeffs[coeffs[acronyme].notnull()].index
etudids_communs = list(
coeffs_df.index.intersection(etuds_avec_coeffs)
)
coeffs_df.loc[etudids_communs, competence] = coeffs.loc[
etudids_communs, acronyme
]
# 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
notes_dfs[sxtag_id] = notes_df
coeffs_dfs[sxtag_id] = coeffs_df
"""Réunit les inscriptions sous forme d'un cube etudids x competences x semestres"""
sxtag_x_etudids_x_comps = [
@ -344,7 +278,26 @@ class RCSemXTag(pe_tabletags.TableTag):
sxtag_x_etudids_x_comps, axis=-1
)
return inscriptions_dfs, inscriptions_etudids_x_comps_x_sxtag
"""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)
"""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 (
inscriptions_dfs,
inscriptions_etudids_x_comps_x_sxtag,
notes_dfs,
notes_etudids_x_comps_x_sxtag,
coeffs_dfs,
coeffs_etudids_x_comps_x_sxtag,
)
def _do_taglist(self) -> list[str]:
"""Synthétise les tags à partir des Sxtags aggrégés.
@ -370,7 +323,9 @@ class RCSemXTag(pe_tabletags.TableTag):
dict_competences |= sxtag.acronymes_ues_to_competences
return dict_competences
def compute_notes_competences(self, set_cube: np.array, inscriptions: np.array):
def compute_notes_et_coeffs_competences(
self, notes_cube: np.array, coeffs_cube: np.array, inscr_mask: 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
@ -379,9 +334,11 @@ class RCSemXTag(pe_tabletags.TableTag):
par aggrégat de plusieurs semestres.
Args:
set_cube: notes moyennes aux compétences ndarray
notes_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
coeffs_cube: coeffs appliqués aux compétences
(etuds x UEs|compétences x sxtags), des floats avec des NaN
inscr_mask: 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,
@ -389,78 +346,45 @@ class RCSemXTag(pe_tabletags.TableTag):
"""
# 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
nb_etuds, nb_comps, nb_semestres = notes_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
# Applique le masque d'inscriptions aux notes et aux coeffs
notes_significatives = notes_cube * inscr_mask
coeffs_significatifs = coeffs_cube * inscr_mask
# 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)
# Enlève les NaN des cubes pour les entrées manquantes
notes_no_nan = np.nan_to_num(notes_significatives, nan=0.0)
coeffs_no_nan = np.nan_to_num(coeffs_significatifs, 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)
mask = ~np.isnan(
notes_significatives
) # Quelles entrées contiennent des notes ?
etud_moy_tag = np.sum(notes_no_nan, axis=2) / np.sum(mask, axis=2)
coeffs_pris_en_compte = coeffs_no_nan * mask
coeff_tag = np.sum(coeffs_pris_en_compte, axis=2)
inscr_prise_en_compte = inscr_mask * mask
inscr_prise_en_compte = np.nan_to_num(inscr_prise_en_compte, nan=-1.0)
inscr_tag = np.max(inscr_prise_en_compte, axis=2)
inscr_tag[inscr_tag < 0] = np.NaN # fix les max non calculés (-1) -> Na?
# Le dataFrame des notes moyennes
etud_moy_tag = etud_moy_tag * inscr_tag
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
coeff_tag = coeff_tag * inscr_tag # Réapplique le masque des inscriptions
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
return etud_moy_tag_df, coeffs_df

View File

@ -98,8 +98,10 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
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"""
self.ues_inscr_parcours_df = (
self.load_ues_inscr_parcours()
) # peut contenir du sport
"""Inscription des étudiants aux UEs des parcours (etudids x ue_ids)"""
# Les acronymes des UEs
self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards}
@ -144,6 +146,12 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}"
)
# Les inscriptions aux acronymes d'ues
self.acro_ues_inscr_parcours = self._get_acro_ues_inscr_parcours(
self.ues_inscr_parcours_df, self.ues_standards
)
"""DataFrame indiquant à quelles UEs (données par leurs acronymes) sont inscrits les étudiants)"""
# 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 )"""
@ -167,7 +175,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
)
# Ajoute les moyennes par UEs + la moyenne générale (but)
moy_gen = self.compute_moy_gen()
moy_gen = self.compute_moy_gen(self.acro_ues_inscr_parcours)
self.moyennes_tags["but"] = pe_moytag.MoyennesTag(
"but",
pe_moytag.CODE_MOY_UE,
@ -240,6 +248,31 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index(axis=1)
return matrice_coeffs_moy_gen
def _get_acro_ues_inscr_parcours(
self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns]
) -> pd.DataFrame:
"""Renvoie un dataFrame donnant les inscriptions (Nan ou 1) des
étudiants aux UEs définies par leur acronyme, en fonction de leur parcours
(cf. ues_inscr_parcours_df) et en limitant les données aux UEs standards (hors sport=
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_inscription = ues_inscr_parcours_df * [
1 for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé
]
matrice_inscription.columns = [
self.ues_to_acronymes[ue.id] for ue in ues_standards
]
# Tri par etudids (dim 0) et par acronymes (dim 1)
matrice_inscription = matrice_inscription.sort_index()
matrice_inscription = matrice_inscription.sort_index(axis=1)
return matrice_inscription
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).
@ -342,6 +375,9 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
colonnes = [ue.id for ue in self.ues_standards]
moyennes_ues_tag = moyennes_ues_tag[colonnes]
# Met à zéro les moyennes non calculées/calculables
moyennes_ues_tag.fillna(0.0, inplace=True)
# Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
@ -355,7 +391,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
return moyennes_ues_tag
def compute_moy_gen(self):
def compute_moy_gen(self, acro_ues_inscr_parcours):
"""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)
"""
@ -368,6 +404,12 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
acronymes = [self.ues_to_acronymes[col] for col in colonnes]
df_ues.columns = acronymes
# Met à zéro les moyennes non calculées/calculables
df_ues.fillna(0.0, inplace=True)
# Réapplique le mask d'inscription
df_ues = df_ues * acro_ues_inscr_parcours
# Tri par ordre aphabétique de colonnes
df_ues.sort_index(axis=1)

View File

@ -141,6 +141,10 @@ class SxTag(pe_tabletags.TableTag):
aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")
# Les inscriptions des étudiants aux UEs (donnée par leur acronyme)
# par report de celle du ressemfinal
self.acro_ues_inscr_parcours = self.ressembuttag_final.acro_ues_inscr_parcours
# 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
@ -178,7 +182,7 @@ class SxTag(pe_tabletags.TableTag):
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())
# inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy())
# Moyennes (tous modules confondus)
if not self.has_notes_tag(tag):
@ -194,6 +198,7 @@ class SxTag(pe_tabletags.TableTag):
notes_df_gen, notes_cube_gen = self.compute_notes_ues_cube(tag)
# DataFrame des moyennes (tous modules confondus)
inscr_mask = self.acro_ues_inscr_parcours.to_numpy()
matrice_moys_ues = self.compute_notes_ues(
notes_cube_gen, masque_cube, inscr_mask
)
@ -289,7 +294,7 @@ class SxTag(pe_tabletags.TableTag):
def compute_notes_ues(
self,
set_cube: np.array,
masque_cube: np.array,
cap_mask_3D: np.array,
inscr_mask: np.array,
) -> pd.DataFrame:
"""Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
@ -298,7 +303,8 @@ class SxTag(pe_tabletags.TableTag):
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
cap_mask_3D
: 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)
@ -320,10 +326,7 @@ class SxTag(pe_tabletags.TableTag):
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)
set_cube = set_cube * cap_mask_3D
# 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)
@ -332,9 +335,12 @@ class SxTag(pe_tabletags.TableTag):
# 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
# Fix les max non calculés (-1) -> NaN
etud_moy[etud_moy < 0] = np.NaN
# Réapplique le masque d'inscription (dans le doute)
etud_moy = etud_moy * inscr_mask
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy,

View File

@ -9,6 +9,7 @@
from flask import g
from app import log
from app.pe.rcss import pe_rcs
import app.pe.pe_comp as pe_comp
PE_DEBUG = False
@ -135,23 +136,45 @@ def aff_tags_par_categories(dict_tags):
aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto])
return f"Tags automatiques {aff_tags_auto} (aucun tag personnalisé)"
# Affichage
def repr_jeune(etudid, etudiants):
"""Renvoie la représentation d'un étudiant"""
etat = "" if etudid in etudiants.abandons_ids else ""
jeune = f"{etat} {etudiants.identites[etudid].nomprenom} (#{etudid})"
return jeune
def aff_trajectoires_suivies_par_etudiants(etudiants):
"""Affiche les trajectoires (regroupement de (form)semestres)
amenant un étudiant du S1 à un semestre final"""
amenant un étudiant du S1 à un semestre final,
en regroupant les étudiants par profil de trajectoires"""
# 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}) :")
profils_traj = {}
for no_etud, etudid in jeunes:
jeune = repr_jeune(etudid, etudiants)
# La trajectoire du jeune
trajectoires = etudiants.trajectoires[etudid]
profil_traj = []
for nom_rcs, rcs in trajectoires.items():
if rcs:
pe_print(f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}")
profil_traj += [f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}"]
aff_profil_traj = "\n".join(profil_traj)
if aff_profil_traj not in profils_traj:
profils_traj[aff_profil_traj] = []
profils_traj[aff_profil_traj] += [jeune]
# Affichage final
for profil, jeunes in profils_traj.items():
pe_print(f"--> Trajectoire suivie par : ")
pe_print("\n".join([" " + jeune for jeune in jeunes]))
pe_print(profil)
def aff_semXs_suivis_par_etudiants(etudiants):
@ -198,13 +221,11 @@ def aff_capitalisations(etuds, ressembuttags, fid_final, acronymes_sorted, masqu
def repr_comp_et_ues(acronymes_ues_to_competences):
"""Affichage pour debug"""
asso_comp_to_ues = pe_comp.asso_comp_to_accronymes(acronymes_ues_to_competences)
aff_comp = []
competences_sorted = sorted(acronymes_ues_to_competences.keys())
competences_sorted = sorted(asso_comp_to_ues.keys())
for comp in competences_sorted:
liste = []
for acro in acronymes_ues_to_competences:
if acronymes_ues_to_competences[acro] == comp:
liste += ["📍" + acro]
liste = ["📍" + accro for accro in asso_comp_to_ues[comp]]
aff_comp += [f" 💡{comp} (⇔ {', '.join(liste)})"]
return "\n".join(aff_comp)

View File

@ -337,3 +337,21 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSeme
dernier_semestre = semestres[fid]
return dernier_semestre
return None
def asso_comp_to_accronymes(accro_ues_to_competences):
"""Partant d'un dictionnaire ``{nom_ue: compétence}`` associant des
accronymes d'UEs à des compétences, renvoie l'association d'une compétence
à ou aux UEs qui l'adresse : ``{competence: [liste_nom_ue]}``
Args:
accro_ues_to_competences: Dictionnaire ``{nom_ue: compétence}``
Return:
Le dictionnaire ``{competence: [liste_nom_ue]}``
"""
asso = {}
for accro, comp in accro_ues_to_competences.items():
if comp not in asso:
asso[comp] = []
asso[comp].append(accro)
return asso

View File

@ -705,7 +705,7 @@ class JuryPE(object):
tag, aggregat=aggregat, type_colonnes=False, options=self.options
)
if not df_groupe.empty:
aff_aggregat += [aggregat]
aff_aggregat += [aggregat + " (Groupe)"]
df = df.join(df_groupe)
# Le dataframe du classement sur la promo
@ -718,7 +718,7 @@ class JuryPE(object):
)
if not df_promo.empty:
aff_aggregat += [aggregat]
aff_aggregat += [aggregat + " (Promo)"]
df = df.join(df_promo)
if aff_aggregat:

View File

@ -15,7 +15,7 @@ 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
"""Modélise un regroupement cohérent de SemX (en 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

View File

@ -175,10 +175,9 @@ def sidebar(etudid: int = None):
inscription = etud.inscription_courante()
if inscription:
formsemestre = inscription.formsemestre
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count(
etudid, formsemestre
)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du {
formsemestre.date_debut.strftime("%d/%m/%Y")
@ -186,7 +185,7 @@ def sidebar(etudid: int = None):
formsemestre.date_fin.strftime("%d/%m/%Y")
}">({
sco_preferences.get_preference("assi_metrique", None)})
<br>{nbabsjust:1.0f} J., {nbabsnj:1.0f} N.J.</span>"""
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
)
H.append("<ul>")
if current_user.has_permission(Permission.AbsChange):

View File

@ -67,7 +67,7 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
if not formsemestre:
return # non inscrit a la date, pas de notification
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
etudid,
metrique=scu.translate_assiduites_metric(
sco_preferences.get_preference(

View File

@ -671,7 +671,7 @@ def create_absence_billet(
# Gestion du cache
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
@ -687,17 +687,17 @@ def formsemestre_get_assiduites_count(
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
etudid,
date_debut=scu.localize_datetime(
datetime.combine(formsemestre.date_debut, time(8, 0))
datetime.combine(formsemestre.date_debut, time(0, 0))
),
date_fin=scu.localize_datetime(
datetime.combine(formsemestre.date_fin, time(18, 0))
datetime.combine(formsemestre.date_fin, time(23, 0))
),
metrique=scu.translate_assiduites_metric(metrique),
moduleimpl_id=moduleimpl_id,
@ -714,12 +714,12 @@ def get_assiduites_count_in_interval(
moduleimpl_id: int = None,
):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
On peut spécifier les dates comme datetime ou iso.
Utilise un cache.
"""
date_debut_iso = date_debut_iso or date_debut.isoformat()
date_fin_iso = date_fin_iso or date_fin.isoformat()
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key)
@ -744,9 +744,10 @@ def get_assiduites_count_in_interval(
if not ans:
log("warning: get_assiduites_count failed to cache")
nb_abs: dict = r["absent"][metrique]
nb_abs_just: dict = r["absent_just"][metrique]
return (nb_abs, nb_abs_just)
nb_abs: int = r["absent"][metrique]
nb_abs_nj: int = r["absent_non_just"][metrique]
nb_abs_just: int = r["absent_just"][metrique]
return (nb_abs_nj, nb_abs_just, nb_abs)
def invalidate_assiduites_count(etudid: int, sem: dict):

View File

@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# --- Absences
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
_, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
# --- Decision Jury
infos, dpv = etud_descr_situation_semestre(
@ -471,7 +471,7 @@ def _ue_mod_bulletin(
) # peut etre 'NI'
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
if bul_show_abs_modules:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
mod_abs = [nbabs, nbabsjust]
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
else:

View File

@ -296,7 +296,7 @@ def formsemestre_bulletinetud_published_dict(
# --- Absences
if prefs["bul_show_abs"]:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Décision Jury

View File

@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud(
numero=str(mod["numero"]),
titre=quote_xml_attr(mod["titre"]),
abbrev=quote_xml_attr(mod["abbrev"]),
code_apogee=quote_xml_attr(mod["code_apogee"])
code_apogee=quote_xml_attr(mod["code_apogee"]),
# ects=ects ects des modules maintenant inutilisés
)
x_ue.append(x_mod)
@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# --- Decision Jury
if (

View File

@ -722,8 +722,8 @@ def formsemestre_recap_parcours_table(
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
)
# Absences (nb d'abs non just. dans ce semestre)
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
# UEs
for ue in ues:

View File

@ -105,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
rangs.append(["rang_" + code_module, rang_module])
# Absences
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count(
etudid, nt.sem
)
# En BUT, prend tout, sinon ne prend que les semestre validés par le jury
if nt.is_apc or (
dec
@ -125,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
("date_debut", s["date_debut"]),
("date_fin", s["date_fin"]),
("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])),
("AbsNonJust", nbabs - nbabsjust),
("AbsNonJust", nbabsnj),
("AbsJust", nbabsjust),
]
# ajout des 2 champs notes des modules et classement dans chaque module

View File

@ -620,7 +620,7 @@ class RowRecap(tb.Row):
def add_abs(self):
"Ajoute les colonnes absences"
# Absences (nb d'abs non just. dans ce semestre)
nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id)
_, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id)
self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs)
self.add_cell(
"nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust

View File

@ -154,50 +154,25 @@ div.submit > input {
{% include "sco_timepicker.j2" %}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Suppression d'un fichier justificatif
function delete_file(justif_id, fileName, liElement) {
// Construct the URL
var url = "{{url_for('apiweb.justif_remove', justif_id=-1, scodoc_dept=g.scodoc_dept)}}".replace('-1', justif_id);
payload = {
"remove": "list",
"filenames" : [ fileName ],
}
// Send API request
fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
if (response.ok) {
// Hide the <li> element on successful deletion
liElement.style.display = 'none';
sco_message("fichier supprimé");
} else {
// Handle non-successful responses here
console.error('Deletion failed:', response.statusText);
sco_error_message("erreur lors de la suppression du fichier");
}
})
.catch(error => {
console.error('Error:', error);
sco_error_message("erreur lors de la suppression du fichier (2)");
});
}
// Add event listeners to all elements with class 'suppr_fichier_just'
var deleteButtons = document.querySelectorAll('.suppr_fichier_just');
const form = document.getElementById('ajout-justificatif-etud');
deleteButtons.forEach(function(button) {
button.addEventListener('click', function() {
// Get the text content of the next sibling node
var justif_id = this.dataset.justif_id;
var fileName = this.nextSibling.nodeValue.trim();
var liElement = this.parentNode; // Get the parent <li> element
delete_file(justif_id, fileName, liElement);
// Create a hidden input element to store the file name
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'suppr_fichier_just';
input.value = fileName;
form.appendChild(input);
liElement.remove();
});
});
});

View File

@ -174,7 +174,7 @@
}
window.addEventListener('load', ()=>{
const table_columns = [...document.querySelectorAll('.external-sort')];
const table_columns = [...document.querySelectorAll('th.external-sort')];
table_columns.forEach((e)=>e.addEventListener('click', ()=>{
// récupération de l'ordre "ascending" / "descending"

View File

@ -56,9 +56,9 @@
</h2>
<b>Absences</b>
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1.0f'|format(sco.nbabsjust)}} J., {{'%1.0f'|format(sco.nbabsnj)}} N.J.</span>
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1g'|format(sco.nbabsjust)}} J., {{'%1g'|format(sco.nbabsnj)}} N.J.</span>
{% endif %}
<ul>
{% if current_user.has_permission(sco.Permission.AbsChange) %}

View File

@ -74,8 +74,9 @@ class ScoData:
if ins:
self.etud_cur_sem = ins.formsemestre
(
self.nbabs,
self.nbabsnj,
self.nbabsjust,
self.nbabs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
self.etud_cur_sem.date_debut.isoformat(),
@ -84,7 +85,6 @@ class ScoData:
sco_preferences.get_preference("assi_metrique")
),
)
self.nbabsnj = self.nbabs - self.nbabsjust
else:
self.etud_cur_sem = None
else:

View File

@ -658,6 +658,8 @@ def edit_justificatif_etud(justif_id: int):
etudid=justif.etudiant.id,
)
if form.validate_on_submit():
if form.cancel.data: # cancel button
return redirect(redirect_url)
if _record_justificatif_etud(justif.etudiant, form, justif):
return redirect(redirect_url)
@ -757,7 +759,6 @@ def _record_justificatif_etud(
dt_fin_tz_server,
dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form, all_day=True)
if not ok:
log("_record_justificatif_etud: dates invalides")
form.set_error("Erreur: dates invalides")
@ -793,6 +794,14 @@ def _record_justificatif_etud(
)
else:
message = "Pas de modification"
fichier_suppr: list[str] = request.form.getlist("suppr_fichier_just")
if len(fichier_suppr) > 0 and justif.fichier is not None:
archiver: JustificatifArchiver = JustificatifArchiver()
for fichier in fichier_suppr:
archiver.delete_justificatif(etud, justif.fichier, fichier)
flash(f"Fichier {fichier} supprimé")
else:
justif = Justificatif.create_justificatif(
etud,

View File

@ -1187,14 +1187,18 @@ def view_module_abs(moduleimpl_id, fmt="html"):
rows = []
for etud in inscrits:
nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(
(
nb_abs_nj,
nb_abs_just,
nb_abs,
) = sco_assiduites.formsemestre_get_assiduites_count(
etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id
)
rows.append(
{
"nomprenom": etud.nomprenom,
"just": nb_abs_just,
"nojust": nb_abs - nb_abs_just,
"nojust": nb_abs_nj,
"total": nb_abs,
"_nomprenom_target": url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id

View File

@ -72,8 +72,6 @@ def test_general(test_client):
verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres)
verifier_filtrage_justificatifs(etuds[0], justificatifs)
essais_cache(etuds[0].etudid, formsemestres[:2], moduleimpls)
editer_supprimer_assiduites(etuds, moduleimpls)
editer_supprimer_justificatif(etuds[0])
@ -402,54 +400,6 @@ def _get_justi(
).first()
def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]):
"""Vérification des fonctionnalités du cache"""
# TODO faire un test séparé du test_general
# voir test_calcul_assiduites pour faire
date_deb: str = "2022-09-01T07:00"
date_fin: str = "2023-01-31T19:00"
assiduites_count_no_cache = scass.get_assiduites_count_in_interval(
etudid, date_deb, date_fin
)
assiduites_count_cache = scass.get_assiduites_count_in_interval(
etudid, date_deb, date_fin
)
assert (
assiduites_count_cache == assiduites_count_no_cache == (2, 1)
), "Erreur cache classique"
assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == (
2,
1,
), "Erreur formsemestre_get_assiduites_count (sans module) A"
assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (sans module) B"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[0].id
) == (
1,
1,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[1].id
) == (
1,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[2].id
) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
def ajouter_justificatifs(etud):
"""test de l'ajout des justificatifs"""
@ -1414,6 +1364,7 @@ def test_cas_justificatifs(test_client):
Tests de certains cas particuliers des justificatifs
- Création du justificatif avant ou après assiduité
- Assiduité complétement couverte ou non
- Modification de la couverture (edition du justificatif)
"""
data = _setup_fake_db(
@ -1503,3 +1454,279 @@ def test_cas_justificatifs(test_client):
assert (
len(scass.justifies(justif_4)) == 0
), "Justification complète non prise en compte (c2)"
# <- Vérification modification de la couverture ->
# Deux assiduités, 8/01/2024 de 8h à 10h et 14h à 16h
assi_2: Assiduite = Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T08:00", True),
date_fin=scu.is_iso_formated("2024-01-08T10:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
assi_3: Assiduite = Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T14:00", True),
date_fin=scu.is_iso_formated("2024-01-08T16:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
# <=>Justification complète<=>
# les deux assiduités sont couvertes
justif_5: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T00:00:00", True),
date_fin=scu.is_iso_formated("2024-01-08T23:59:59", True),
etat=scu.EtatJustificatif.VALIDE,
)
# Justification des assiduités
assi_ids: list[int] = justif_5.justifier_assiduites()
assert len(assi_ids) == 2, "Vérification Modification couverture (d1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d2)"
assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d3)"
assert assi_2.est_just is True, "Vérification Modification couverture (d4)"
assert assi_3.est_just is True, "Vérification Modification couverture (d5)"
# Déjustification des assiduités
justif_5.dejustifier_assiduites()
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 2, "Vérification Modification couverture (d6)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d7)"
assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d8)"
assert assi_2.est_just is False, "Vérification Modification couverture (d9)"
assert assi_3.est_just is False, "Vérification Modification couverture (d10)"
# <=>Justification Partielle<=>
# Seule la première assiduité est couverte
justif_5.date_fin = scu.is_iso_formated("2024-01-08T11:00", True)
db.session.add(justif_5)
db.session.commit()
assi_ids: list[int] = justif_5.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (e1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e2)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (e3)"
assert assi_2.est_just is True, "Vérification Modification couverture (e4)"
assert assi_3.est_just is False, "Vérification Modification couverture (e5)"
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (e6)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e7)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (e3)"
assert assi_2.est_just is False, "Vérification Modification couverture (e8)"
assert assi_3.est_just is False, "Vérification Modification couverture (e9)"
# <=>Justification Multiple<=>
# Deux justificatifs couvrent une même assiduité
# on justifie la première assiduité avec le premier justificatif
justif_5.justifier_assiduites()
# deuxième justificatif
justif_6: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T08:00", True),
date_fin=scu.is_iso_formated("2024-01-08T10:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
assi_ids: list[int] = justif_6.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (f1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f2)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (f3)"
assert assi_2.est_just is True, "Vérification Modification couverture (f4)"
assert assi_3.est_just is False, "Vérification Modification couverture (f5)"
# on déjustifie le justificatif 5
justif_5.etat = scu.EtatJustificatif.NON_VALIDE
db.session.add(justif_5)
db.session.commit()
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 0, "Vérification Modification couverture (f6)"
assert (
assi_2.assiduite_id not in assi_ids
), "Vérification Modification couverture (f7)"
assert assi_2.est_just is True, "Vérification Modification couverture (f8)"
# on déjustifie le justificatif 6
justif_6.etat = scu.EtatJustificatif.NON_VALIDE
db.session.add(justif_6)
db.session.commit()
assi_ids: list[int] = justif_6.dejustifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (f9)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f10)"
assert assi_2.est_just is False, "Vérification Modification couverture (f11)"
# <=>Justification Chevauchée<=>
# 1 justificatif chevauche une assiduité (8h -> 10h) (9h -> 11h)
justif_7: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T09:00", True),
date_fin=scu.is_iso_formated("2024-01-08T11:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
assi_ids: list[int] = justif_7.justifier_assiduites()
assert len(assi_ids) == 0, "Vérification Modification couverture (g1)"
assert (
assi_2.assiduite_id not in assi_ids
), "Vérification Modification couverture (g2)"
assert assi_2.est_just is False, "Vérification Modification couverture (g3)"
# Modification pour correspondre à l'assiduité
justif_7.date_debut = scu.is_iso_formated("2024-01-08T08:00", True)
db.session.add(justif_7)
db.session.commit()
assi_ids: list[int] = justif_7.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (g4)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (g5)"
assert assi_2.est_just is True, "Vérification Modification couverture (g6)"
def test_cache_assiduites(test_client):
"""Vérification du bon fonctionnement du cache des assiduités"""
data = _setup_fake_db(
[("2024-01-01", "2024-06-30"), ("2024-07-01", "2024-12-31")],
1,
1,
)
formsemestre1: FormSemestre = data["formsemestres"][0]
formsemestre2: FormSemestre = data["formsemestres"][1]
moduleimpl: ModuleImpl = data["moduleimpls"][0]
etud: Identite = data["etuds"][0]
# Création des assiduités
assiduites: list[dict] = [
# Semestre 1
{
"date_debut": "2024-01-08T08:00",
"date_fin": "2024-01-08T10:00",
"moduleimpl": moduleimpl,
},
{
"date_debut": "2024-01-08T14:00",
"date_fin": "2024-01-08T16:00",
"moduleimpl": moduleimpl,
},
{
"date_debut": "2024-01-09T08:00",
"date_fin": "2024-01-09T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-09T14:00",
"date_fin": "2024-01-09T16:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-10T08:00",
"date_fin": "2024-01-10T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-10T14:00",
"date_fin": "2024-01-10T16:00",
"moduleimpl": moduleimpl,
},
# Semestre 2
{
"date_debut": "2024-07-09T14:00",
"date_fin": "2024-07-09T16:00",
"moduleimpl": None,
},
{
"date_debut": "2024-07-10T08:00",
"date_fin": "2024-07-10T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-07-10T14:00",
"date_fin": "2024-07-10T16:00",
"moduleimpl": None,
},
]
justificatifs: list[dict] = [
{
"date_debut": "2024-01-10T00:00",
"date_fin": "2024-01-10T23:59",
},
{
"date_debut": "2024-07-09T00:00",
"date_fin": "2024-07-09T23:59",
},
]
# On ajoute les assiduités et les justificatifs
for assi in assiduites:
Assiduite.create_assiduite(
etud=etud,
date_debut=scu.is_iso_formated(assi["date_debut"], True),
date_fin=scu.is_iso_formated(assi["date_fin"], True),
moduleimpl=assi["moduleimpl"],
etat=scu.EtatAssiduite.ABSENT,
)
for justi in justificatifs:
Justificatif.create_justificatif(
etudiant=etud,
date_debut=scu.is_iso_formated(justi["date_debut"], True),
date_fin=scu.is_iso_formated(justi["date_fin"], True),
etat=scu.EtatJustificatif.VALIDE,
).justifier_assiduites()
# Premier semestre 4nj / 2j / 6t
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
# ModuleImpl 2nj / 1j / 3t
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (2, 1, 3)
# Deuxième semestre 2nj / 1j / 3t
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
# On supprime la première assiduité (sans invalider le cache)
assi: Assiduite = Assiduite.query.filter_by(etudid=etud.id).first()
db.session.delete(assi)
db.session.commit()
# Premier semestre 4nj / 2j / 6t (Identique car cache)
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
# ModuleImpl 1nj / 1j / 2t (Change car non cache)
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (1, 1, 2)
# Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
# On invalide maintenant le cache
scass.invalidate_assiduites_count(etud.id, formsemestre1.to_dict())
# Premier semestre 3nj / 2j / 5t (Change car cache invalidé)
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (3, 2, 5)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (3, 2, 5)
# ModuleImpl 1nj / 1j / 2t (Ne change pas car pas de changement)
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (1, 1, 2)
# Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)

View File

@ -191,7 +191,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
etudid = etuds[0]["etudid"]
_signal_absences_justificatifs(etudid)
nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = scass.get_assiduites_count(etudid, sem)
assert nbabs == 6, f"incorrect nbabs ({nbabs})"
assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"

View File

@ -396,7 +396,7 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre):
for etud in formsemestre.etuds:
base_date = datetime.datetime(
2022, 9, [5, 12, 19, 26][random.randint(0, 3)], 8, 0, 0
2021, 9, [6, 13, 20, 27][random.randint(0, 3)], 8, 0, 0
)
base_date = localize_datetime(base_date)