From 7f32f1fb992e19651dde7ac56b3718f69fc1624e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 24 Feb 2024 16:49:41 +0100 Subject: [PATCH] Evaluations de type bonus. Implements #848 --- app/but/bulletin_but.py | 104 +++++++++++++---------- app/but/bulletin_but_pdf.py | 13 ++- app/comp/moy_mod.py | 104 ++++++++++++++++++++--- app/models/evaluations.py | 15 +++- app/scodoc/sco_bulletins.py | 9 +- app/scodoc/sco_bulletins_standard.py | 14 ++- app/scodoc/sco_evaluation_edit.py | 25 ++++-- app/scodoc/sco_evaluations.py | 25 +++--- app/scodoc/sco_liste_notes.py | 36 ++++---- app/scodoc/sco_moduleimpl_status.py | 20 +++-- app/scodoc/sco_saisie_notes.py | 11 ++- app/scodoc/sco_ue_external.py | 2 +- app/scodoc/sco_utils.py | 4 - app/static/css/releve-but.css | 6 +- app/static/css/scodoc.css | 8 +- app/static/js/releve-but.js | 7 +- app/tables/recap.py | 25 ++++-- app/templates/scodoc/help/evaluations.j2 | 26 ++++-- sco_version.py | 7 +- tests/unit/test_notes_rattrapage.py | 5 +- 20 files changed, 312 insertions(+), 154 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 918d14fd2..87b175468 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -104,9 +104,11 @@ class BulletinBUT: "competence": None, # XXX TODO lien avec référentiel "moyenne": None, # Le bonus sport appliqué sur cette UE - "bonus": fmt_note(res.bonus_ues[ue.id][etud.id]) - if res.bonus_ues is not None and ue.id in res.bonus_ues - else fmt_note(0.0), + "bonus": ( + fmt_note(res.bonus_ues[ue.id][etud.id]) + if res.bonus_ues is not None and ue.id in res.bonus_ues + else fmt_note(0.0) + ), "malus": fmt_note(res.malus[ue.id][etud.id]), "capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "ressources": self.etud_ue_mod_results(etud, ue, res.ressources), @@ -181,14 +183,16 @@ class BulletinBUT: "is_external": ue_capitalisee.is_external, "date_capitalisation": ue_capitalisee.event_date, "formsemestre_id": ue_capitalisee.formsemestre_id, - "bul_orig_url": url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - etudid=etud.id, - formsemestre_id=ue_capitalisee.formsemestre_id, - ) - if ue_capitalisee.formsemestre_id - else None, + "bul_orig_url": ( + url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + etudid=etud.id, + formsemestre_id=ue_capitalisee.formsemestre_id, + ) + if ue_capitalisee.formsemestre_id + else None + ), "ressources": {}, # sans détail en BUT "saes": {}, } @@ -227,13 +231,15 @@ class BulletinBUT: "id": modimpl.id, "titre": modimpl.module.titre, "code_apogee": modimpl.module.code_apogee, - "url": url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - if has_request_context() - else "na", + "url": ( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ) + if has_request_context() + else "na" + ), "moyenne": { # # moyenne indicative de module: moyenne des UE, # # ignorant celles sans notes (nan) @@ -242,18 +248,20 @@ class BulletinBUT: # "max": fmt_note(moyennes_etuds.max()), # "moy": fmt_note(moyennes_etuds.mean()), }, - "evaluations": [ - self.etud_eval_results(etud, e) - for e in modimpl.evaluations - if (e.visibulletin or version == "long") - and (e.id in modimpl_results.evaluations_etat) - and ( - modimpl_results.evaluations_etat[e.id].is_complete - or self.prefs["bul_show_all_evals"] - ) - ] - if version != "short" - else [], + "evaluations": ( + [ + self.etud_eval_results(etud, e) + for e in modimpl.evaluations + if (e.visibulletin or version == "long") + and (e.id in modimpl_results.evaluations_etat) + and ( + modimpl_results.evaluations_etat[e.id].is_complete + or self.prefs["bul_show_all_evals"] + ) + ] + if version != "short" + else [] + ), } return d @@ -274,9 +282,11 @@ class BulletinBUT: poids = collections.defaultdict(lambda: 0.0) d = { "id": e.id, - "coef": fmt_note(e.coefficient) - if e.evaluation_type == scu.EVALUATION_NORMALE - else None, + "coef": ( + fmt_note(e.coefficient) + if e.evaluation_type == Evaluation.EVALUATION_NORMALE + else None + ), "date_debut": e.date_debut.isoformat() if e.date_debut else None, "date_fin": e.date_fin.isoformat() if e.date_fin else None, "description": e.description, @@ -291,18 +301,20 @@ class BulletinBUT: "moy": fmt_note(notes_ok.mean(), note_max=e.note_max), }, "poids": poids, - "url": url_for( - "notes.evaluation_listenotes", - scodoc_dept=g.scodoc_dept, - evaluation_id=e.id, - ) - if has_request_context() - else "na", + "url": ( + url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=e.id, + ) + if has_request_context() + else "na" + ), # deprecated (supprimer avant #sco9.7) "date": e.date_debut.isoformat() if e.date_debut else None, - "heure_debut": e.date_debut.time().isoformat("minutes") - if e.date_debut - else None, + "heure_debut": ( + e.date_debut.time().isoformat("minutes") if e.date_debut else None + ), "heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None, } return d @@ -524,9 +536,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"])) diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index f56b86c0f..999846f77 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -24,7 +24,7 @@ from reportlab.lib.colors import blue from reportlab.lib.units import cm, mm from reportlab.platypus import Paragraph, Spacer -from app.models import ScoDocSiteConfig +from app.models import Evaluation, ScoDocSiteConfig from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc import gen_tables from app.scodoc.codes_cursus import UE_SPORT @@ -422,7 +422,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()): "lignes des évaluations" for e in evaluations: - coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*" + coef = ( + e["coef"] + if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE + else "*" + ) t = { "titre": f"{e['description'] or ''}", "moyenne": e["note"]["value"], @@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): ), "coef": coef, "_coef_pdf": Paragraph( - f"{coef}" + f"""{ + coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS + else "bonus" + }""" ), "_pdf_style": [ ( diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 94f056b2f..a977894d6 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -157,8 +157,7 @@ class ModuleImplResults: etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = ( - (evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE) - or (evaluation.evaluation_type == scu.EVALUATION_SESSION2) + (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE) or (evaluation.publish_incomplete) or (not etudids_sans_note) ) @@ -240,19 +239,20 @@ class ModuleImplResults: ).formsemestre.inscriptions ] - def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array: + def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array: """Coefficients des évaluations. - Les coefs des évals incomplètes et non "normales" (session 2, rattrapage) - sont zéro. + Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro. Résultat: 2d-array of floats, shape (nb_evals, 1) """ return ( np.array( [ - e.coefficient - if e.evaluation_type == scu.EVALUATION_NORMALE - else 0.0 - for e in moduleimpl.evaluations + ( + e.coefficient + if e.evaluation_type == Evaluation.EVALUATION_NORMALE + else 0.0 + ) + for e in modimpl.evaluations ], dtype=float, ) @@ -285,7 +285,7 @@ class ModuleImplResults: for (etudid, x) in self.evals_notes[evaluation_id].items() } - def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl): + def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None: """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. @@ -293,25 +293,41 @@ class ModuleImplResults: eval_list = [ e for e in moduleimpl.evaluations - if e.evaluation_type == scu.EVALUATION_RATTRAPAGE + if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE ] if eval_list: return eval_list[0] return None - def get_evaluation_session2(self, moduleimpl: ModuleImpl): + def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None: """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 e.evaluation_type == Evaluation.EVALUATION_SESSION2 ] if eval_list: return eval_list[0] return None + def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]: + """Les évaluations bonus de ce module, ou liste vide s'il n'en a pas.""" + return [ + e + for e in modimpl.evaluations + if e.evaluation_type == Evaluation.EVALUATION_BONUS + ] + + def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]: + """Les indices des évaluations bonus""" + return [ + i + for (i, e) in enumerate(modimpl.evaluations) + if e.evaluation_type == Evaluation.EVALUATION_BONUS + ] + class ModuleImplResultsAPC(ModuleImplResults): "Calcul des moyennes de modules à la mode BUT" @@ -356,7 +372,7 @@ class ModuleImplResultsAPC(ModuleImplResults): # et dans dans evals_poids_etuds # (rappel: la comparaison est toujours false face à un NaN) # shape: (nb_etuds, nb_evals, nb_ues) - poids_stacked = np.stack([evals_poids] * nb_etuds) + poids_stacked = np.stack([evals_poids] * nb_etuds) # nb_etuds, nb_evals, nb_ues evals_poids_etuds = np.where( np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, poids_stacked, @@ -364,10 +380,20 @@ class ModuleImplResultsAPC(ModuleImplResults): ) # Calcule la moyenne pondérée sur les notes disponibles: evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2) + # evals_notes_stacked shape: nb_etuds, nb_evals, nb_ues with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etuds_moy_module = np.sum( evals_poids_etuds * evals_notes_stacked, axis=1 ) / np.sum(evals_poids_etuds, axis=1) + # etuds_moy_module shape: nb_etuds x nb_ues + + # Application des évaluations bonus: + etuds_moy_module = self.apply_bonus( + etuds_moy_module, + modimpl, + evals_poids_df, + evals_notes_stacked, + ) # Session2 : quand elle existe, remplace la note de module eval_session2 = self.get_evaluation_session2(modimpl) @@ -416,6 +442,30 @@ class ModuleImplResultsAPC(ModuleImplResults): ) return self.etuds_moy_module + def apply_bonus( + self, + etuds_moy_module: pd.DataFrame, + modimpl: ModuleImpl, + evals_poids_df: pd.DataFrame, + evals_notes_stacked: np.ndarray, + ): + """Ajoute les points des évaluations bonus. + Il peut y avoir un nb quelconque d'évaluations bonus. + Les points sont directement ajoutés (ils peuvent être négatifs). + """ + evals_bonus = self.get_evaluations_bonus(modimpl) + if not evals_bonus: + return etuds_moy_module + poids_stacked = np.stack([evals_poids_df.values] * len(etuds_moy_module)) + for evaluation in evals_bonus: + eval_idx = evals_poids_df.index.get_loc(evaluation.id) + etuds_moy_module += ( + evals_notes_stacked[:, eval_idx, :] * poids_stacked[:, eval_idx, :] + ) + # Clip dans [0,20] + etuds_moy_module.clip(0, 20, out=etuds_moy_module) + return etuds_moy_module + def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe @@ -532,6 +582,13 @@ class ModuleImplResultsClassic(ModuleImplResults): evals_coefs_etuds * evals_notes_20, axis=1 ) / np.sum(evals_coefs_etuds, axis=1) + # Application des évaluations bonus: + etuds_moy_module = self.apply_bonus( + etuds_moy_module, + modimpl, + evals_notes_20, + ) + # Session2 : quand elle existe, remplace la note de module eval_session2 = self.get_evaluation_session2(modimpl) if eval_session2: @@ -571,3 +628,22 @@ class ModuleImplResultsClassic(ModuleImplResults): ) return self.etuds_moy_module + + def apply_bonus( + self, + etuds_moy_module: np.ndarray, + modimpl: ModuleImpl, + evals_notes_20: np.ndarray, + ): + """Ajoute les points des évaluations bonus. + Il peut y avoir un nb quelconque d'évaluations bonus. + Les points sont directement ajoutés (ils peuvent être négatifs). + """ + evals_bonus_idx = self.get_evaluations_bonus_idx(modimpl) + if not evals_bonus_idx: + return etuds_moy_module + for eval_idx in evals_bonus_idx: + etuds_moy_module += evals_notes_20[:, eval_idx] + # Clip dans [0,20] + etuds_moy_module.clip(0, 20, out=etuds_moy_module) + return etuds_moy_module diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 7da2d84cc..37e3ac794 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -23,8 +23,6 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365) NOON = datetime.time(12, 00) DEFAULT_EVALUATION_TIME = datetime.time(8, 0) -VALID_EVALUATION_TYPES = {0, 1, 2} - class Evaluation(db.Model): """Evaluation (contrôle, examen, ...)""" @@ -57,6 +55,17 @@ class Evaluation(db.Model): numero = db.Column(db.Integer, nullable=False, default=0) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) + EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer ! + EVALUATION_RATTRAPAGE = 1 + EVALUATION_SESSION2 = 2 + EVALUATION_BONUS = 3 + VALID_EVALUATION_TYPES = { + EVALUATION_NORMALE, + EVALUATION_RATTRAPAGE, + EVALUATION_SESSION2, + EVALUATION_BONUS, + } + def __repr__(self): return f"""• ' + e["name"], - "coef": ("" + e["coef_txt"] + "") - if prefs["bul_show_coef"] - else "", + "coef": ( + ( + f"{e['coef_txt']}" + if e["evaluation_type"] != Evaluation.EVALUATION_BONUS + else "bonus" + ) + if prefs["bul_show_coef"] + else "" + ), "_hidden": hidden, "_module_target": e["target_html"], # '_module_help' : , diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index b26ed9fbe..c7b0dd676 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -183,7 +183,8 @@ def evaluation_create_form( { "size": 6, "type": "float", # peut être négatif (!) - "explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)", + "explanation": """coef. dans le module (choisi librement par + l'enseignant, non utilisé pour rattrapage, 2ème session et bonus)""", "allow_null": False, }, ) @@ -195,7 +196,7 @@ def evaluation_create_form( "size": 4, "type": "float", "title": "Notes de 0 à", - "explanation": f"barème (note max actuelle: {min_note_max_str})", + "explanation": f"""barème (note max actuelle: {min_note_max_str}).""", "allow_null": False, "max_value": scu.NOTES_MAX, "min_value": min_note_max, @@ -206,7 +207,8 @@ def evaluation_create_form( { "size": 36, "type": "text", - "explanation": """type d'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".""", + "explanation": """type d'évaluation, apparait sur le bulletins longs. + Exemples: "contrôle court", "examen de TP", "examen final".""", }, ), ( @@ -230,16 +232,20 @@ def evaluation_create_form( { "input_type": "menu", "title": "Modalité", - "allowed_values": ( - scu.EVALUATION_NORMALE, - scu.EVALUATION_RATTRAPAGE, - scu.EVALUATION_SESSION2, - ), + "allowed_values": Evaluation.VALID_EVALUATION_TYPES, "type": "int", "labels": ( "Normale", "Rattrapage (remplace si meilleure note)", "Deuxième session (remplace toujours)", + ( + "Bonus " + + ( + "(pondéré par poids et ajouté aux moyennes de ce module)" + if is_apc + else "(ajouté à la moyenne de ce module)" + ) + ), ), }, ), @@ -251,7 +257,8 @@ def evaluation_create_form( { "size": 6, "type": "float", - "explanation": "importance de l'évaluation (multiplie les poids ci-dessous)", + "explanation": """importance de l'évaluation (multiplie les poids ci-dessous). + Non utilisé pour les bonus.""", "allow_null": False, }, ), diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 610cb255a..d157e0a30 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -217,19 +217,9 @@ def do_evaluation_etat( gr_incomplets = list(group_nb_missing.keys()) gr_incomplets.sort() - if ( - (total_nb_missing > 0) - and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE) - and (E["evaluation_type"] != scu.EVALUATION_SESSION2) - ): - complete = False - else: - complete = True - complete = ( - (total_nb_missing == 0) - or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE) - or (E["evaluation_type"] == scu.EVALUATION_SESSION2) + complete = (total_nb_missing == 0) or ( + E["evaluation_type"] != Evaluation.EVALUATION_NORMALE ) evalattente = (total_nb_missing > 0) and ( (total_nb_missing == total_nb_att) or E["publish_incomplete"] @@ -498,13 +488,14 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"): """Experimental: un tableau indiquant pour chaque évaluation le nombre de jours avant la publication des notes. - N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus. + N'indique que les évaluations "normales" (pas rattrapage, ni bonus, ni session2, + ni celles des modules de bonus/malus). """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) evaluations = formsemestre.get_evaluations() rows = [] for e in evaluations: - if (e.evaluation_type != scu.EVALUATION_NORMALE) or ( + if (e.evaluation_type != Evaluation.EVALUATION_NORMALE) or ( e.moduleimpl.module.module_type == ModuleType.MALUS ): continue @@ -610,13 +601,17 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True) # Indique l'UE ue = modimpl.module.ue H.append(f"

