diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index cc51e2a28..9441c0316 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -71,7 +71,7 @@ class BulletinBUT(ResultatsSemestreBUT): "bonus": fmt_note(self.bonus_ues[ue.id][etud.id]) if self.bonus_ues is not None and ue.id in self.bonus_ues else fmt_note(0.0), - "malus": None, # XXX TODO voir ce qui est ici + "malus": self.malus[ue.id][etud.id], "capitalise": None, # "AAAA-MM-JJ" TODO "ressources": self.etud_ue_mod_results(etud, ue, self.ressources), "saes": self.etud_ue_mod_results(etud, ue, self.saes), diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 485c233f0..60361c32c 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -104,7 +104,6 @@ class BonusSport: # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport) # ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus) nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2] - nb_ues = len(ues) # Enlève les NaN du numérateur: sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0) @@ -162,7 +161,7 @@ class BonusSport: """ raise NotImplementedError("méthode virtuelle") - def get_bonus_ues(self) -> pd.Series: + def get_bonus_ues(self) -> pd.DataFrame: """Les bonus à appliquer aux UE Résultat: DataFrame de float, index etudid, columns: ue.id """ diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index f79982f7c..2e2298dbe 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -43,6 +43,8 @@ from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_utils import ModuleType + @dataclass class EvaluationEtat: @@ -291,7 +293,12 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... # Initialise poids non enregistrés: - default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0 + default_poids = ( + 1.0 + if modimpl.module.ue.type == UE_SPORT + or modimpl.module.module_type == ModuleType.MALUS + else 0.0 + ) if np.isnan(evals_poids.values.flat).any(): ue_coefs = modimpl.module.get_ue_coef_dict() diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 4c4f2a0c2..8a8057ac3 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -27,6 +27,7 @@ """Fonctions de calcul des moyennes d'UE (classiques ou BUT) """ +from re import X import numpy as np import pandas as pd @@ -380,3 +381,42 @@ def compute_ue_moys_classic( etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df + + +def compute_malus( + formsemestre: FormSemestre, + sem_modimpl_moys: np.array, + ues: list[UniteEns], + modimpl_inscr_df: pd.DataFrame, +) -> pd.DataFrame: + """Calcul le malus sur les UE + Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS. + Leurs notes sont positives ou négatives. leur somme sera _soustraite_ à la moyenne + de chaque UE. + Arguments: + - sem_modimpl_moys : + notes moyennes aux modules (tous les étuds x tous les modimpls) + floats avec des NaN. + En classique: sem_matrix, ndarray (etuds x modimpls) + En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus) + - ues: les ues du semestre (incluant le bonus sport) + - modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl) + + Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN) + """ + ues_idx = [ue.id for ue in ues] + malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float) + for ue in ues: + if ue.type != UE_SPORT: + modimpl_mask = np.array( + [ + (m.module.module_type == ModuleType.MALUS) + and (m.module.ue.id == ue.id) + for m in formsemestre.modimpls_sorted + ] + ) + malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1) + malus[ue.id] = malus_moys + + malus.fillna(0.0, inplace=True) + return malus diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 4423f3fa1..c4697238d 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -68,6 +68,12 @@ class ResultatsSemestreBUT(NotesTableCompat): 1.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 len(modimpls_sport) > 0: bonus_class = ScoDocSiteConfig.get_bonus_sport_class() diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 8d52d0c32..58c505ee5 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -71,6 +71,16 @@ class ResultatsSemestreClassic(NotesTableCompat): self.modimpl_coefs, modimpl_standards_mask, ) + # --- Modules de MALUS sur les UEs et la moyenne générale + self.malus = moy_ue.compute_malus( + self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df + ) + self.etud_moy_ue -= self.malus + # ajuste la moyenne générale (à l'aide des coefs d'UE) + self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum( + axis=1 + ) / self.etud_coef_ue_df.sum(axis=1) + # --- Bonus Sport & Culture bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ae143cbc0..7bcfcd78d 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -115,22 +115,30 @@ def do_module_create(args) -> int: return r -def module_create(matiere_id=None, module_type=None, semestre_id=None): - """Création d'un module""" +def module_create( + matiere_id=None, module_type=None, semestre_id=None, formation_id=None +): + """Formulaire de création d'un module + Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal). + Sinon, donne le choix de l'UE de rattachement et utilise la première + matière de cette UE (si elle n'existe pas, la crée). + """ from app.scodoc import sco_formations from app.scodoc import sco_edit_ue - matiere = Matiere.query.get_or_404(matiere_id) - if matiere is None: - raise ScoValueError("invalid matiere !") - ue = matiere.ue - parcours = ue.formation.get_parcours() + if matiere_id: + matiere = Matiere.query.get_or_404(matiere_id) + ue = matiere.ue + formation = ue.formation + else: + formation = Formation.query.get_or_404(formation_id) + parcours = formation.get_parcours() is_apc = parcours.APC_SAE - ues = ue.formation.ues.order_by( + ues = formation.ues.order_by( UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme ).all() # cherche le numero adéquat (pour placer le module en fin de liste) - modules = matiere.ue.formation.modules.all() + modules = formation.modules.all() if modules: default_num = max([m.numero or 0 for m in modules]) + 10 else: @@ -143,9 +151,11 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): H = [ html_sco_header.sco_header(page_title=f"Création {object_name}"), ] - if is_apc: + if not matiere_id: H += [ - f"""