Evaluations de type bonus. Implements #848

This commit is contained in:
Emmanuel Viennet 2024-02-24 16:49:41 +01:00
parent 81fab97018
commit 7f32f1fb99
20 changed files with 312 additions and 154 deletions

View File

@ -104,9 +104,11 @@ class BulletinBUT:
"competence": None, # XXX TODO lien avec référentiel "competence": None, # XXX TODO lien avec référentiel
"moyenne": None, "moyenne": None,
# Le bonus sport appliqué sur cette UE # Le bonus sport appliqué sur cette UE
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id]) "bonus": (
if res.bonus_ues is not None and ue.id in res.bonus_ues fmt_note(res.bonus_ues[ue.id][etud.id])
else fmt_note(0.0), 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]), "malus": fmt_note(res.malus[ue.id][etud.id]),
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "capitalise": None, # "AAAA-MM-JJ" TODO #sco93
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
@ -181,14 +183,16 @@ class BulletinBUT:
"is_external": ue_capitalisee.is_external, "is_external": ue_capitalisee.is_external,
"date_capitalisation": ue_capitalisee.event_date, "date_capitalisation": ue_capitalisee.event_date,
"formsemestre_id": ue_capitalisee.formsemestre_id, "formsemestre_id": ue_capitalisee.formsemestre_id,
"bul_orig_url": url_for( "bul_orig_url": (
"notes.formsemestre_bulletinetud", url_for(
scodoc_dept=g.scodoc_dept, "notes.formsemestre_bulletinetud",
etudid=etud.id, scodoc_dept=g.scodoc_dept,
formsemestre_id=ue_capitalisee.formsemestre_id, etudid=etud.id,
) formsemestre_id=ue_capitalisee.formsemestre_id,
if ue_capitalisee.formsemestre_id )
else None, if ue_capitalisee.formsemestre_id
else None
),
"ressources": {}, # sans détail en BUT "ressources": {}, # sans détail en BUT
"saes": {}, "saes": {},
} }
@ -227,13 +231,15 @@ class BulletinBUT:
"id": modimpl.id, "id": modimpl.id,
"titre": modimpl.module.titre, "titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee, "code_apogee": modimpl.module.code_apogee,
"url": url_for( "url": (
"notes.moduleimpl_status", url_for(
scodoc_dept=g.scodoc_dept, "notes.moduleimpl_status",
moduleimpl_id=modimpl.id, scodoc_dept=g.scodoc_dept,
) moduleimpl_id=modimpl.id,
if has_request_context() )
else "na", if has_request_context()
else "na"
),
"moyenne": { "moyenne": {
# # moyenne indicative de module: moyenne des UE, # # moyenne indicative de module: moyenne des UE,
# # ignorant celles sans notes (nan) # # ignorant celles sans notes (nan)
@ -242,18 +248,20 @@ class BulletinBUT:
# "max": fmt_note(moyennes_etuds.max()), # "max": fmt_note(moyennes_etuds.max()),
# "moy": fmt_note(moyennes_etuds.mean()), # "moy": fmt_note(moyennes_etuds.mean()),
}, },
"evaluations": [ "evaluations": (
self.etud_eval_results(etud, e) [
for e in modimpl.evaluations self.etud_eval_results(etud, e)
if (e.visibulletin or version == "long") for e in modimpl.evaluations
and (e.id in modimpl_results.evaluations_etat) if (e.visibulletin or version == "long")
and ( and (e.id in modimpl_results.evaluations_etat)
modimpl_results.evaluations_etat[e.id].is_complete and (
or self.prefs["bul_show_all_evals"] modimpl_results.evaluations_etat[e.id].is_complete
) or self.prefs["bul_show_all_evals"]
] )
if version != "short" ]
else [], if version != "short"
else []
),
} }
return d return d
@ -274,9 +282,11 @@ class BulletinBUT:
poids = collections.defaultdict(lambda: 0.0) poids = collections.defaultdict(lambda: 0.0)
d = { d = {
"id": e.id, "id": e.id,
"coef": fmt_note(e.coefficient) "coef": (
if e.evaluation_type == scu.EVALUATION_NORMALE fmt_note(e.coefficient)
else None, if e.evaluation_type == Evaluation.EVALUATION_NORMALE
else None
),
"date_debut": e.date_debut.isoformat() if e.date_debut 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, "date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description, "description": e.description,
@ -291,18 +301,20 @@ class BulletinBUT:
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max), "moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
}, },
"poids": poids, "poids": poids,
"url": url_for( "url": (
"notes.evaluation_listenotes", url_for(
scodoc_dept=g.scodoc_dept, "notes.evaluation_listenotes",
evaluation_id=e.id, scodoc_dept=g.scodoc_dept,
) evaluation_id=e.id,
if has_request_context() )
else "na", if has_request_context()
else "na"
),
# deprecated (supprimer avant #sco9.7) # deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None, "date": e.date_debut.isoformat() if e.date_debut else None,
"heure_debut": e.date_debut.time().isoformat("minutes") "heure_debut": (
if e.date_debut e.date_debut.time().isoformat("minutes") if e.date_debut else None
else None, ),
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None, "heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
} }
return d return d
@ -524,9 +536,9 @@ class BulletinBUT:
d.update(infos) d.update(infos)
# --- Rangs # --- Rangs
d[ d["rang_nt"] = (
"rang_nt" f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" )
d["rang_txt"] = "Rang " + d["rang_nt"] d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -24,7 +24,7 @@ from reportlab.lib.colors import blue
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer 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.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables from app.scodoc import gen_tables
from app.scodoc.codes_cursus import UE_SPORT 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=()): def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
"lignes des évaluations" "lignes des évaluations"
for e in evaluations: 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 = { t = {
"titre": f"{e['description'] or ''}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": e["note"]["value"],
@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
), ),
"coef": coef, "coef": coef,
"_coef_pdf": Paragraph( "_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": [ "_pdf_style": [
( (

View File

@ -157,8 +157,7 @@ class ModuleImplResults:
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = ( is_complete = (
(evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE) (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.evaluation_type == scu.EVALUATION_SESSION2)
or (evaluation.publish_incomplete) or (evaluation.publish_incomplete)
or (not etudids_sans_note) or (not etudids_sans_note)
) )
@ -240,19 +239,20 @@ class ModuleImplResults:
).formsemestre.inscriptions ).formsemestre.inscriptions
] ]
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array: def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations. """Coefficients des évaluations.
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage) Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
sont zéro.
Résultat: 2d-array of floats, shape (nb_evals, 1) Résultat: 2d-array of floats, shape (nb_evals, 1)
""" """
return ( return (
np.array( np.array(
[ [
e.coefficient (
if e.evaluation_type == scu.EVALUATION_NORMALE e.coefficient
else 0.0 if e.evaluation_type == Evaluation.EVALUATION_NORMALE
for e in moduleimpl.evaluations else 0.0
)
for e in modimpl.evaluations
], ],
dtype=float, dtype=float,
) )
@ -285,7 +285,7 @@ class ModuleImplResults:
for (etudid, x) in self.evals_notes[evaluation_id].items() 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. """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 Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage. des autres évals et la note eval rattrapage.
@ -293,25 +293,41 @@ class ModuleImplResults:
eval_list = [ eval_list = [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
] ]
if eval_list: if eval_list:
return eval_list[0] return eval_list[0]
return None 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. """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. Session 2: remplace la note de moyenne des autres évals.
""" """
eval_list = [ eval_list = [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_SESSION2 if e.evaluation_type == Evaluation.EVALUATION_SESSION2
] ]
if eval_list: if eval_list:
return eval_list[0] return eval_list[0]
return None 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): class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT" "Calcul des moyennes de modules à la mode BUT"
@ -356,7 +372,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
# et dans dans evals_poids_etuds # et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours false face à un NaN) # (rappel: la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues) # 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( evals_poids_etuds = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked, poids_stacked,
@ -364,10 +380,20 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
# Calcule la moyenne pondérée sur les notes disponibles: # 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 = 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) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum( etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1 evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, 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 # Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl) eval_session2 = self.get_evaluation_session2(modimpl)
@ -416,6 +442,30 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
return self.etuds_moy_module 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]: def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
"""Charge poids des évaluations d'un module et retourne un dataframe """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 evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, 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 # Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl) eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2: if eval_session2:
@ -571,3 +628,22 @@ class ModuleImplResultsClassic(ModuleImplResults):
) )
return self.etuds_moy_module 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

