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
"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"]))

View File

@ -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": [
(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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">&bull;</bullet>&nbsp;' + 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' : ,

View File

@ -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,
},
),

View File

@ -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)

View File

@ -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"

View File

@ -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"&nbsp;&nbsp;&nbsp; <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_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"]:

View File

@ -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:

View File

@ -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",
)

View File

@ -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.

View File

@ -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);;
}

View File

@ -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);

View File

@ -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.&nbsp;${evaluation.coef ?? "*"}</em>
<em>${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : ""
}&nbsp;${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>

View File

@ -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
)

View File

@ -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

View File

@ -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>

View File

@ -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)