WIP: calcul des moyennes de modules BUT

This commit is contained in:
Emmanuel Viennet 2021-11-22 00:31:53 +01:00
parent 3a0a2382c8
commit a83ab8f684
13 changed files with 370 additions and 184 deletions

View File

@ -41,11 +41,14 @@ from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
def df_load_evaluations_poids(moduleimpl_id: int, default_poids=0.0) -> pd.DataFrame: def df_load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0
) -> 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
rows = evaluations, columns = UE, value = poids (float). rows = evaluations, columns = UE, value = poids (float).
Les valeurs manquantes (évaluations sans coef vers des UE) sont Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies par default_poids. remplies par default_poids.
Résultat: (evals_poids, liste de UE du semestre)
""" """
modimpl = ModuleImpl.query.get(moduleimpl_id) modimpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
@ -59,7 +62,7 @@ def df_load_evaluations_poids(moduleimpl_id: int, default_poids=0.0) -> pd.DataF
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
if default_poids is not None: if default_poids is not None:
df.fillna(value=default_poids, inplace=True) df.fillna(value=default_poids, inplace=True)
return df return df, ues
def check_moduleimpl_conformity( def check_moduleimpl_conformity(
@ -83,12 +86,14 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
colonnes: evaluation_id (le nom de la colonne est l'evaluation_id en str) colonnes: evaluation_id (le nom de la colonne est l'evaluation_id en str)
index (lignes): etudid index (lignes): etudid
Résultat: (evals_notes, liste de évaluations du moduleimpl)
L'ensemble des étudiants est celui des inscrits au module. L'ensemble des étudiants est celui des inscrits au module.
Valeurs des notes: Les notes renvoyées sont "brutes" et peuvent prendre els valeurs:
note : float (valeur enregistrée brute, pas normalisée sur 20) note : float (valeur enregistrée brute, non normalisée sur 20)
pas de note: NaN pas de note: NaN
absent: 0. absent: NaN
excusé: NOTES_NEUTRALISE (voir sco_utils) excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE attente: NOTES_ATTENTE
@ -96,7 +101,7 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
""" """
etudids = [e.etudid for e in ModuleImpl.query.get(moduleimpl_id).inscriptions] etudids = [e.etudid for e in ModuleImpl.query.get(moduleimpl_id).inscriptions]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id) evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
df = pd.DataFrame(index=etudids, dtype=float) # empty df with all students evals_notes = pd.DataFrame(index=etudids, dtype=float) # empty df with all students
for evaluation in evaluations: for evaluation in evaluations:
eval_df = pd.read_sql( eval_df = pd.read_sql(
@ -107,14 +112,30 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
params={"evaluation_id": evaluation.evaluation_id}, params={"evaluation_id": evaluation.evaluation_id},
index_col="etudid", index_col="etudid",
) )
# Remplace les ABS (NULL en BD, donc NaN ici) par des zéros. evals_notes = evals_notes.merge(
eval_df.fillna(value=0.0, inplace=True) eval_df, how="outer", left_index=True, right_index=True
df = df.merge(eval_df, how="outer", left_index=True, right_index=True) )
return df return evals_notes, evaluations
def compute_module_moy(evals_notes: pd.DataFrame, evals_poids: pd.DataFrame): def normalize_evals_notes(evals_notes: pd.DataFrame, evaluations: list) -> pd.DataFrame:
"""Transforme les notes brutes (en base) en valeurs entre 0 et 20:
les notes manquantes, ABS, EXC ATT sont mises à zéro, et les valeurs
normalisées entre 0 et 20.
Return: notes sur 20"""
# Le fillna (pour traiter les ABS) est inutile car le where matche le NaN
# eval_df.fillna(value=0.0, inplace=True)
return evals_notes.where(evals_notes > -1000, 0) / [
e.note_max / 20.0 for e in evaluations
]
def compute_module_moy(
evals_notes: pd.DataFrame,
evals_poids: pd.DataFrame,
evals_coefs=1.0,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module """Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid - evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
@ -123,6 +144,8 @@ def compute_module_moy(evals_notes: pd.DataFrame, evals_poids: pd.DataFrame):
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs - evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
- evals_coefs: sequence, 1 coef par UE
Résultat: DataFrame, colonnes UE, lignes etud Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module. = la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes) ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
@ -131,7 +154,7 @@ def compute_module_moy(evals_notes: pd.DataFrame, evals_poids: pd.DataFrame):
nb_etuds = len(evals_notes) nb_etuds = len(evals_notes)
nb_ues = evals_poids.shape[1] nb_ues = evals_poids.shape[1]
etud_moy_module_arr = np.zeros((nb_etuds, nb_ues)) etud_moy_module_arr = np.zeros((nb_etuds, nb_ues))
evals_poids_arr = evals_poids.to_numpy().transpose() evals_poids_arr = evals_poids.to_numpy().transpose() * evals_coefs
evals_notes_arr = evals_notes.values # .to_numpy() evals_notes_arr = evals_notes.values # .to_numpy()
val_neutres = np.array((scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE)) val_neutres = np.array((scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE))
for i in range(nb_etuds): for i in range(nb_etuds):

View File

@ -46,7 +46,7 @@ class Evaluation(db.Model):
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
def __repr__(self): def __repr__(self):
return f"<Evaluation {self.id} {self.jour.isoformat()}" return f"<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''}{self.description[:16] if self.description else ''}"
def to_dict(self): def to_dict(self):
e = dict(self.__dict__) e = dict(self.__dict__)

View File

@ -5,6 +5,7 @@ from typing import Any
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
@ -86,6 +87,16 @@ class UniteEns(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>" return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
return e
def is_locked(self): def is_locked(self):
"""True if UE should not be modified """True if UE should not be modified
(contains modules used in a locked formsemestre) (contains modules used in a locked formsemestre)
@ -95,6 +106,20 @@ class UniteEns(db.Model):
return sco_edit_ue.ue_is_locked(self.id) return sco_edit_ue.ue_is_locked(self.id)
def guess_semestre_idx(self) -> None:
"""Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre.
Cette méthode fixe le semestre en prenant celui du premier module,
ou à défaut le met à 1.
"""
if self.semestre_idx is None:
if self.modules:
self.semestre_idx = self.modules[0].semestre_id
else:
self.semestre_idx = 1
db.session.add(self)
db.session.commit()
class Matiere(db.Model): class Matiere(db.Model):
"""Matières: regroupe les modules d'une UE """Matières: regroupe les modules d'une UE

View File

@ -587,7 +587,11 @@ def module_edit(module_id=None):
), ),
] ]
# force module semestre_idx to its UE # force module semestre_idx to its UE
module["semestre_id"] = a_module.ue.semestre_idx if a_module.ue.semestre_idx:
module["semestre_id"] = a_module.ue.semestre_idx
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
if not module["semestre_id"]:
module["semestre_id"] = 1
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),