View File

@ -23,8 +23,6 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
NOON = datetime.time(12, 00) NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0) DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
VALID_EVALUATION_TYPES = {0, 1, 2}
class Evaluation(db.Model): class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
@ -57,6 +55,17 @@ class Evaluation(db.Model):
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) 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): def __repr__(self):
return f"""<Evaluation {self.id} { return f"""<Evaluation {self.id} {
self.date_debut.isoformat() if self.date_debut else ''} "{ self.date_debut.isoformat() if self.date_debut else ''} "{
@ -546,7 +555,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
# --- evaluation_type # --- evaluation_type
try: try:
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0) 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") raise ScoValueError("invalid evaluation_type value")
except ValueError as exc: except ValueError as exc:
raise ScoValueError("invalid evaluation_type value") from exc raise ScoValueError("invalid evaluation_type value") from exc

View File

@ -610,16 +610,19 @@ def _ue_mod_bulletin(
e_dict["coef_txt"] = "" e_dict["coef_txt"] = ""
else: else:
e_dict["coef_txt"] = scu.fmt_coef(e.coefficient) 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." 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" e_dict["coef_txt"] = "Ses. 2"
if modimpl_results.evaluations_etat[e.id].nb_attente: if modimpl_results.evaluations_etat[e.id].nb_attente:
mod_attente = True # une eval en attente dans ce module mod_attente = True # une eval en attente dans ce module
if ((not is_malus) or (val != "NP")) and ( 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 # ne liste pas les eval malus sans notes
# ni les rattrapages et sessions 2 si pas de note # ni les rattrapages et sessions 2 si pas de note

View File

@ -51,7 +51,7 @@ from reportlab.lib.colors import Color, blue
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table 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 import app.scodoc.sco_utils as scu
from app.scodoc import ( from app.scodoc import (
gen_tables, gen_tables,
@ -715,9 +715,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = "" eval_style = ""
t = { t = {
"module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"], "module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
"coef": ("<i>" + e["coef_txt"] + "</i>") "coef": (
if prefs["bul_show_coef"] (
else "", f"<i>{e['coef_txt']}</i>"
if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
else "bonus"
)
if prefs["bul_show_coef"]
else ""
),
"_hidden": hidden, "_hidden": hidden,
"_module_target": e["target_html"], "_module_target": e["target_html"],
# '_module_help' : , # '_module_help' : ,

View File

@ -183,7 +183,8 @@ def evaluation_create_form(
{ {
"size": 6, "size": 6,
"type": "float", # peut être négatif (!) "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, "allow_null": False,
}, },
) )
@ -195,7 +196,7 @@ def evaluation_create_form(
"size": 4, "size": 4,
"type": "float", "type": "float",
"title": "Notes de 0 à", "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, "allow_null": False,
"max_value": scu.NOTES_MAX, "max_value": scu.NOTES_MAX,
"min_value": min_note_max, "min_value": min_note_max,
@ -206,7 +207,8 @@ def evaluation_create_form(
{ {
"size": 36, "size": 36,
"type": "text", "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", "input_type": "menu",
"title": "Modalité", "title": "Modalité",
"allowed_values": ( "allowed_values": Evaluation.VALID_EVALUATION_TYPES,
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"type": "int", "type": "int",
"labels": ( "labels": (
"Normale", "Normale",
"Rattrapage (remplace si meilleure note)", "Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)", "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, "size": 6,
"type": "float", "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, "allow_null": False,
}, },
), ),

View File

@ -217,19 +217,9 @@ def do_evaluation_etat(
gr_incomplets = list(group_nb_missing.keys()) gr_incomplets = list(group_nb_missing.keys())
gr_incomplets.sort() 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 = ( complete = (total_nb_missing == 0) or (
(total_nb_missing == 0) E["evaluation_type"] != Evaluation.EVALUATION_NORMALE
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
) )
evalattente = (total_nb_missing > 0) and ( evalattente = (total_nb_missing > 0) and (
(total_nb_missing == total_nb_att) or E["publish_incomplete"] (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 """Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes. 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) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
evaluations = formsemestre.get_evaluations() evaluations = formsemestre.get_evaluations()
rows = [] rows = []
for e in evaluations: 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 e.moduleimpl.module.module_type == ModuleType.MALUS
): ):
continue continue
@ -610,13 +601,17 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
# Indique l'UE # Indique l'UE
ue = modimpl.module.ue ue = modimpl.module.ue
H.append(f"<p><b>UE : {ue.acronyme}</b></p>") 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: # store min/max values used by JS client-side checks:
H.append( H.append(
"""<span id="eval_note_min" class="sco-hidden">-20.</span> """<span id="eval_note_min" class="sco-hidden">-20.</span>
<span id="eval_note_max" class="sco-hidden">20.</span>""" <span id="eval_note_max" class="sco-hidden">20.</span>"""
) )
else: 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: if evaluation.date_debut is not None:
H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ") H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
group_id = sco_groups.get_default_group(modimpl.formsemestre_id) group_id = sco_groups.get_default_group(modimpl.formsemestre_id)

View File

@ -490,9 +490,9 @@ def _make_table_notes(
rlinks = {"_table_part": "head"} rlinks = {"_table_part": "head"}
for e in evaluations: for e in evaluations:
rlinks[e.id] = "afficher" rlinks[e.id] = "afficher"
rlinks[ rlinks["_" + str(e.id) + "_help"] = (
"_" + str(e.id) + "_help" "afficher seulement les notes de cette évaluation"
] = "afficher seulement les notes de cette évaluation" )
rlinks["_" + str(e.id) + "_target"] = url_for( rlinks["_" + str(e.id) + "_target"] = url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, 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) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
if evaluation.date_debut: if evaluation.date_debut:
titles[ titles[evaluation.id] = (
evaluation.id f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" )
else: else:
titles[evaluation.id] = f"{evaluation.description} " titles[evaluation.id] = f"{evaluation.description} "
@ -820,14 +820,17 @@ def _add_eval_columns(
row_moys[evaluation.id] = scu.fmt_note( row_moys[evaluation.id] = scu.fmt_note(
sum_notes / nb_notes, keep_numeric=keep_numeric sum_notes / nb_notes, keep_numeric=keep_numeric
) )
row_moys[ row_moys["_" + str(evaluation.id) + "_help"] = (
"_" + str(evaluation.id) + "_help" "moyenne sur %d notes (%s le %s)"
] = "moyenne sur %d notes (%s le %s)" % ( % (
nb_notes, nb_notes,
evaluation.description, evaluation.description,
evaluation.date_debut.strftime("%d/%m/%Y") (
if evaluation.date_debut evaluation.date_debut.strftime("%d/%m/%Y")
else "", if evaluation.date_debut
else ""
),
)
) )
else: else:
row_moys[evaluation.id] = "" row_moys[evaluation.id] = ""
@ -884,8 +887,9 @@ def _add_moymod_column(
row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if etudid in inscrits and not isinstance(val, str): if etudid in inscrits and not isinstance(val, str):
notes.append(val) notes.append(val)
nb_notes = nb_notes + 1 if not np.isnan(val):
sum_notes += val nb_notes = nb_notes + 1
sum_notes += val
row_coefs[col_id] = "(avec abs)" row_coefs[col_id] = "(avec abs)"
if is_apc: if is_apc:
row_poids[col_id] = "à titre indicatif" row_poids[col_id] = "à titre indicatif"

View File

@ -519,13 +519,15 @@ def _ligne_evaluation(
partition_id=partition_id, partition_id=partition_id,
select_first_partition=True, select_first_partition=True,
) )
if evaluation.evaluation_type in ( if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
):
tr_class = "mievr mievr_rattr" 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: else:
tr_class = "mievr" tr_class = "mievr"
if not evaluation.visibulletin: if not evaluation.visibulletin:
tr_class += " non_visible_inter" tr_class += " non_visible_inter"
tr_class_1 = "mievr" tr_class_1 = "mievr"
@ -563,13 +565,17 @@ def _ligne_evaluation(
}" class="mievr_evalnodate">Évaluation sans date</a>""" }" class="mievr_evalnodate">Évaluation sans date</a>"""
) )
H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description or ''}</em>") H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description or ''}</em>")
if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE: if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
H.append( H.append(
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>""" """<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( H.append(
"""<span class="mievr_rattr" title="remplace autres notes">session 2</span>""" """<span class="mievr_session2" title="remplace autres notes">session 2</span>"""
)
elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
H.append(
"""<span class="mievr_bonus" title="s'ajoute aux moyennes de ce module">bonus</span>"""
) )
# #
if etat["last_modif"]: if etat["last_modif"]:

View File

@ -134,12 +134,12 @@ def _displayNote(val):
return val return val
def _check_notes(notes: list[(int, float)], evaluation: Evaluation): def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
# XXX typehint : float or str
"""notes is a list of tuples (etudid, value) """notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus) mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value) 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 note_max = evaluation.note_max or 0.0
module: Module = evaluation.moduleimpl.module module: Module = evaluation.moduleimpl.module
@ -148,7 +148,10 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
scu.ModuleType.RESSOURCE, scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE, 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: elif module.module_type == ModuleType.MALUS:
note_min = -20.0 note_min = -20.0
else: else:

View File

@ -175,7 +175,7 @@ def external_ue_inscrit_et_note(
note_max=20.0, note_max=20.0,
coefficient=1.0, coefficient=1.0,
publish_incomplete=True, publish_incomplete=True,
evaluation_type=scu.EVALUATION_NORMALE, evaluation_type=Evaluation.EVALUATION_NORMALE,
visibulletin=False, visibulletin=False,
description="note externe", description="note externe",
) )

View File

@ -454,10 +454,6 @@ NOTES_MENTIONS_LABS = (
"Excellent", "Excellent",
) )
EVALUATION_NORMALE = 0
EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2
# Dates et années scolaires # Dates et années scolaires
# Ces dates "pivot" sont paramétrables dans les préférences générales # Ces dates "pivot" sont paramétrables dans les préférences générales
# on donne ici les valeurs par défaut. # on donne ici les valeurs par défaut.

View File

@ -273,6 +273,10 @@ section>div:nth-child(1) {
min-width: 80px; min-width: 80px;
display: inline-block; display: inline-block;
} }
div.eval-bonus {
color: #197614;
background-color: pink;
}
.ueBonus, .ueBonus,
.ueBonus h3 { .ueBonus h3 {

View File

@ -2103,11 +2103,11 @@ tr.mievr {
background-color: #eeeeee; background-color: #eeeeee;
} }
tr.mievr_rattr { tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus {
background-color: #dddddd; background-color: #dddddd;
} }
span.mievr_rattr { span.mievr_rattr, span.mievr_session2, span.mievr_bonus {
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
font-size: 80%; font-size: 80%;
@ -4743,6 +4743,10 @@ table.table_recap th.col_malus {
font-weight: bold; font-weight: bold;
color: rgb(165, 0, 0); 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 { table.table_recap tr.ects td {
color: rgb(160, 86, 3); color: rgb(160, 86, 3);

View File

@ -491,14 +491,15 @@ class releveBUT extends HTMLElement {
let output = ""; let output = "";
evaluations.forEach((evaluation) => { evaluations.forEach((evaluation) => {
output += ` output += `
<div class=eval> <div class="eval ${evaluation.evaluation_type == 3 ? "eval-bonus" : ""}">
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
<div> <div>
${evaluation.note.value} ${evaluation.note.value}
<em>Coef.&nbsp;${evaluation.coef ?? "*"}</em> <em>${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : ""
}&nbsp;${evaluation.coef ?? ""}</em>
</div> </div>
<div class=complement> <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>Max. promo.</div><div>${evaluation.note.max}</div>
<div>Moy. promo.</div><div>${evaluation.note.moy}</div> <div>Moy. promo.</div><div>${evaluation.note.moy}</div>
<div>Min. promo.</div><div>${evaluation.note.min}</div> <div>Min. promo.</div><div>${evaluation.note.min}</div>

View File

@ -13,7 +13,7 @@ import numpy as np
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.comp.res_common import ResultatsSemestre 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.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -405,15 +405,22 @@ class TableRecap(tb.Table):
val = notes_db[etudid]["value"] val = notes_db[etudid]["value"]
else: else:
# Note manquante mais prise en compte immédiate: affiche ATT # 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) content = self.fmt_note(val)
classes = col_classes + [ if e.evaluation_type != Evaluation.EVALUATION_BONUS:
{ classes = col_classes + [
"ABS": "abs", {
"ATT": "att", "ABS": "abs",
"EXC": "exc", "ATT": "att",
}.get(content, "") "EXC": "exc",
] }.get(content, "")
]
else:
classes = col_classes + ["col_eval_bonus"]
row.add_cell( row.add_cell(
col_id, title, content, group="eval", classes=classes col_id, title, content, group="eval", classes=classes
) )

View File

@ -8,13 +8,15 @@
</p> </p>
{%if is_apc%} {%if is_apc%}
<p class="help help_but"> <p class="help help_but">
Dans le BUT, une évaluation peut évaluer différents apprentissages critiques... (à compléter) Dans le BUT, une évaluation peut évaluer différents apprentissages critiques,
Le coefficient est multiplié par les poids vers chaque UE. 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> </p>
{%endif%} {%endif%}
<p class="help"> <p class="help">
Ne pas confondre ce coefficient avec le coefficient du module, qui est 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 les moyennes de chaque module pour obtenir les moyennes d'UE et la
moyenne générale. moyenne générale.
</p> </p>
@ -22,17 +24,31 @@
L'option <em>Visible sur bulletins</em> indique que la note sera L'option <em>Visible sur bulletins</em> indique que la note sera
reportée sur les bulletins en version dite "intermédiaire" (dans cette reportée sur les bulletins en version dite "intermédiaire" (dans cette
version, on peut ne faire apparaitre que certaines notes, en sus des 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 publication sur les bulletins en version "longue" (la note est donc
visible par les étudiants sur le portail). visible par les étudiants sur le portail).
</p> </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"> <p class="help">
Les modalités "rattrapage" et "deuxième session" définissent des Les modalités "rattrapage" et "deuxième session" définissent des
évaluations prises en compte de façon spéciale: évaluations prises en compte de façon spéciale:
</p> </p>
<ul> <ul>
<li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes <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>
<li>les notes de "deuxième session" remplacent, lorsqu'elles sont <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 saisies, la moyenne de l'étudiant à ce module, même si la note de

View File

@ -1,19 +1,20 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.944" SCOVERSION = "9.6.945"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2023</h4> <h4>Année 2023-2024</h4>
<ul> <ul>
<li>ScoDoc 9.6 (juillet 2023)</li> <li>ScoDoc 9.6 (2023-2024)</li>
<ul> <ul>
<li>Nouveaux bulletins BUT compacts</li> <li>Nouveaux bulletins BUT compacts</li>
<li>Nouvelle gestion des absences et assiduité</li> <li>Nouvelle gestion des absences et assiduité</li>
<li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li> <li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li>
<li>Evaluations bonus</li>
</ul> </ul>
<li>ScoDoc 9.5 (juillet 2023)</li> <li>ScoDoc 9.5 (juillet 2023)</li>

View File

@ -1,5 +1,6 @@
"""Test calculs rattrapages """Test calculs rattrapages
""" """
import datetime import datetime
import app import app
@ -68,7 +69,7 @@ def test_notes_rattrapage(test_client):
date_debut=datetime.datetime(2020, 1, 2), date_debut=datetime.datetime(2020, 1, 2),
description="evaluation rattrapage", description="evaluation rattrapage",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_RATTRAPAGE, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE,
) )
etud = etuds[0] etud = etuds[0]
_, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.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), date_debut=datetime.datetime(2020, 1, 2),
description="evaluation session 2", description="evaluation session 2",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_SESSION2, evaluation_type=Evaluation.EVALUATION_SESSION2,
) )
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)