diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index cc51e2a28f..9441c03165 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 e7f97c13d8..60361c32c4 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) @@ -124,7 +123,7 @@ class BonusSport: # Annule les coefs des modules où l'étudiant n'est pas inscrit: modimpl_coefs_etuds = np.where( modimpl_inscr_spo_stacked, - np.stack([modimpl_coefs_spo.T] * nb_etuds), + np.stack([modimpl_coefs_spo] * nb_etuds), 0.0, ) else: @@ -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 bf4afe7a01..2e2298dbe8 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: @@ -233,6 +235,8 @@ class ModuleImplResultsAPC(ModuleImplResults): assert evals_poids_df.shape[0] == nb_evals # compat notes/poids if nb_etuds == 0: return pd.DataFrame(index=[], columns=evals_poids_df.columns) + if nb_ues == 0: + return pd.DataFrame(index=self.evals_notes.index, columns=[]) evals_coefs = self.get_evaluations_coefs(moduleimpl) evals_poids = evals_poids_df.values * evals_coefs # -> evals_poids shape : (nb_evals, nb_ues) @@ -289,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 d7ed476671..8a8057ac34 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 @@ -263,9 +264,10 @@ def compute_ue_moys_apc( # # Version vectorisée # - etud_moy_ue = np.sum( - modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_ue = np.sum( + modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) return pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, # les etudids @@ -379,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 4423f3fa16..c4697238d2 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 8d52d0c329..58c505ee54 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/forms/main/config_apo.py b/app/forms/main/config_apo.py index 6151f2f311..6ebb6a8b30 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -70,7 +70,7 @@ class CodesDecisionsForm(FlaskForm): ATT = _build_code_field("ATT") CMP = _build_code_field("CMP") DEF = _build_code_field("DEF") - DEM = _build_code_field("DEF") + DEM = _build_code_field("DEM") NAR = _build_code_field("NAR") RAT = _build_code_field("RAT") submit = SubmitField("Valider") diff --git a/app/models/config.py b/app/models/config.py index 9c9c563850..8a56d3879e 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -178,7 +178,7 @@ class ScoDocSiteConfig(db.Model): return getattr(bonus_sport, func_name) except AttributeError: raise ScoValueError( - f"""Fonction de calcul maison inexistante: {func_name}. + f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}". (contacter votre administrateur local).""" ) diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index 23b500393f..25442194f2 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -276,14 +276,24 @@ class TF(object): ) ok = 0 if typ[:3] == "int" or typ == "float" or typ == "real": - if "min_value" in descr and val < descr["min_value"]: + if ( + val != "" + and val != None + and "min_value" in descr + and val < descr["min_value"] + ): msg.append( "La valeur (%d) du champ '%s' est trop petite (min=%s)" % (val, field, descr["min_value"]) ) ok = 0 - if "max_value" in descr and val > descr["max_value"]: + if ( + val != "" + and val != None + and "max_value" in descr + and val > descr["max_value"] + ): msg.append( "La valeur (%s) du champ '%s' est trop grande (max=%s)" % (val, field, descr["max_value"]) diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 5fc3b8b4d1..b49b6159bd 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -456,6 +456,11 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None): return bonus +def bonus_iutlemans(notes_sport, coefs, infos=None): + "fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente" + return 0.0 + + def bonus_iutlr(notes_sport, coefs, infos=None): """Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 565d1168b2..04a2fe2c56 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -125,8 +125,8 @@ APO_NEWLINE = "\r\n" def _apo_fmt_note(note): "Formatte une note pour Apogée (séparateur décimal: ',')" - if not note and isinstance(note, float): - return "" + # if not note and isinstance(note, float): changé le 31/1/2022, étrange ? + # return "" try: val = float(note) except ValueError: diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 6bcb8cc32c..823dd19fcd 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -141,7 +141,7 @@ BUG = "BUG" ALL = "ALL" -# Explication des codes (de demestre ou d'UE) +# Explication des codes (de semestre ou d'UE) CODES_EXPL = { ADC: "Validé par compensation", ADJ: "Validé par le Jury", @@ -154,6 +154,7 @@ CODES_EXPL = { DEF: "Défaillant", NAR: "Échec, non autorisé à redoubler", RAT: "En attente d'un rattrapage", + DEM: "Démission", } # Nota: ces explications sont personnalisables via le fichier # de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ae143cbc07..7bcfcd78df 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"""