View File

@ -467,8 +467,19 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
else: else:
semestre_idx = int(semestre_idx) semestre_idx = int(semestre_idx)
semestre_ids = range(1, parcours.NB_SEM + 1) semestre_ids = range(1, parcours.NB_SEM + 1)
ues = ue_list(args={"formation_id": formation_id, "is_external": False}) # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True}) # basées sur des dicts
ues_obj = UniteEns.query.filter_by(formation_id=formation_id, is_external=False)
ues_externes_obj = UniteEns.query.filter_by(
formation_id=formation_id, is_external=True
)
if is_apc:
# pour faciliter la transition des anciens programmes non APC
for ue in ues_obj:
ue.guess_semestre_idx()
ues = [ue.to_dict() for ue in ues_obj]
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
# tri par semestre et numero: # tri par semestre et numero:
_add_ue_semestre_id(ues, is_apc) _add_ue_semestre_id(ues, is_apc)
_add_ue_semestre_id(ues_externes, is_apc) _add_ue_semestre_id(ues_externes, is_apc)
@ -928,78 +939,6 @@ def _ue_table_matieres(
return "\n".join(H) return "\n".join(H)
def _ue_table_ressources_saes(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
module_type=None,
):
"""Édition de programme: liste des ressources et SAÉs d'une UE.
(pour les parcours APC_SAE)
"""
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
if not matieres:
# Les formations APC (BUT) n'utilisent pas de matières
# mais il doit y en avoir une par UE
# silently fix this on-the-fly to ease migration
_ = sco_edit_matiere.do_matiere_create(
{"ue_id": ue["ue_id"], "titre": "APC", "numero": 1},
)
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
assert matieres
mat = matieres[0]
H = [
"""
<ul class="notes_matiere_list but_matiere_list">
"""
]
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
for titre, element_name, element_type in (
("Ressources", "ressource", scu.ModuleType.RESSOURCE),
("SAÉs", "SAÉ", scu.ModuleType.SAE),
("Autres modules", "xxx", None),
):
H.append(f'<li class="notes_matiere_list">{titre}')
elements = [
m
for m in modules
if element_type == m["module_type"]
or (
(element_type is None)
and m["module_type"]
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
)
]
H.append(
_ue_table_modules(
parcours,
mat,
elements,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
module_type=module_type,
empty_list_msg="Aucune " + element_name,
create_element_msg="créer une " + element_name,
add_suppress_link=False,
)
)
H.append("</li></ul>")
return "\n".join(H)
def _ue_table_modules( def _ue_table_modules(
parcours, parcours,
mat, mat,

View File

@ -31,9 +31,11 @@
import flask import flask
from flask import url_for, g, request from flask import url_for, g, request
from app import models
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.comp import moy_mod
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
@ -204,6 +206,7 @@ def do_evaluation_listenotes():
group_ids=tf[2]["group_ids"], group_ids=tf[2]["group_ids"],
hide_groups=hide_groups, hide_groups=hide_groups,
with_emails=with_emails, with_emails=with_emails,
mode=mode,
) )
@ -216,15 +219,22 @@ def _make_table_notes(
hide_groups=False, hide_groups=False,
with_emails=False, with_emails=False,
group_ids=[], group_ids=[],
mode="module", # "eval" or "module"
): ):
"""Table liste notes (une seule évaluation ou toutes celles d'un module)""" """Table liste notes (une seule évaluation ou toutes celles d'un module)"""
# Code à ré-écrire !
if not evals: if not evals:
return "<p>Aucune évaluation !</p>" return "<p>Aucune évaluation !</p>"
E = evals[0] E = evals[0]
moduleimpl_id = E["moduleimpl_id"] moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] module = models.Module.query.get(modimpl["module_id"])
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) is_apc = module.formation.get_parcours().APC_SAE
if is_apc:
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id)
else:
evals_poids, ues = None, None
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
# (debug) check that all evals are in same module: # (debug) check that all evals are in same module:
for e in evals: for e in evals:
if e["moduleimpl_id"] != moduleimpl_id: if e["moduleimpl_id"] != moduleimpl_id:
@ -236,16 +246,12 @@ def _make_table_notes(
keep_numeric = False keep_numeric = False
# Si pas de groupe, affiche tout # Si pas de groupe, affiche tout
if not group_ids: if not group_ids:
group_ids = [sco_groups.get_default_group(M["formsemestre_id"])] group_ids = [sco_groups.get_default_group(modimpl["formsemestre_id"])]
groups = sco_groups.listgroups(group_ids) groups = sco_groups.listgroups(group_ids)
gr_title = sco_groups.listgroups_abbrev(groups) gr_title = sco_groups.listgroups_abbrev(groups)
gr_title_filename = sco_groups.listgroups_filename(groups) gr_title_filename = sco_groups.listgroups_filename(groups)
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
E["evaluation_id"], groups, include_dems=True
)
if anonymous_listing: if anonymous_listing:
columns_ids = ["code"] # cols in table columns_ids = ["code"] # cols in table
else: else:
@ -269,7 +275,7 @@ def _make_table_notes(
rows = [] rows = []
class keymgr(dict): # comment : key (pour regrouper les comments a la fin) class KeyManager(dict): # comment : key (pour regrouper les comments a la fin)
def __init__(self): def __init__(self):
self.lastkey = 1 self.lastkey = 1
@ -279,7 +285,19 @@ def _make_table_notes(
# self.lastkey = chr(ord(self.lastkey)+1) # self.lastkey = chr(ord(self.lastkey)+1)
return str(r) return str(r)
K = keymgr() key_mgr = KeyManager()
# code pour listings anonyme, à la place du nom
if sco_preferences.get_preference("anonymous_lst_code") == "INE":
anonymous_lst_key = "code_ine"
elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
anonymous_lst_key = "code_nip"
else:
anonymous_lst_key = "etudid"
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
E["evaluation_id"], groups, include_dems=True
)
for etudid, etat in etudid_etats: for etudid, etat in etudid_etats:
css_row_class = None css_row_class = None
# infos identite etudiant # infos identite etudiant
@ -295,11 +313,7 @@ def _make_table_notes(
else: else:
grc = etat grc = etat
code = "" # code pour listings anonyme, à la place du nom code = etud.get(anonymous_lst_key)
if sco_preferences.get_preference("anonymous_lst_code") == "INE":
code = etud["code_ine"]
elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
code = etud["code_nip"]
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
code = etudid code = etudid
@ -310,7 +324,7 @@ def _make_table_notes(
"etudid": etudid, "etudid": etudid,
"nom": etud["nom"].upper(), "nom": etud["nom"].upper(),
"_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" "_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
% (M["formsemestre_id"], etudid), % (modimpl["formsemestre_id"], etudid),
"_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]), "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
"prenom": etud["prenom"].lower().capitalize(), "prenom": etud["prenom"].lower().capitalize(),
"nomprenom": etud["nomprenom"], "nomprenom": etud["nomprenom"],
@ -322,7 +336,7 @@ def _make_table_notes(
) )
# Lignes en tête: # Lignes en tête:
coefs = { row_coefs = {
"nom": "", "nom": "",
"prenom": "", "prenom": "",
"nomprenom": "", "nomprenom": "",
@ -331,7 +345,16 @@ def _make_table_notes(
"_css_row_class": "sorttop fontitalic", "_css_row_class": "sorttop fontitalic",
"_table_part": "head", "_table_part": "head",
} }
note_max = { row_poids = {
"nom": "",
"prenom": "",
"nomprenom": "",
"group": "",
"code": "",
"_css_row_class": "sorttop poids",
"_table_part": "head",
}
row_note_max = {
"nom": "", "nom": "",
"prenom": "", "prenom": "",
"nomprenom": "", "nomprenom": "",
@ -340,7 +363,7 @@ def _make_table_notes(
"_css_row_class": "sorttop fontitalic", "_css_row_class": "sorttop fontitalic",
"_table_part": "head", "_table_part": "head",
} }
moys = { row_moys = {
"_css_row_class": "moyenne sortbottom", "_css_row_class": "moyenne sortbottom",
"_table_part": "foot", "_table_part": "foot",
#'_nomprenom_td_attrs' : 'colspan="2" ', #'_nomprenom_td_attrs' : 'colspan="2" ',
@ -352,12 +375,16 @@ def _make_table_notes(
e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
notes, nb_abs, nb_att = _add_eval_columns( notes, nb_abs, nb_att = _add_eval_columns(
e, e,
evals_poids,
ues,
rows, rows,
titles, titles,
coefs, row_coefs,
note_max, row_poids,
moys, row_note_max,
K, row_moys,
is_apc,
key_mgr,
note_sur_20, note_sur_20,
keep_numeric, keep_numeric,
) )
@ -370,28 +397,51 @@ def _make_table_notes(
key=lambda x: (x["nom"] or "", x["prenom"] or "") key=lambda x: (x["nom"] or "", x["prenom"] or "")
) # sort by nom, prenom ) # sort by nom, prenom
# Si module, ajoute moyenne du module: # Si module, ajoute la (les) "moyenne(s) du module:
if len(evals) > 1: if mode == "module":
_add_moymod_column( if len(evals) > 1:
sem["formsemestre_id"], # Moyenne de l'étudant dans le module
e, # Affichée même en APC à titre indicatif
rows, _add_moymod_column(
titles, sem["formsemestre_id"],
coefs, moduleimpl_id,
note_max, rows,
moys, columns_ids,
note_sur_20, titles,
keep_numeric, row_coefs,
) row_poids,
columns_ids.append("moymod") row_note_max,
row_moys,
is_apc,
keep_numeric,
)
if is_apc:
# Ajoute une colonne par UE
_add_apc_columns(
moduleimpl_id,
evals_poids,
ues,
rows,
columns_ids,
titles,
row_coefs,
row_poids,
row_note_max,
row_moys,
keep_numeric,
)
# Ajoute colonnes emails tout à droite: # Ajoute colonnes emails tout à droite:
if with_emails: if with_emails:
columns_ids += ["email", "emailperso"] columns_ids += ["email", "emailperso"]
# Ajoute lignes en tête et moyennes # Ajoute lignes en tête et moyennes
if len(evals) > 0: if len(evals) > 0:
rows = [coefs, note_max] + rows rows_head = [row_coefs]
rows.append(moys) if is_apc:
rows_head.append(row_poids)
rows_head.append(row_note_max)
rows = rows_head + rows
rows.append(row_moys)
# ajout liens HTMl vers affichage une evaluation: # ajout liens HTMl vers affichage une evaluation:
if format == "html" and len(evals) > 1: if format == "html" and len(evals) > 1:
rlinks = {"_table_part": "head"} rlinks = {"_table_part": "head"}
@ -425,7 +475,7 @@ def _make_table_notes(
if with_emails: if with_emails:
gl = "&with_emails%3Alist=yes" + gl gl = "&with_emails%3Alist=yes" + gl
if len(evals) == 1: if len(evals) == 1:
evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"])) evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"]))
hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats)) hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats))
filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename))
caption = hh caption = hh
@ -437,8 +487,8 @@ def _make_table_notes(
% (nb_abs, nb_att) % (nb_abs, nb_att)
) )
else: else:
filename = scu.make_filename("notes_%s_%s" % (Mod["code"], gr_title_filename)) filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
title = "Notes du module %(code)s %(titre)s" % Mod title = f"Notes {module.type_name()} {module.code} {module.titre}"
title += " semestre %(titremois)s" % sem title += " semestre %(titremois)s" % sem
if gr_title and gr_title != "tous": if gr_title and gr_title != "tous":
title += " %s" % gr_title title += " %s" % gr_title
@ -447,10 +497,11 @@ def _make_table_notes(
if format == "pdf": if format == "pdf":
caption = "" # same as pdf_title caption = "" # same as pdf_title
pdf_title = title pdf_title = title
html_title = ( html_title = f"""<h2 class="formsemestre">Notes {module.type_name()} <a href="{
"""<h2 class="formsemestre">Notes du module <a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a></h2>""" url_for("notes.moduleimpl_status",
% (moduleimpl_id, Mod["code"], Mod["titre"]) scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
) }">{module.code} {module.titre}</a></h2>
"""
base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl
# display # display
tab = GenTable( tab = GenTable(
@ -469,7 +520,7 @@ def _make_table_notes(
html_title=html_title, html_title=html_title,
pdf_title=pdf_title, pdf_title=pdf_title,
html_class="table_leftalign notes_evaluation", html_class="table_leftalign notes_evaluation",
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), preferences=sco_preferences.SemPreferences(modimpl["formsemestre_id"]),
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
) )
@ -497,7 +548,7 @@ def _make_table_notes(
+ "</div></td>\n", + "</div></td>\n",
'<td style="padding-left: 50px; vertical-align: top;"><p>', '<td style="padding-left: 50px; vertical-align: top;"><p>',
] ]
commentkeys = list(K.items()) # [ (comment, key), ... ] commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
commentkeys.sort(key=lambda x: int(x[1])) commentkeys.sort(key=lambda x: int(x[1]))
for (comment, key) in commentkeys: for (comment, key) in commentkeys:
C.append( C.append(
@ -526,7 +577,19 @@ def _make_table_notes(
def _add_eval_columns( def _add_eval_columns(
e, rows, titles, coefs, note_max, moys, K, note_sur_20, keep_numeric e,
evals_poids,
ues,
rows,
titles,
row_coefs,
row_poids,
row_note_max,
row_moys,
is_apc,
K,
note_sur_20,
keep_numeric,
): ):
"""Add eval e""" """Add eval e"""
nb_notes = 0 nb_notes = 0
@ -594,19 +657,23 @@ def _add_eval_columns(
} }
) )
coefs[evaluation_id] = "coef. %s" % e["coefficient"] row_coefs[evaluation_id] = "coef. %s" % e["coefficient"]
if is_apc:
row_poids[evaluation_id] = _mini_table_eval_ue_poids(
evaluation_id, evals_poids, ues
)
if note_sur_20: if note_sur_20:
nmax = 20.0 nmax = 20.0
else: else:
nmax = e["note_max"] nmax = e["note_max"]
if keep_numeric: if keep_numeric:
note_max[evaluation_id] = nmax row_note_max[evaluation_id] = nmax
else: else:
note_max[evaluation_id] = "/ %s" % nmax row_note_max[evaluation_id] = "/ %s" % nmax
if nb_notes > 0: if nb_notes > 0:
moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes) row_moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes)
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,
@ -614,7 +681,7 @@ def _add_eval_columns(
e["jour"], e["jour"],
) )
else: else:
moys[evaluation_id] = "" row_moys[evaluation_id] = ""
titles[evaluation_id] = "%(description)s (%(jour)s)" % e titles[evaluation_id] = "%(description)s (%(jour)s)" % e
@ -628,15 +695,29 @@ def _add_eval_columns(
return notes, nb_abs, nb_att # pour histogramme return notes, nb_abs, nb_att # pour histogramme
def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues):
"contenu de la cellule: poids"
return (
"""<table class="eval_poids" title="poids vers les UE"><tr><td>"""
+ "</td><td>".join([f"{ue.acronyme}" for ue in ues])
+ "</td></tr>"
+ "<tr><td>"
+ "</td><td>".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues])
+ "</td></tr></table>"
)
def _add_moymod_column( def _add_moymod_column(
formsemestre_id, formsemestre_id,
e, moduleimpl_id,
rows, rows,
columns_ids,
titles, titles,
coefs, row_coefs,
note_max, row_poids,
moys, row_note_max,
note_sur_20, row_moys,
is_apc,
keep_numeric, keep_numeric,
): ):
"""Ajoute la colonne moymod à rows""" """Ajoute la colonne moymod à rows"""
@ -647,23 +728,61 @@ def _add_moymod_column(
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
for row in rows: for row in rows:
etudid = row["etudid"] etudid = row["etudid"]
val = nt.get_etud_mod_moy( val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
e["moduleimpl_id"], etudid
) # note sur 20, ou 'NA','NI'
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric) row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if not isinstance(val, str): if not isinstance(val, str):
notes.append(val) notes.append(val)
nb_notes = nb_notes + 1 nb_notes = nb_notes + 1
sum_notes += val sum_notes += val
coefs[col_id] = "(avec abs)" row_coefs[col_id] = "(avec abs)"
if is_apc:
row_poids[col_id] = "à titre indicatif"
if keep_numeric: if keep_numeric:
note_max[col_id] = 20.0 row_note_max[col_id] = 20.0
else: else:
note_max[col_id] = "/ 20" row_note_max[col_id] = "/ 20"
titles[col_id] = "Moyenne module" titles[col_id] = "Moyenne module"
columns_ids.append(col_id)
if nb_notes > 0: if nb_notes > 0:
moys[col_id] = "%.3g" % (sum_notes / nb_notes) row_moys[col_id] = "%.3g" % (sum_notes / nb_notes)
moys["_" + col_id + "_help"] = "moyenne des moyennes" row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
else: else:
moys[col_id] = "" row_moys[col_id] = ""
def _add_apc_columns(
moduleimpl_id,
evals_poids,
ues,
rows,
columns_ids,
titles,
row_coefs,
row_poids,
row_note_max,
row_moys,
keep_numeric,
):
"""Ajoute les colonnes moyennes vers les UE"""
# On raccorde ici les nouveaux calculs de notes (BUT 2021)
# sur l'ancien code ScoDoc
# => On recharge tout dans les nouveaux modèles
# rows est une liste de dict avec une clé "etudid"
# on va y ajouter une clé par UE du semestre
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(moduleimpl_id)
evals_notes_sur_20 = moy_mod.normalize_evals_notes(evals_notes, evaluations)
etud_moy_module = moy_mod.compute_module_moy(
evals_notes_sur_20, evals_poids, [e.coefficient for e in evaluations]
)
for row in rows:
for ue in ues:
moy_ue = etud_moy_module[ue.id].get(row["etudid"], "?")
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
for ue in ues:
col_id = f"moy_ue_{ue.id}"
titles[col_id] = ue.acronyme
columns_ids.append(col_id)

