418 lines
17 KiB
Python
418 lines
17 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Résultats semestres BUT
|
|
"""
|
|
import time
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from flask import g, url_for
|
|
|
|
from app import log
|
|
from app.comp import moy_ue, moy_sem, inscr_mod
|
|
from app.comp.res_common import NotesTableCompat
|
|
from app.comp.bonus_spo import BonusSport
|
|
from app.models import ScoDocSiteConfig
|
|
from app.models.etudiants import Identite
|
|
from app.models.moduleimpls import ModuleImpl
|
|
from app.models.ues import UniteEns
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_codes_parcours
|
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
|
import app.scodoc.sco_utils as scu
|
|
|
|
|
|
class ResultatsSemestreBUT(NotesTableCompat):
|
|
"""Résultats BUT: organisation des calculs"""
|
|
|
|
_cached_attrs = NotesTableCompat._cached_attrs + (
|
|
"modimpl_coefs_df",
|
|
"modimpls_evals_poids",
|
|
"sem_cube",
|
|
)
|
|
|
|
def __init__(self, formsemestre):
|
|
super().__init__(formsemestre)
|
|
self.modimpl_coefs_df = None
|
|
"""DataFrame, row UEs(sans bonus), cols modimplid, value coef"""
|
|
self.sem_cube = None
|
|
"""ndarray (etuds x modimpl x ue)"""
|
|
|
|
if not self.load_cached():
|
|
t0 = time.time()
|
|
self.compute()
|
|
t1 = time.time()
|
|
self.store()
|
|
t2 = time.time()
|
|
log(
|
|
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
|
)
|
|
|
|
def compute(self):
|
|
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
|
(
|
|
self.sem_cube,
|
|
self.modimpls_evals_poids,
|
|
self.modimpls_results,
|
|
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
|
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
|
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
|
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
|
)
|
|
# l'idx de la colonne du mod modimpl.id est
|
|
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
|
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
|
|
|
# Masque de tous les modules _sauf_ les bonus (sport)
|
|
modimpls_mask = [
|
|
modimpl.module.ue.type != UE_SPORT
|
|
for modimpl in self.formsemestre.modimpls_sorted
|
|
]
|
|
|
|
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
|
self.sem_cube,
|
|
self.etuds,
|
|
self.formsemestre.modimpls_sorted,
|
|
self.ues,
|
|
self.modimpl_inscr_df,
|
|
self.modimpl_coefs_df,
|
|
modimpls_mask,
|
|
)
|
|
# Les coefficients d'UE ne sont pas utilisés en APC
|
|
self.etud_coef_ue_df = pd.DataFrame(
|
|
0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
|
|
)
|
|
|
|
# --- Modules de MALUS sur les UEs
|
|
self.malus = moy_ue.compute_malus(
|
|
self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df
|
|
)
|
|
self.etud_moy_ue -= self.malus
|
|
|
|
# --- Bonus Sport & Culture
|
|
if not all(modimpls_mask): # au moins un module bonus
|
|
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
|
|
if bonus_class is not None:
|
|
bonus: BonusSport = bonus_class(
|
|
self.formsemestre,
|
|
self.sem_cube,
|
|
self.ues,
|
|
self.modimpl_inscr_df,
|
|
self.modimpl_coefs_df.transpose(),
|
|
self.etud_moy_gen,
|
|
self.etud_moy_ue,
|
|
)
|
|
self.bonus_ues = bonus.get_bonus_ues()
|
|
if self.bonus_ues is not None:
|
|
self.etud_moy_ue += self.bonus_ues # somme les dataframes
|
|
|
|
# Clippe toutes les moyennes d'UE dans [0,20]
|
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
|
|
|
# Moyenne générale indicative:
|
|
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
|
# donc la moyenne indicative)
|
|
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
|
# self.etud_moy_ue, self.modimpl_coefs_df
|
|
# )
|
|
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
|
self.etud_moy_ue,
|
|
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
|
|
formation_id=self.formsemestre.formation_id,
|
|
skip_empty_ues=sco_preferences.get_preference(
|
|
"but_moy_skip_empty_ues", self.formsemestre.id
|
|
),
|
|
)
|
|
# --- UE capitalisées
|
|
self.apply_capitalisation()
|
|
|
|
# --- Classements:
|
|
self.compute_rangs()
|
|
|
|
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
|
"""La moyenne de l'étudiant dans le moduleimpl
|
|
En APC, il s'agit d'une moyenne indicative sans valeur.
|
|
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
|
"""
|
|
mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
|
etud_idx = self.etud_index[etudid]
|
|
# moyenne sur les UE:
|
|
if len(self.sem_cube[etud_idx, mod_idx]):
|
|
return np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
|
return np.nan
|
|
|
|
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
|
"""Détermine le coefficient de l'UE pour cet étudiant.
|
|
N'est utilisé que pour l'injection des UE capitalisées dans la
|
|
moyenne générale.
|
|
En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE.
|
|
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
|
|
"""
|
|
return self.modimpl_coefs_df.loc[ue.id].sum()
|
|
|
|
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
|
|
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
|
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
|
|
"""
|
|
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
|
|
coefs = self.modimpl_coefs_df # row UE, cols modimpl
|
|
modimpls = [
|
|
modimpl
|
|
for modimpl in self.formsemestre.modimpls_sorted
|
|
if (coefs[modimpl.id][ue_id] != 0)
|
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
|
]
|
|
if not with_bonus:
|
|
return [
|
|
modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT
|
|
]
|
|
return modimpls
|
|
|
|
def get_table_moyennes_triees(self, convert_values=False) -> list:
|
|
"""Result: tuple avec
|
|
- rows: liste de dicts { column_id : value }
|
|
- titles: { column_id : title }
|
|
- columns_ids: (liste des id de colonnes)
|
|
|
|
. Si convert_values, transforme les notes en chaines ("12.34").
|
|
Les colonnes générées sont:
|
|
etudid
|
|
rang : rang indicatif (basé sur moy gen)
|
|
moy_gen : moy gen indicative
|
|
moy_ue_<ue_id>, ..., les moyennes d'UE
|
|
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
|
|
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
|
|
|
|
On ajoute aussi des attributs:
|
|
- pour les lignes:
|
|
_css_row_class (inutilisé pour le monent)
|
|
_<column_id>_class classe css:
|
|
- la moyenne générale a la classe col_moy_gen
|
|
- les colonnes SAE ont la classe col_sae
|
|
- les colonnes Resources ont la classe col_res
|
|
- les colonnes d'UE ont la classe col_ue
|
|
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
|
|
_<column_id>_order : clé de tri
|
|
"""
|
|
|
|
def fmt_note(x):
|
|
return scu.fmt_note(x) if convert_values else x
|
|
|
|
barre_moy = (
|
|
self.formsemestre.formation.get_parcours().BARRE_MOY - scu.NOTES_TOLERANCE
|
|
)
|
|
barre_valid_ue = self.formsemestre.formation.get_parcours().NOTES_BARRE_VALID_UE
|
|
NO_NOTE = "-" # contenu des cellules sans notes
|
|
rows = []
|
|
titles = {"rang": "Rg"} # column_id : title
|
|
|
|
def add_cell(
|
|
row: dict, col_id: str, title: str, content: str, classes: str = ""
|
|
):
|
|
"Add a row to our table. classes is a list of css class names"
|
|
row[col_id] = content
|
|
if classes:
|
|
row[f"_{col_id}_class"] = classes
|
|
if not col_id in titles:
|
|
titles[col_id] = title
|
|
if classes:
|
|
titles[f"_{col_id}_class"] = classes
|
|
|
|
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
|
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
|
modimpl_ids = set() # modimpl effectivement présents dans la table
|
|
for etudid in etuds_inscriptions:
|
|
etud = Identite.query.get(etudid)
|
|
row = {"etudid": etudid}
|
|
# --- Rang
|
|
add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang")
|
|
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
|
|
# --- Identité étudiant
|
|
add_cell(row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
|
add_cell(row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
|
add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail")
|
|
add_cell(row, "nom_short", "Nom", etud.nom_short, "identite_court")
|
|
row["_nom_short_target"] = url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=self.formsemestre.id,
|
|
etudid=etudid,
|
|
)
|
|
row[f"_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
|
row["_nom_disp_target"] = row["_nom_short_target"]
|
|
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
|
self._recap_etud_groups_infos(etudid, row, titles)
|
|
# --- Moyenne générale
|
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
|
note_class = ""
|
|
if moy_gen is False:
|
|
moy_gen = NO_NOTE
|
|
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
|
note_class = " moy_inf"
|
|
add_cell(
|
|
row,
|
|
"moy_gen",
|
|
"Moy",
|
|
fmt_note(moy_gen),
|
|
"col_moy_gen" + note_class,
|
|
)
|
|
# --- Moyenne d'UE
|
|
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
|
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
|
if ue_status is not None:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
val = ue_status["moy"]
|
|
note_class = ""
|
|
if isinstance(val, float):
|
|
if val < barre_moy:
|
|
note_class = " moy_inf"
|
|
elif val >= barre_valid_ue:
|
|
note_class = " moy_ue_valid"
|
|
add_cell(
|
|
row,
|
|
col_id,
|
|
ue.acronyme,
|
|
fmt_note(val),
|
|
"col_ue" + note_class,
|
|
)
|
|
# Les moyennes des ressources et SAÉs dans cette UE
|
|
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
|
|
if ue_status["is_capitalized"]:
|
|
val = "-c-"
|
|
else:
|
|
modimpl_results = self.modimpls_results.get(modimpl.id)
|
|
if modimpl_results: # pas bonus
|
|
moys_vers_ue = modimpl_results.etuds_moy_module.get(
|
|
ue.id
|
|
)
|
|
val = (
|
|
moys_vers_ue.get(etudid, "?")
|
|
if moys_vers_ue is not None
|
|
else ""
|
|
)
|
|
else:
|
|
val = ""
|
|
|
|
col_id = (
|
|
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
|
)
|
|
add_cell(
|
|
row,
|
|
col_id,
|
|
modimpl.module.code,
|
|
fmt_note(val),
|
|
# class col_res mod_ue_123
|
|
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
|
)
|
|
modimpl_ids.add(modimpl.id)
|
|
|
|
rows.append(row)
|
|
|
|
# tri par rang croissant
|
|
rows.sort(key=lambda e: e["_rang_order"])
|
|
|
|
# INFOS POUR FOOTER
|
|
bottom_infos = self._recap_bottom_infos(
|
|
[ue for ue in ues if ue.type != UE_SPORT], modimpl_ids, fmt_note
|
|
)
|
|
|
|
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
|
footer_rows = []
|
|
for bottom_line in bottom_infos:
|
|
row = bottom_infos[bottom_line]
|
|
# Cases vides à styler:
|
|
row["moy_gen"] = row.get("moy_gen", "")
|
|
row["_moy_gen_class"] = "col_moy_gen"
|
|
# titre de la ligne:
|
|
row["prenom"] = row["nom_short"] = bottom_line.capitalize()
|
|
row["_tr_class"] = bottom_line.lower()
|
|
footer_rows.append(row)
|
|
return (
|
|
rows,
|
|
footer_rows,
|
|
titles,
|
|
[title for title in titles if not title.startswith("_")],
|
|
)
|
|
|
|
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
|
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
|
bottom_infos = { # { key : row } avec key = min, max, moy, coef
|
|
"min": {},
|
|
"max": {},
|
|
"moy": {},
|
|
"coef": {},
|
|
}
|
|
# --- ECTS
|
|
row = {}
|
|
for ue in ues:
|
|
row[f"moy_ue_{ue.id}"] = ue.ects
|
|
row[f"_moy_ue_{ue.id}_class"] = "col_ue"
|
|
# style cases vides pour borders verticales
|
|
bottom_infos["coef"][f"moy_ue_{ue.id}"] = ""
|
|
bottom_infos["coef"][f"_moy_ue_{ue.id}_class"] = "col_ue"
|
|
row["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
|
|
row["_moy_gen_class"] = "col_moy_gen"
|
|
bottom_infos["ects"] = row
|
|
|
|
# --- MIN, MAX, MOY
|
|
row_min, row_max, row_moy = {}, {}, {}
|
|
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
|
|
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
|
|
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
|
|
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
|
|
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
|
|
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
|
row_min[f"_{col_id}_class"] = "col_ue"
|
|
row_max[f"_{col_id}_class"] = "col_ue"
|
|
row_moy[f"_{col_id}_class"] = "col_ue"
|
|
|
|
for modimpl in self.formsemestre.modimpls_sorted:
|
|
if modimpl.id in modimpl_ids:
|
|
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
|
bottom_infos["coef"][col_id] = fmt_note(
|
|
self.modimpl_coefs_df[modimpl.id][ue.id]
|
|
)
|
|
i = self.modimpl_coefs_df.columns.get_loc(modimpl.id)
|
|
j = self.modimpl_coefs_df.index.get_loc(ue.id)
|
|
notes = self.sem_cube[:, i, j]
|
|
row_min[col_id] = fmt_note(np.nanmin(notes))
|
|
row_max[col_id] = fmt_note(np.nanmax(notes))
|
|
row_moy[col_id] = fmt_note(np.nanmean(notes))
|
|
|
|
bottom_infos["min"] = row_min
|
|
bottom_infos["max"] = row_max
|
|
bottom_infos["moy"] = row_moy
|
|
return bottom_infos
|
|
|
|
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
|
|
"""Ajoute à row les colonnes sur les groupes pour etud"""
|
|
# XXX à remonter dans res_common
|
|
# dec = self.get_etud_decision_sem(etudid)
|
|
# if dec:
|
|
# codes_nb[dec["code"]] += 1
|
|
row_class = ""
|
|
etud_etat = self.get_etud_etat(etudid)
|
|
if etud_etat == sco_codes_parcours.DEM:
|
|
gr_name = "Dém."
|
|
row_class = "dem"
|
|
elif etud_etat == sco_codes_parcours.DEF:
|
|
gr_name = "Déf."
|
|
row_class = "def"
|
|
else:
|
|
# XXX probablement à revoir pour utiliser données cachées,
|
|
# via get_etud_groups_in_partition ou autre
|
|
group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id)
|
|
gr_name = group["group_name"] or ""
|
|
row["group"] = gr_name
|
|
row["_group_class"] = "group"
|
|
if row_class:
|
|
row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class])
|
|
titles["group"] = "Gr"
|