UE : {ue.acronyme}

") + if ( + modimpl.module.module_type == ModuleType.MALUS + or evaluation.evaluation_type == Evaluation.EVALUATION_BONUS + ): # store min/max values used by JS client-side checks: H.append( """-20. 20.""" ) else: - # date et absences (pas pour evals de malus) + # date et absences (pas pour evals bonus ni des modules de malus) if evaluation.date_debut is not None: H.append(f"

Réalisée le {evaluation.descr_date()} ") group_id = sco_groups.get_default_group(modimpl.formsemestre_id) diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 440584746..9e668c902 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -490,9 +490,9 @@ def _make_table_notes( rlinks = {"_table_part": "head"} for e in evaluations: rlinks[e.id] = "afficher" - rlinks[ - "_" + str(e.id) + "_help" - ] = "afficher seulement les notes de cette évaluation" + rlinks["_" + str(e.id) + "_help"] = ( + "afficher seulement les notes de cette évaluation" + ) rlinks["_" + str(e.id) + "_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, @@ -709,9 +709,9 @@ def _add_eval_columns( notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) if evaluation.date_debut: - titles[ - evaluation.id - ] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" + titles[evaluation.id] = ( + f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" + ) else: titles[evaluation.id] = f"{evaluation.description} " @@ -820,14 +820,17 @@ def _add_eval_columns( row_moys[evaluation.id] = scu.fmt_note( sum_notes / nb_notes, keep_numeric=keep_numeric ) - row_moys[ - "_" + str(evaluation.id) + "_help" - ] = "moyenne sur %d notes (%s le %s)" % ( - nb_notes, - evaluation.description, - evaluation.date_debut.strftime("%d/%m/%Y") - if evaluation.date_debut - else "", + row_moys["_" + str(evaluation.id) + "_help"] = ( + "moyenne sur %d notes (%s le %s)" + % ( + nb_notes, + evaluation.description, + ( + evaluation.date_debut.strftime("%d/%m/%Y") + if evaluation.date_debut + else "" + ), + ) ) else: row_moys[evaluation.id] = "" @@ -884,8 +887,9 @@ def _add_moymod_column( row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' if etudid in inscrits and not isinstance(val, str): notes.append(val) - nb_notes = nb_notes + 1 - sum_notes += val + if not np.isnan(val): + nb_notes = nb_notes + 1 + sum_notes += val row_coefs[col_id] = "(avec abs)" if is_apc: row_poids[col_id] = "à titre indicatif" diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 9ee69aec3..28021e13f 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -519,13 +519,15 @@ def _ligne_evaluation( partition_id=partition_id, select_first_partition=True, ) - if evaluation.evaluation_type in ( - scu.EVALUATION_RATTRAPAGE, - scu.EVALUATION_SESSION2, - ): + if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: tr_class = "mievr mievr_rattr" + elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: + tr_class = "mievr mievr_session2" + elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + tr_class = "mievr mievr_bonus" else: tr_class = "mievr" + if not evaluation.visibulletin: tr_class += " non_visible_inter" tr_class_1 = "mievr" @@ -563,13 +565,17 @@ def _ligne_evaluation( }" class="mievr_evalnodate">Évaluation sans date""" ) H.append(f"    {evaluation.description or ''}") - if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE: + if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: H.append( """rattrapage""" ) - elif evaluation.evaluation_type == scu.EVALUATION_SESSION2: + elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: H.append( - """session 2""" + """session 2""" + ) + elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + H.append( + """bonus""" ) # if etat["last_modif"]: diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 8f84e8c6c..a9195d29b 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -134,12 +134,12 @@ def _displayNote(val): return val -def _check_notes(notes: list[(int, float)], evaluation: Evaluation): - # XXX typehint : float or str +def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation): """notes is a list of tuples (etudid, value) mod is the module (used to ckeck type, for malus) returns list of valid notes (etudid, float value) - and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress + and 4 lists of etudid: + etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress """ note_max = evaluation.note_max or 0.0 module: Module = evaluation.moduleimpl.module @@ -148,7 +148,10 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation): scu.ModuleType.RESSOURCE, scu.ModuleType.SAE, ): - note_min = scu.NOTES_MIN + if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + note_min, note_max = -20, 20 + else: + note_min = scu.NOTES_MIN elif module.module_type == ModuleType.MALUS: note_min = -20.0 else: diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index 4d32a4053..f1840386b 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -175,7 +175,7 @@ def external_ue_inscrit_et_note( note_max=20.0, coefficient=1.0, publish_incomplete=True, - evaluation_type=scu.EVALUATION_NORMALE, + evaluation_type=Evaluation.EVALUATION_NORMALE, visibulletin=False, description="note externe", ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 6b3850997..7e03f38f7 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -454,10 +454,6 @@ NOTES_MENTIONS_LABS = ( "Excellent", ) -EVALUATION_NORMALE = 0 -EVALUATION_RATTRAPAGE = 1 -EVALUATION_SESSION2 = 2 - # Dates et années scolaires # Ces dates "pivot" sont paramétrables dans les préférences générales # on donne ici les valeurs par défaut. diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 25a31c972..b0c0f5332 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -273,6 +273,10 @@ section>div:nth-child(1) { min-width: 80px; display: inline-block; } +div.eval-bonus { + color: #197614; + background-color: pink; +} .ueBonus, .ueBonus h3 { @@ -280,7 +284,7 @@ section>div:nth-child(1) { color: #000 !important; } /* UE Capitalisée */ -.synthese .ue.capitalisee, +.synthese .ue.capitalisee, .ue.capitalisee>h3{ background: var(--couleurFondTitresUECapitalisee);; } diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 4bfb81864..a5e8d173d 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2103,11 +2103,11 @@ tr.mievr { background-color: #eeeeee; } -tr.mievr_rattr { +tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus { background-color: #dddddd; } -span.mievr_rattr { +span.mievr_rattr, span.mievr_session2, span.mievr_bonus { display: inline-block; font-weight: bold; font-size: 80%; @@ -4743,6 +4743,10 @@ table.table_recap th.col_malus { font-weight: bold; color: rgb(165, 0, 0); } +table.table_recap td.col_eval_bonus, +table.table_recap th.col_eval_bonus { + color: #90c; +} table.table_recap tr.ects td { color: rgb(160, 86, 3); diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index d76ec5359..2523b227f 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -491,14 +491,15 @@ class releveBUT extends HTMLElement { let output = ""; evaluations.forEach((evaluation) => { output += ` -

