diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 918d14fd2e3320beae12d6fdf2ae6b665e66e7b5..87b175468fce6eaa78a950002a592b4b7c79a8c8 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 f56b86c0fc7284c6497a67ead72fdf527e01a958..999846f7794966b3bd2e436878f4c858321b3bcc 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"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>" + f"""<para align=right fontSize={self.small_fontsize}><i>{ + coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS + else "bonus" + }</i></para>""" ), "_pdf_style": [ ( diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 94f056b2f321404eef258da8b8d80ca6cc9ad8d9..a977894d6bf6b0d14b767758e1f324526f458c92 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 7da2d84cc4f0dd2f843b8575fe5040410ca7cc88..37e3ac79487f76970625a77b63eda5a99d3e5d4f 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"""<Evaluation {self.id} { self.date_debut.isoformat() if self.date_debut else ''} "{ @@ -546,7 +555,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): # --- evaluation_type try: data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0) - if not data["evaluation_type"] in VALID_EVALUATION_TYPES: + if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES: raise ScoValueError("invalid evaluation_type value") except ValueError as exc: raise ScoValueError("invalid evaluation_type value") from exc diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index d7d91ba22a81581191aa2cc079d941eff6477147..69c13df36f9c88e01e9864749b5a689d39916afc 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -610,16 +610,19 @@ def _ue_mod_bulletin( e_dict["coef_txt"] = "" else: e_dict["coef_txt"] = scu.fmt_coef(e.coefficient) - if e.evaluation_type == scu.EVALUATION_RATTRAPAGE: + if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: e_dict["coef_txt"] = "rat." - elif e.evaluation_type == scu.EVALUATION_SESSION2: + elif e.evaluation_type == Evaluation.EVALUATION_SESSION2: e_dict["coef_txt"] = "Ses. 2" if modimpl_results.evaluations_etat[e.id].nb_attente: mod_attente = True # une eval en attente dans ce module if ((not is_malus) or (val != "NP")) and ( - (e.evaluation_type == scu.EVALUATION_NORMALE or not np.isnan(val)) + ( + e.evaluation_type == Evaluation.EVALUATION_NORMALE + or not np.isnan(val) + ) ): # ne liste pas les eval malus sans notes # ni les rattrapages et sessions 2 si pas de note diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index a35a021e26a2045ac32258322234c4bb9e35583e..f2309e941ccbe333afd7d9fcf54ff0269275493f 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -51,7 +51,7 @@ from reportlab.lib.colors import Color, blue from reportlab.lib.units import cm, mm from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table -from app.models import BulAppreciations +from app.models import BulAppreciations, Evaluation import app.scodoc.sco_utils as scu from app.scodoc import ( gen_tables, @@ -715,9 +715,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): eval_style = "" t = { "module": '<bullet indent="2mm">•</bullet> ' + e["name"], - "coef": ("<i>" + e["coef_txt"] + "</i>") - if prefs["bul_show_coef"] - else "", + "coef": ( + ( + f"<i>{e['coef_txt']}</i>" + 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 b26ed9fbe3108816ed314c93a85b74a5cccd310b..c7b0dd676f3bdaacb7c450c796c64aadfe04ab77 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 610cb255a9e97f0c39810a690d8a5a1a0d416b27..d157e0a30ed865fe11678e45b273c71a366d2a62 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"<p><b>UE : {ue.acronyme}</b></p>") + 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( """<span id="eval_note_min" class="sco-hidden">-20.</span> <span id="eval_note_max" class="sco-hidden">20.</span>""" ) 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"<p>Réalisée le <b>{evaluation.descr_date()}</b> ") 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 4405847460a7ccf0df69aec79d42e290a60584d8..9e668c9027fca3db3c4b31ac17a4ecd3e0360ce1 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 9ee69aec3d33a80ceea0e27da747336d6b838cc9..28021e13f00c6c8de7b2bc71448666c86a1557ad 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</a>""" ) H.append(f" <em>{evaluation.description or ''}</em>") - if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE: + if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: H.append( """<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>""" ) - elif evaluation.evaluation_type == scu.EVALUATION_SESSION2: + elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: + H.append( + """<span class="mievr_session2" title="remplace autres notes">session 2</span>""" + ) + elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: H.append( - """<span class="mievr_rattr" title="remplace autres notes">session 2</span>""" + """<span class="mievr_bonus" title="s'ajoute aux moyennes de ce module">bonus</span>""" ) # if etat["last_modif"]: diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 8f84e8c6c6eb8f42645078b3799a369be82a7185..a9195d29b2beb2964777518299941b0edb2ac45c 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 4d32a4053005204ea05a056cdf6efce19bf6a7da..f1840386bf12cb29872b26860899266a143498a2 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 6b3850997565b4bd31458ea95e7638b05ac9acf0..7e03f38f711613ea852f4acbf74064a4f689d1bd 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 25a31c972e2f55701e947aab84a5aba79f1a2f40..b0c0f53328688ec8f084505b8998d7dabdee4476 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 4bfb81864667cc52975bb75a37e6df710d993b40..a5e8d173d93b50625eaa7be2794adc6b85cedff4 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 d76ec53599b2f2f6df64a5b248fcb1caaa4666b3..2523b227f7ee0f36b51d660142c8896607ed4c8d 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 += ` - <div class=eval> + <div class="eval ${evaluation.evaluation_type == 3 ? "eval-bonus" : ""}"> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div> ${evaluation.note.value} - <em>Coef. ${evaluation.coef ?? "*"}</em> + <em>${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : "" + } ${evaluation.coef ?? ""}</em> </div> <div class=complement> - <div>Coef</div><div>${evaluation.coef}</div> + <div>${evaluation.evaluation_type == 0 ? "Coef." : ""}</div><div>${evaluation.coef ?? ""}</div> <div>Max. promo.</div><div>${evaluation.note.max}</div> <div>Moy. promo.</div><div>${evaluation.note.moy}</div> <div>Min. promo.</div><div>${evaluation.note.min}</div> diff --git a/app/tables/recap.py b/app/tables/recap.py index f4882983cb18185b2dac9cc4295bae25580778de..0e2872037f5d7ec1f255b9b69d5f9487f755e3ec 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 ea844fd8d95cf50429c22ca40ca89728a4289ac8..1f133896fa65403c902e6cdeeadc7b48f6b69101 100644 --- a/app/templates/scodoc/help/evaluations.j2 +++ b/app/templates/scodoc/help/evaluations.j2 @@ -8,13 +8,15 @@ </p> {%if is_apc%} <p class="help help_but"> - 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. </p> {%endif%} <p class="help"> 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. </p> @@ -22,17 +24,31 @@ L'option <em>Visible sur bulletins</em> 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). </p> + <p class="help"> + Les évaluations bonus sont particulières: + </p> + <ul> + <li>la valeur est ajoutée à la moyenne du module;</li> + <li>le bonus peut être négatif (malus); + </li> + <li>le bonus ne s'applique pas aux notes de rattrapage et deuxième session; + </li> + <li>le coefficient est ignoré, mais en BUT le bonus vers une UE est multiplié + par le poids correspondant (par défaut égal à 1); + </li> + <li>les notes de bonus sont prises en compte même si incomplètes.</li> + </ul> <p class="help"> Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de façon spéciale: </p> <ul> <li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes - du module <em>si elles sont meilleures que celles calculées</em>. + du module <em>si elles sont meilleures que celles calculées;</em>. </li> <li>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 2b38021dc6ef0499ece139aaba8aa9d300dbb596..ff1b244fb60d17e7b36c9e07b42b57c3daaa2e1a 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 = """ -<h4>Année 2023</h4> +<h4>Année 2023-2024</h4> <ul> -<li>ScoDoc 9.6 (juillet 2023)</li> +<li>ScoDoc 9.6 (2023-2024)</li> <ul> <li>Nouveaux bulletins BUT compacts</li> <li>Nouvelle gestion des absences et assiduité</li> <li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li> + <li>Evaluations bonus</li> </ul> <li>ScoDoc 9.5 (juillet 2023)</li> diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py index 918a357c5b87522c4fab3efc6e2bbdf839097ed1..4dfaee3361e01d5c07887e4db4090db0cda15f88 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)