View File

@ -255,7 +255,7 @@ def do_evaluation_upload_xls():
diag.append("Notes invalides pour: " + ", ".join(etudsnames)) diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue() raise InvalidNoteValue()
else: else:
nb_changed, nb_suppress, existing_decisions = _notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
authuser, evaluation_id, L, comment authuser, evaluation_id, L, comment
) )
# news # news
@ -345,7 +345,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
) )
# ok # ok
comment = "Initialisation notes manquantes" comment = "Initialisation notes manquantes"
nb_changed, _, _ = _notes_add(current_user, evaluation_id, L, comment) nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
# news # news
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
@ -407,7 +407,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()] notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]
if not dialog_confirmed: if not dialog_confirmed:
nb_changed, nb_suppress, existing_decisions = _notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, do_it=False current_user, evaluation_id, notes, do_it=False
) )
msg = ( msg = (
@ -425,7 +425,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
) )
# modif # modif
nb_changed, nb_suppress, existing_decisions = _notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, comment="effacer tout" current_user, evaluation_id, notes, comment="effacer tout"
) )
assert nb_changed == nb_suppress assert nb_changed == nb_suppress
@ -454,7 +454,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True): def notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True) -> tuple:
""" """
Insert or update notes Insert or update notes
notes is a list of tuples (etudid,value) notes is a list of tuples (etudid,value)
@ -462,7 +462,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
WOULD be changed or suppressed. WOULD be changed or suppressed.
Nota: Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log) - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
Return number of changed notes Return tuple (nb_changed, nb_suppress, existing_decisions)
""" """
now = psycopg2.Timestamp( now = psycopg2.Timestamp(
*time.localtime()[:6] *time.localtime()[:6]
@ -563,7 +563,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
else: # suppression ancienne note else: # suppression ancienne note
if do_it: if do_it:
log( log(
"_notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s" "notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
% (evaluation_id, etudid, oldval) % (evaluation_id, etudid, oldval)
) )
cursor.execute( cursor.execute(
@ -587,7 +587,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
if has_existing_decision(M, E, etudid): if has_existing_decision(M, E, etudid):
existing_decisions.append(etudid) existing_decisions.append(etudid)
except: except:
log("*** exception in _notes_add") log("*** exception in notes_add")
if do_it: if do_it:
cnx.rollback() # abort cnx.rollback() # abort
# inval cache # inval cache
@ -1265,7 +1265,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
else: else:
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod) L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
if L: if L:
nbchanged, _, existing_decisions = _notes_add( nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True authuser, evaluation_id, L, comment=comment, do_it=True
) )
sco_news.add( sco_news.add(

View File

@ -167,7 +167,7 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds):
description="note externe", description="note externe",
) )
# Saisie des notes # Saisie des notes
_, _, _ = sco_saisie_notes._notes_add( _, _, _ = sco_saisie_notes.notes_add(
current_user, current_user,
evaluation_id, evaluation_id,
list(notes_etuds.items()), list(notes_etuds.items()),

View File

@ -44,6 +44,7 @@ import unicodedata
import urllib import urllib
from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode
import numpy as np
from PIL import Image as PILImage from PIL import Image as PILImage
import pydot import pydot
import requests import requests
@ -138,6 +139,8 @@ def fmt_note(val, note_max=None, keep_numeric=False):
if val == NOTES_ATTENTE: if val == NOTES_ATTENTE:
return "ATT" # attente, note neutralisee return "ATT" # attente, note neutralisee
if isinstance(val, float) or isinstance(val, int): if isinstance(val, float) or isinstance(val, int):
if np.isnan(val):
return "/"
if note_max != None and note_max > 0: if note_max != None and note_max > 0:
val = val * 20.0 / note_max val = val * 20.0 / note_max
if keep_numeric: if keep_numeric:

View File

@ -1040,6 +1040,14 @@ td.colcomment, span.colcomment {
color: rgb(80,100,80); color: rgb(80,100,80);
} }
table.notes_evaluation table.eval_poids {
font-size: 50%;
}
table.notes_evaluation td.moy_ue {
font-weight: bold;
color:rgb(1, 116, 96);
}
h2.formsemestre, .gtrcontent h2 { h2.formsemestre, .gtrcontent h2 {
margin-top: 2px; margin-top: 2px;
font-size: 130%; font-size: 130%;

View File

@ -302,7 +302,7 @@ class ScoFake(object):
): ):
if user is None: if user is None:
user = self.default_user user = self.default_user
return sco_saisie_notes._notes_add( return sco_saisie_notes.notes_add(
user, user,
evaluation["evaluation_id"], evaluation["evaluation_id"],
[(etud["etudid"], note)], [(etud["etudid"], note)],

View File

@ -3,13 +3,14 @@ Test modèles évaluations avec poids BUT
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from app.models.etudiants import Identite
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
from app import db from app import db
from app import models from app import models
from app.comp import moy_mod from app.comp import moy_mod
from app.comp import moy_ue from app.comp import moy_ue
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours, sco_saisie_notes
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE
""" """
@ -175,28 +176,29 @@ def _setup_module_evaluation(ue_coefs=(1.0, 2.0, 3.0)):
coefficient=0, coefficient=0,
) )
evaluation_id = _e1["evaluation_id"] evaluation_id = _e1["evaluation_id"]
return formation_id, evaluation_id, ue1, ue2, ue3 return G, formation_id, sem, evaluation_id, ue1, ue2, ue3
def test_module_conformity(test_client): def test_module_conformity(test_client):
"""Vérification coefficients module<->UE vs poids des évaluations""" """Vérification coefficients module<->UE vs poids des évaluations"""
formation_id, evaluation_id, ue1, ue2, ue3 = _setup_module_evaluation() _, formation_id, _, evaluation_id, ue1, ue2, ue3 = _setup_module_evaluation()
semestre_idx = 2 semestre_idx = 2
nb_ues = 3 # 3 UEs dans ce test nb_ues = 3 # 3 UEs dans ce test
nb_mods = 1 # 1 seul module nb_mods = 1 # 1 seul module
nb_evals = 1 # 1 seule evaluation pour l'instant nb_evals = 1 # 1 seule evaluation pour l'instant
p1, p2, p3 = 1.0, 2.0, 0.0 # poids de l'éval vers les UE 1, 2 et 3 p1, p2, p3 = 1.0, 2.0, 0.0 # poids de l'éval vers les UE 1, 2 et 3
evaluation = models.Evaluation.query.get(evaluation_id) evaluation = models.Evaluation.query.get(evaluation_id)
evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2}) evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2, ue3.id: p3})
assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2} assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2, ue3.id: p3}
# On n'est pas conforme car p3 est nul alors que c3 est non nul # On n'est pas conforme car p3 est nul alors que c3 est non nul
modules_coefficients, _ues, _modules = moy_ue.df_load_ue_coefs( modules_coefficients, _ues, _modules = moy_ue.df_load_ue_coefs(
formation_id, semestre_idx formation_id, semestre_idx
) )
assert isinstance(modules_coefficients, pd.DataFrame) assert isinstance(modules_coefficients, pd.DataFrame)
assert modules_coefficients.shape == (nb_ues, nb_mods) assert modules_coefficients.shape == (nb_ues, nb_mods)
evals_poids = moy_mod.df_load_evaluations_poids(evaluation.moduleimpl_id) evals_poids, ues = moy_mod.df_load_evaluations_poids(evaluation.moduleimpl_id)
assert isinstance(evals_poids, pd.DataFrame) assert isinstance(evals_poids, pd.DataFrame)
assert len(ues) == nb_ues
assert all(evals_poids.dtypes == np.float64) assert all(evals_poids.dtypes == np.float64)
assert evals_poids.shape == (nb_evals, nb_ues) assert evals_poids.shape == (nb_evals, nb_ues)
assert not moy_mod.check_moduleimpl_conformity( assert not moy_mod.check_moduleimpl_conformity(
@ -204,9 +206,9 @@ def test_module_conformity(test_client):
) )
def test_module_moy(): def test_module_moy_elem(test_client):
"""Vérification calcul moyenne d'un module """Vérification calcul moyenne d'un module
(calcul bas niveau) (notes entrées dans un DataFrame sans passer par ScoDoc)
""" """
# Repris du notebook CalculNotesBUT.ipynb # Repris du notebook CalculNotesBUT.ipynb
data = [ # Les notes de chaque étudiant dans les 2 evals: data = [ # Les notes de chaque étudiant dans les 2 evals:
@ -240,12 +242,71 @@ def test_module_moy():
{"UE1": 2, "UE2": 5, "UE3": 0}, {"UE1": 2, "UE2": 5, "UE3": 0},
] ]
evals_poids = pd.DataFrame(data, index=["EVAL1", "EVAL2"], dtype=float) evals_poids = pd.DataFrame(data, index=["EVAL1", "EVAL2"], dtype=float)
etud_moy_module_df = moy_mod.compute_module_moy(evals_notes, evals_poids) etud_moy_module_df = moy_mod.compute_module_moy(
evals_notes.fillna(0.0), evals_poids
)
NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN) NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
r = etud_moy_module_df.fillna(NAN) r = etud_moy_module_df.fillna(NAN)
tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN) assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN) assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
tuple(r.loc["etud3"]) == (13, NAN, NAN) assert tuple(r.loc["etud3"]) == (13, NAN, NAN)
tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN) assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN)
tuple(r.loc["etud5"]) == (0.0, 0.0, NAN) assert tuple(r.loc["etud5"]) == (0.0, 0.0, NAN)
# note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls # note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls
def test_module_moy(test_client):
"""Test calcul moyenne module avec saisie des notes via ScoDoc"""
coef_e1, coef_e2 = 7.0, 11.0 # coefficients des évaluations
G, formation_id, sem, evaluation1_id, ue1, ue2, ue3 = _setup_module_evaluation()
etud = G.create_etud(nom="test")
G.inscrit_etudiant(sem, etud)
etudid = etud["etudid"]
evaluation1 = models.Evaluation.query.get(evaluation1_id)
# Crée une deuxième évaluation dans le même moduleimpl:
evaluation2_id = G.create_evaluation(
moduleimpl_id=evaluation1.moduleimpl_id,
jour="02/01/2021",
description="evaluation 2",
coefficient=coef_e2,
)["evaluation_id"]
evaluation2 = models.Evaluation.query.get(evaluation2_id)
# Coefficients de l'eval 1
evaluation1.coefficient = coef_e1
# Poids des évaluations:
e1p1, e1p2, e1p3 = 1.0, 2.0, 0.0 # poids de l'éval 1 vers les UE 1, 2 et 3
e2p1, e2p2, e2p3 = 0.0, 1.0, 0.0 # poids de l'éval 2 vers les UE
evaluation1.set_ue_poids_dict({ue1.id: e1p1, ue2.id: e1p2, ue3.id: e1p3})
evaluation2.set_ue_poids_dict({ue1.id: e2p1, ue2.id: e2p2, ue3.id: e2p3})
# Saisie d'une note dans chaque éval
note1, note2 = 11.0, 12.0
t = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, note1)])
assert t == (1, 0, [])
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, note2)])
#
# Vérifications
moduleimpl_id = evaluation1.moduleimpl_id
nb_evals = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count()
assert nb_evals == 2
nb_ues = 3
# Calcul de la moyenne du module
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id)
assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(moduleimpl_id)
evals_notes_sur_20 = moy_mod.normalize_evals_notes(evals_notes, evaluations)
etud_moy_module = moy_mod.compute_module_moy(
evals_notes_sur_20, evals_poids, [coef_e1, coef_e2]
)
# Moyenne dans les UE 1, 2, 3:
moy_ue1 = etud_moy_module[ue1.id][etudid]
assert moy_ue1 == ((note1 * e1p1 * coef_e1) + (note2 * e2p1 * coef_e2)) / (
e1p1 * coef_e1 + e2p1 * coef_e2
)
moy_ue2 = etud_moy_module[ue2.id][etudid]
assert moy_ue2 == ((note1 * e1p2 * coef_e1) + (note2 * e2p2 * coef_e2)) / (
e1p2 * coef_e1 + e2p2 * coef_e2
)
moy_ue3 = etud_moy_module[ue3.id][etudid]
assert np.isnan(moy_ue3)
# moy_ue3 == ((note1 * e1p3 * coef_e1) + (note2 * e2p3 * coef_e2)) / (
# e1p3 * coef_e1 + e2p3 * coef_e2)