+
${this.URL(evaluation.url, evaluation.description || "Évaluation")}
${evaluation.note.value} - Coef. ${evaluation.coef ?? "*"} + ${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : "" + } ${evaluation.coef ?? ""}
-
Coef
${evaluation.coef}
+
${evaluation.evaluation_type == 0 ? "Coef." : ""}
${evaluation.coef ?? ""}
Max. promo.
${evaluation.note.max}
Moy. promo.
${evaluation.note.moy}
Min. promo.
${evaluation.note.min}
diff --git a/app/tables/recap.py b/app/tables/recap.py index f4882983c..0e2872037 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -13,7 +13,7 @@ import numpy as np from app import db from app.auth.models import User from app.comp.res_common import ResultatsSemestre -from app.models import Identite, FormSemestre, UniteEns +from app.models import Identite, Evaluation, FormSemestre, UniteEns from app.scodoc.codes_cursus import UE_SPORT, DEF from app.scodoc import sco_evaluation_db from app.scodoc import sco_groups @@ -405,15 +405,22 @@ class TableRecap(tb.Table): val = notes_db[etudid]["value"] else: # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE + val = ( + scu.NOTES_ATTENTE + if e.evaluation_type != Evaluation.EVALUATION_BONUS + else "" + ) content = self.fmt_note(val) - classes = col_classes + [ - { - "ABS": "abs", - "ATT": "att", - "EXC": "exc", - }.get(content, "") - ] + if e.evaluation_type != Evaluation.EVALUATION_BONUS: + classes = col_classes + [ + { + "ABS": "abs", + "ATT": "att", + "EXC": "exc", + }.get(content, "") + ] + else: + classes = col_classes + ["col_eval_bonus"] row.add_cell( col_id, title, content, group="eval", classes=classes ) diff --git a/app/templates/scodoc/help/evaluations.j2 b/app/templates/scodoc/help/evaluations.j2 index ea844fd8d..1f133896f 100644 --- a/app/templates/scodoc/help/evaluations.j2 +++ b/app/templates/scodoc/help/evaluations.j2 @@ -8,13 +8,15 @@

