diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 2e2298dbe8..f890b6a615 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -41,7 +41,6 @@ from app import db from app.models import ModuleImpl, Evaluation, EvaluationUEPoids 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 @@ -92,6 +91,10 @@ class ModuleImplResults: ne donnent pas de coef vers cette UE. """ self.load_notes() + self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) + """1 bool par etud, indique si sa moyenne de module vient de la session2""" + self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index) + """1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage""" def load_notes(self): # ré-écriture de df_load_modimpl_notes """Charge toutes les notes de toutes les évaluations du module. @@ -135,8 +138,11 @@ class ModuleImplResults: eval_df = self._load_evaluation_notes(evaluation) # is_complete ssi tous les inscrits (non dem) au semestre ont une note # ou évaluation déclarée "à prise en compte immédiate" - is_complete = evaluation.publish_incomplete or ( - not (inscrits_module - set(eval_df.index)) + # Les évaluations de rattrapage et 2eme session sont toujours incomplètes + # car on calcule leur moyenne à part. + is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( + evaluation.publish_incomplete + or (not (inscrits_module - set(eval_df.index))) ) self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete @@ -212,6 +218,33 @@ class ModuleImplResults: self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0 ) / [e.note_max / 20.0 for e in moduleimpl.evaluations] + def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl): + """L'évaluation de rattrapage de ce module, ou None s'il n'en a pas. + Rattrapage: la moyenne du module est la meilleure note entre moyenne + des autres évals et la note eval rattrapage. + """ + eval_list = [ + e + for e in moduleimpl.evaluations + if e.evaluation_type == scu.EVALUATION_RATTRAPAGE + ] + if eval_list: + return eval_list[0] + return None + + def get_evaluation_session2(self, moduleimpl: ModuleImpl): + """L'évaluation de deuxième session de ce module, ou None s'il n'en a pas. + Session 2: remplace la note de moyenne des autres évals. + """ + eval_list = [ + e + for e in moduleimpl.evaluations + if e.evaluation_type == scu.EVALUATION_SESSION2 + ] + if eval_list: + return eval_list[0] + return None + class ModuleImplResultsAPC(ModuleImplResults): "Calcul des moyennes de modules à la mode BUT" @@ -229,7 +262,7 @@ class ModuleImplResultsAPC(ModuleImplResults): ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ne donnent pas de coef vers cette UE. """ - moduleimpl = ModuleImpl.query.get(self.moduleimpl_id) + modimpl = ModuleImpl.query.get(self.moduleimpl_id) nb_etuds, nb_evals = self.evals_notes.shape nb_ues = evals_poids_df.shape[1] assert evals_poids_df.shape[0] == nb_evals # compat notes/poids @@ -237,11 +270,11 @@ class ModuleImplResultsAPC(ModuleImplResults): 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_coefs = self.get_evaluations_coefs(modimpl) evals_poids = evals_poids_df.values * evals_coefs # -> evals_poids shape : (nb_evals, nb_ues) assert evals_poids.shape == (nb_evals, nb_ues) - evals_notes_20 = self.get_eval_notes_sur_20(moduleimpl) + evals_notes_20 = self.get_eval_notes_sur_20(modimpl) # Les poids des évals pour chaque étudiant: là où il a des notes # non neutralisées @@ -262,6 +295,45 @@ class ModuleImplResultsAPC(ModuleImplResults): etuds_moy_module = np.sum( evals_poids_etuds * evals_notes_stacked, axis=1 ) / np.sum(evals_poids_etuds, axis=1) + + # Session2 : quand elle existe, remplace la note de module + eval_session2 = self.get_evaluation_session2(modimpl) + if eval_session2: + notes_session2 = self.evals_notes[eval_session2.id].values + # n'utilise que les notes valides (pas ATT, EXC, ABS, NaN) + etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE + etuds_moy_module = np.where( + etuds_use_session2[:, np.newaxis], + np.tile( + (notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis], + nb_ues, + ), + etuds_moy_module, + ) + self.etuds_use_session2 = pd.Series( + etuds_use_session2, index=self.evals_notes.index + ) + else: + # Rattrapage: remplace la note de module ssi elle est supérieure + eval_rat = self.get_evaluation_rattrapage(modimpl) + if eval_rat: + notes_rat = self.evals_notes[eval_rat.id].values + # remplace les notes invalides (ATT, EXC...) par des NaN + notes_rat = np.where( + notes_rat > scu.NOTES_ABSENCE, + notes_rat / (eval_rat.note_max / 20.0), + np.nan, + ) + # prend le max + etuds_use_rattrapage = notes_rat > etuds_moy_module + etuds_moy_module = np.where( + etuds_use_rattrapage[:, np.newaxis], + np.tile(notes_rat[:, np.newaxis], nb_ues), + etuds_moy_module, + ) + self.etuds_use_rattrapage = pd.Series( + etuds_use_rattrapage, index=self.evals_notes.index + ) self.etuds_moy_module = pd.DataFrame( etuds_moy_module, index=self.evals_notes.index, @@ -371,8 +443,42 @@ class ModuleImplResultsClassic(ModuleImplResults): evals_coefs_etuds * evals_notes_20, axis=1 ) / np.sum(evals_coefs_etuds, axis=1) + # Session2 : quand elle existe, remplace la note de module + eval_session2 = self.get_evaluation_session2(modimpl) + if eval_session2: + notes_session2 = self.evals_notes[eval_session2.id].values + # n'utilise que les notes valides (pas ATT, EXC, ABS, NaN) + etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE + etuds_moy_module = np.where( + etuds_use_session2, + notes_session2 / (eval_session2.note_max / 20.0), + etuds_moy_module, + ) + self.etuds_use_session2 = pd.Series( + etuds_use_session2, index=self.evals_notes.index + ) + else: + # Rattrapage: remplace la note de module ssi elle est supérieure + eval_rat = self.get_evaluation_rattrapage(modimpl) + if eval_rat: + notes_rat = self.evals_notes[eval_rat.id].values + # remplace les notes invalides (ATT, EXC...) par des NaN + notes_rat = np.where( + notes_rat > scu.NOTES_ABSENCE, + notes_rat / (eval_rat.note_max / 20.0), + np.nan, + ) + # prend le max + etuds_use_rattrapage = notes_rat > etuds_moy_module + etuds_moy_module = np.where( + etuds_use_rattrapage, notes_rat, etuds_moy_module + ) + self.etuds_use_rattrapage = pd.Series( + etuds_use_rattrapage, index=self.evals_notes.index + ) self.etuds_moy_module = pd.Series( etuds_moy_module, index=self.evals_notes.index, ) + return self.etuds_moy_module diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index f1e481cf1f..12dd9d12bc 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -619,7 +619,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): prefs=prefs, ) - if nbeval: # boite autour des evaluations (en pdf) + if nbeval: # boite autour des évaluations (en pdf) P[-1]["_pdf_style"].append( ("BOX", (1, 1 - nbeval), (-1, 0), 0.2, self.PDF_LIGHT_GRAY) ) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index ab178c1503..2f5efbc4ee 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -207,7 +207,7 @@ def evaluation_create_form( { "size": 6, "type": "float", - "explanation": "coef. dans le module (choisi librement par l'enseignant)", + "explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)", "allow_null": False, }, )