View File

@ -274,6 +274,10 @@ def test_notes_modules(test_client):
{"etudid": etudid, "moduleimpl_id": mi2["moduleimpl_id"]}, {"etudid": etudid, "moduleimpl_id": mi2["moduleimpl_id"]},
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
sco_moduleimpl.do_moduleimpl_inscription_create(
{"etudid": etuds[1]["etudid"], "moduleimpl_id": mi2["moduleimpl_id"]},
formsemestre_id=formsemestre_id,
)
nt = sco_cache.NotesTableCache.get(formsemestre_id) nt = sco_cache.NotesTableCache.get(formsemestre_id)
ue_status = nt.get_etud_ue_status(etudid, ue_id) ue_status = nt.get_etud_ue_status(etudid, ue_id)
assert ue_status["nb_missing"] == 1 # mi2 n'a pas encore de note assert ue_status["nb_missing"] == 1 # mi2 n'a pas encore de note
@ -288,8 +292,8 @@ def test_notes_modules(test_client):
_, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=19.5) _, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=19.5)
nt = sco_cache.NotesTableCache.get(formsemestre_id) nt = sco_cache.NotesTableCache.get(formsemestre_id)
ue_status = nt.get_etud_ue_status(etudid, ue_id) ue_status = nt.get_etud_ue_status(etudid, ue_id)
assert ue_status["nb_missing"] == 0 assert ue_status["nb_missing"] == 1 # manque une note
assert ue_status["nb_notes"] == 2 assert ue_status["nb_notes"] == 1
# Moyenne d'UE si l'un des modules est EXC ("NA") # Moyenne d'UE si l'un des modules est EXC ("NA")
# 2 modules, notes EXC dans le premier, note valide n dans le second # 2 modules, notes EXC dans le premier, note valide n dans le second