{%if is_apc%}

- Dans le BUT, une évaluation peut évaluer différents apprentissages critiques... (à compléter) - Le coefficient est multiplié par les poids vers chaque UE. + Dans le BUT, une évaluation peut évaluer différents apprentissages critiques, + et les poids permettent de moduler l'importance de l'évaluation pour + chaque compétence (UE). + Le coefficient de l'évaluation est multiplié par les poids vers chaque UE.

{%endif%}

Ne pas confondre ce coefficient avec le coefficient du module, qui est - lui fixé par le programme pédagogique (le PPN pour les DUT) et pondère + lui fixé par le programme pédagogique (le PN pour les BUT) et pondère les moyennes de chaque module pour obtenir les moyennes d'UE et la moyenne générale.

@@ -22,17 +24,31 @@ L'option Visible sur bulletins indique que la note sera reportée sur les bulletins en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines notes, en sus des - moyennes de modules. Attention, cette option n'empêche pas la + moyennes de modules). Attention, cette option n'empêche pas la publication sur les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).

+

+ Les évaluations bonus sont particulières: +

+
    +
  • la valeur est ajoutée à la moyenne du module;
  • +
  • le bonus peut être négatif (malus); +
  • +
  • le bonus ne s'applique pas aux notes de rattrapage et deuxième session; +
  • +
  • le coefficient est ignoré, mais en BUT le bonus vers une UE est multiplié + par le poids correspondant (par défaut égal à 1); +
  • +
  • les notes de bonus sont prises en compte même si incomplètes.
  • +

Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de façon spéciale:

  • les notes d'une évaluation de "rattrapage" remplaceront les moyennes - du module si elles sont meilleures que celles calculées. + du module si elles sont meilleures que celles calculées;.
  • les notes de "deuxième session" remplacent, lorsqu'elles sont saisies, la moyenne de l'étudiant à ce module, même si la note de diff --git a/sco_version.py b/sco_version.py index 2b38021dc..ff1b244fb 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,19 +1,20 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.944" +SCOVERSION = "9.6.945" SCONAME = "ScoDoc" SCONEWS = """ -

    Année 2023

    +

    Année 2023-2024

      -
    • ScoDoc 9.6 (juillet 2023)
    • +
    • ScoDoc 9.6 (2023-2024)
      • Nouveaux bulletins BUT compacts
      • Nouvelle gestion des absences et assiduité
      • Mise à jour logiciels: Debian 12, Python 3.11, ...
      • +
      • Evaluations bonus
    • ScoDoc 9.5 (juillet 2023)
    • diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py index 918a357c5..4dfaee336 100644 --- a/tests/unit/test_notes_rattrapage.py +++ b/tests/unit/test_notes_rattrapage.py @@ -1,5 +1,6 @@ """Test calculs rattrapages """ + import datetime import app @@ -68,7 +69,7 @@ def test_notes_rattrapage(test_client): date_debut=datetime.datetime(2020, 1, 2), description="evaluation rattrapage", coefficient=1.0, - evaluation_type=scu.EVALUATION_RATTRAPAGE, + evaluation_type=Evaluation.EVALUATION_RATTRAPAGE, ) etud = etuds[0] _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0) @@ -144,7 +145,7 @@ def test_notes_rattrapage(test_client): date_debut=datetime.datetime(2020, 1, 2), description="evaluation session 2", coefficient=1.0, - evaluation_type=scu.EVALUATION_SESSION2, + evaluation_type=Evaluation.EVALUATION_SESSION2, ) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)