Implémentation des bonus malus

This commit is contained in:
Emmanuel Viennet 2022-02-01 11:37:05 +01:00
parent e3450ebc82
commit bb40532ca6
11 changed files with 168 additions and 32 deletions

View File

@ -71,7 +71,7 @@ class BulletinBUT(ResultatsSemestreBUT):
"bonus": fmt_note(self.bonus_ues[ue.id][etud.id])
if self.bonus_ues is not None and ue.id in self.bonus_ues
else fmt_note(0.0),
"malus": None, # XXX TODO voir ce qui est ici
"malus": self.malus[ue.id][etud.id],
"capitalise": None, # "AAAA-MM-JJ" TODO
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
"saes": self.etud_ue_mod_results(etud, ue, self.saes),

View File

@ -104,7 +104,6 @@ class BonusSport:
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
nb_ues = len(ues)
# Enlève les NaN du numérateur:
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
@ -162,7 +161,7 @@ class BonusSport:
"""
raise NotImplementedError("méthode virtuelle")
def get_bonus_ues(self) -> pd.Series:
def get_bonus_ues(self) -> pd.DataFrame:
"""Les bonus à appliquer aux UE
Résultat: DataFrame de float, index etudid, columns: ue.id
"""

View File

@ -43,6 +43,8 @@ from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
@dataclass
class EvaluationEtat:
@ -291,7 +293,12 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés:
default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0
default_poids = (
1.0
if modimpl.module.ue.type == UE_SPORT
or modimpl.module.module_type == ModuleType.MALUS
else 0.0
)
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()

View File

@ -27,6 +27,7 @@
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
from re import X
import numpy as np
import pandas as pd
@ -380,3 +381,42 @@ def compute_ue_moys_classic(
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list[UniteEns],
modimpl_inscr_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul le malus sur les UE
Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS.
Leurs notes sont positives ou négatives. leur somme sera _soustraite_ à la moyenne
de chaque UE.
Arguments:
- sem_modimpl_moys :
notes moyennes aux modules (tous les étuds x tous les modimpls)
floats avec des NaN.
En classique: sem_matrix, ndarray (etuds x modimpls)
En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus)
- ues: les ues du semestre (incluant le bonus sport)
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN)
"""
ues_idx = [ue.id for ue in ues]
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
for ue in ues:
if ue.type != UE_SPORT:
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.MALUS)
and (m.module.ue.id == ue.id)
for m in formsemestre.modimpls_sorted
]
)
malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1)
malus[ue.id] = malus_moys
malus.fillna(0.0, inplace=True)
return malus

View File

@ -68,6 +68,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Modules de MALUS sur les UEs
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture
if len(modimpls_sport) > 0:
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()

View File

@ -71,6 +71,16 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.modimpl_coefs,
modimpl_standards_mask,
)
# --- Modules de MALUS sur les UEs et la moyenne générale
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# ajuste la moyenne générale (à l'aide des coefs d'UE)
self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
# --- Bonus Sport & Culture
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:

View File

@ -115,22 +115,30 @@ def do_module_create(args) -> int:
return r
def module_create(matiere_id=None, module_type=None, semestre_id=None):
"""Création d'un module"""
def module_create(
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
):
"""Formulaire de création d'un module
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
if matiere is None:
raise ScoValueError("invalid matiere !")
ue = matiere.ue
parcours = ue.formation.get_parcours()
formation = ue.formation
else:
formation = Formation.query.get_or_404(formation_id)
parcours = formation.get_parcours()
is_apc = parcours.APC_SAE
ues = ue.formation.ues.order_by(
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = matiere.ue.formation.modules.all()
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
@ -143,9 +151,11 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
H = [
html_sco_header.sco_header(page_title=f"Création {object_name}"),
]
if is_apc:
if not matiere_id:
H += [
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}, Semestre {ue.semestre_idx}, {ue.acronyme}</h2>"""
f"""<h2>Création {object_name} dans la formation {formation.acronyme}
</h2>
"""
]
else:
H += [
@ -158,7 +168,6 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
ue=ue,
semestre_id=semestre_id,
)
]
@ -170,7 +179,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"size": 10,
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
"allow_null": False,
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id
),
},
@ -192,6 +201,15 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
]
semestres_indices = list(range(1, parcours.NB_SEM + 1))
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# ne propose pas SAE et Ressources:
module_types = set(scu.ModuleType) - {
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
}
descr += [
(
"module_type",
@ -199,8 +217,8 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
},
),
(
@ -256,11 +274,30 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
),
]
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
"allowed_values": [u.id for u in ues],
},
),
]
descr += [
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
("formation_id", {"default": formation.id, "input_type": "hidden"}),
(
"code_apogee",
{
@ -290,6 +327,20 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
else:
if not matiere_id:
# formulaire avec choix UE de rattachement
ue = UniteEns.query.get(tf[2]["ue_id"])
if ue is None:
raise ValueError("UE invalide")
matiere = ue.matieres.first()
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
)
tf[2]["matiere_id"] = matiere_id
tf[2]["semestre_id"] = ue.semestre_idx
_ = do_module_create(tf[2])
@ -298,7 +349,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"],
)
)
@ -493,6 +544,13 @@ def module_edit(module_id=None):
soyez prudents !
</span></div>"""
)
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type}
descr = [
(
@ -514,8 +572,8 @@ def module_edit(module_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
"enabled": unlocked,
},
),

View File

@ -998,6 +998,7 @@ def _ue_table_matieres(
H.append(
_ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1031,6 +1032,7 @@ def _ue_table_matieres(
def _ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1121,8 +1123,12 @@ def _ue_table_modules(
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
warning_semestre = ' <span class="red">incohérent ?</span>'
else:
warning_semestre = ""
H.append(
" %s %s" % (parcours.SESSION_NAME, mod["semestre_id"])
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
+ " (%s)" % heurescoef
+ tag_edit
)

View File

@ -546,7 +546,7 @@ def do_formsemestre_createwithmodules(edit=False):
for mod in mods:
if mod["semestre_id"] == semestre_id and (
(not edit) # creation => tous modules
or (not formation.is_apc()) # pas BUT, on peux mixer les semestres
or (not formation.is_apc()) # pas BUT, on peut mixer les semestres
or (semestre_id == formsemestre.semestre_id) # module du semestre
or (mod["module_id"] in module_ids_set) # module déjà présent
):

View File

@ -219,7 +219,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{Mod['code']}</tt> {Mod['titre']}</h2>
<tt>{Mod['code']}</tt> {Mod['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""}
</h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(Mod['module_type']).name.lower()}">
<table>

View File

@ -71,13 +71,21 @@
</li>
{% endfor %}
{% if editable and matiere_parent %}
<li><a class="stdlink" href="{{
{% if editable %}
<li><a class="stdlink" href=
{% if matiere_parent %}"{{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept,
module_type=module_type|int,
matiere_id=matiere_parent.id
)}}"
{% else %}"{{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept,
module_type=module_type|int,
formation_id=formation.id
)}}"
{% endif %}
>{{create_element_msg}}</a>
</li>
{% endif %}