1
0
forked from ScoDoc/ScoDoc

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
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
rows = evaluations, columns = UE, value = poids (float).
Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies par default_poids.
Résultat: (evals_poids, liste de UE du semestre)
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
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
if default_poids is not None:
df.fillna(value=default_poids, inplace=True)
return df
return df, ues
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)
index (lignes): etudid
Résultat: (evals_notes, liste de évaluations du moduleimpl)
L'ensemble des étudiants est celui des inscrits au module.
Valeurs des notes:
note : float (valeur enregistrée brute, pas normalisée sur 20)
Les notes renvoyées sont "brutes" et peuvent prendre els valeurs:
note : float (valeur enregistrée brute, non normalisée sur 20)
pas de note: NaN
absent: 0.
absent: NaN
excusé: NOTES_NEUTRALISE (voir sco_utils)
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]
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:
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},
index_col="etudid",
)
# Remplace les ABS (NULL en BD, donc NaN ici) par des zéros.
eval_df.fillna(value=0.0, inplace=True)
df = df.merge(eval_df, how="outer", left_index=True, right_index=True)
evals_notes = evals_notes.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
- 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_coefs: sequence, 1 coef par UE
Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
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_ues = evals_poids.shape[1]
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()
val_neutres = np.array((scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE))
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)
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):
e = dict(self.__dict__)

View File

@ -5,6 +5,7 @@ from typing import Any
from app import db
from app.models import APO_CODE_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.sco_utils import ModuleType
from app.scodoc import sco_codes_parcours
@ -86,6 +87,16 @@ class UniteEns(db.Model):
def __repr__(self):
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):
"""True if UE should not be modified
(contains modules used in a locked formsemestre)
@ -95,6 +106,20 @@ class UniteEns(db.Model):
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):
"""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
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(
request.base_url,
scu.get_request_args(),

View File

@ -467,8 +467,19 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
else:
semestre_idx = int(semestre_idx)
semestre_ids = range(1, parcours.NB_SEM + 1)
ues = ue_list(args={"formation_id": formation_id, "is_external": False})
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
# 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:
_add_ue_semestre_id(ues, is_apc)
_add_ue_semestre_id(ues_externes, is_apc)
@ -928,78 +939,6 @@ def _ue_table_matieres(
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(
parcours,
mat,

View File

@ -31,9 +31,11 @@
import flask
from flask import url_for, g, request
from app import models
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.comp import moy_mod
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
@ -204,6 +206,7 @@ def do_evaluation_listenotes():
group_ids=tf[2]["group_ids"],
hide_groups=hide_groups,
with_emails=with_emails,
mode=mode,
)
@ -216,15 +219,22 @@ def _make_table_notes(
hide_groups=False,
with_emails=False,
group_ids=[],
mode="module", # "eval" or "module"
):
"""Table liste notes (une seule évaluation ou toutes celles d'un module)"""
# Code à ré-écrire !
if not evals:
return "<p>Aucune évaluation !</p>"
E = evals[0]
moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
module = models.Module.query.get(modimpl["module_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:
for e in evals:
if e["moduleimpl_id"] != moduleimpl_id:
@ -236,16 +246,12 @@ def _make_table_notes(
keep_numeric = False
# Si pas de groupe, affiche tout
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)
gr_title = sco_groups.listgroups_abbrev(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:
columns_ids = ["code"] # cols in table
else:
@ -269,7 +275,7 @@ def _make_table_notes(
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):
self.lastkey = 1
@ -279,7 +285,19 @@ def _make_table_notes(
# self.lastkey = chr(ord(self.lastkey)+1)
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:
css_row_class = None
# infos identite etudiant
@ -295,11 +313,7 @@ def _make_table_notes(
else:
grc = etat
code = "" # code pour listings anonyme, à la place du nom
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"]
code = etud.get(anonymous_lst_key)
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
code = etudid
@ -310,7 +324,7 @@ def _make_table_notes(
"etudid": etudid,
"nom": etud["nom"].upper(),
"_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"]),
"prenom": etud["prenom"].lower().capitalize(),
"nomprenom": etud["nomprenom"],
@ -322,7 +336,7 @@ def _make_table_notes(
)
# Lignes en tête:
coefs = {
row_coefs = {
"nom": "",
"prenom": "",
"nomprenom": "",
@ -331,7 +345,16 @@ def _make_table_notes(
"_css_row_class": "sorttop fontitalic",
"_table_part": "head",
}
note_max = {
row_poids = {
"nom": "",
"prenom": "",
"nomprenom": "",
"group": "",
"code": "",
"_css_row_class": "sorttop poids",
"_table_part": "head",
}
row_note_max = {
"nom": "",
"prenom": "",
"nomprenom": "",
@ -340,7 +363,7 @@ def _make_table_notes(
"_css_row_class": "sorttop fontitalic",
"_table_part": "head",
}
moys = {
row_moys = {
"_css_row_class": "moyenne sortbottom",
"_table_part": "foot",
#'_nomprenom_td_attrs' : 'colspan="2" ',
@ -352,12 +375,16 @@ def _make_table_notes(
e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
notes, nb_abs, nb_att = _add_eval_columns(
e,
evals_poids,
ues,
rows,
titles,
coefs,
note_max,
moys,
K,
row_coefs,
row_poids,
row_note_max,
row_moys,
is_apc,
key_mgr,
note_sur_20,
keep_numeric,
)
@ -370,28 +397,51 @@ def _make_table_notes(
key=lambda x: (x["nom"] or "", x["prenom"] or "")
) # sort by nom, prenom
# Si module, ajoute moyenne du module:
if len(evals) > 1:
_add_moymod_column(
sem["formsemestre_id"],
e,
rows,
titles,
coefs,
note_max,
moys,
note_sur_20,
keep_numeric,
)
columns_ids.append("moymod")
# Si module, ajoute la (les) "moyenne(s) du module:
if mode == "module":
if len(evals) > 1:
# Moyenne de l'étudant dans le module
# Affichée même en APC à titre indicatif
_add_moymod_column(
sem["formsemestre_id"],
moduleimpl_id,
rows,
columns_ids,
titles,
row_coefs,
row_poids,
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:
if with_emails:
columns_ids += ["email", "emailperso"]
# Ajoute lignes en tête et moyennes
if len(evals) > 0:
rows = [coefs, note_max] + rows
rows.append(moys)
rows_head = [row_coefs]
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:
if format == "html" and len(evals) > 1:
rlinks = {"_table_part": "head"}
@ -425,7 +475,7 @@ def _make_table_notes(
if with_emails:
gl = "&with_emails%3Alist=yes" + gl
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))
filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename))
caption = hh
@ -437,8 +487,8 @@ def _make_table_notes(
% (nb_abs, nb_att)
)
else:
filename = scu.make_filename("notes_%s_%s" % (Mod["code"], gr_title_filename))
title = "Notes du module %(code)s %(titre)s" % Mod
filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
title = f"Notes {module.type_name()} {module.code} {module.titre}"
title += " semestre %(titremois)s" % sem
if gr_title and gr_title != "tous":
title += " %s" % gr_title
@ -447,10 +497,11 @@ def _make_table_notes(
if format == "pdf":
caption = "" # same as pdf_title
pdf_title = title
html_title = (
"""<h2 class="formsemestre">Notes du module <a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a></h2>"""
% (moduleimpl_id, Mod["code"], Mod["titre"])
)
html_title = f"""<h2 class="formsemestre">Notes {module.type_name()} <a href="{
url_for("notes.moduleimpl_status",
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
# display
tab = GenTable(
@ -469,7 +520,7 @@ def _make_table_notes(
html_title=html_title,
pdf_title=pdf_title,
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
)
@ -497,7 +548,7 @@ def _make_table_notes(
+ "</div></td>\n",
'<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]))
for (comment, key) in commentkeys:
C.append(
@ -526,7 +577,19 @@ def _make_table_notes(
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"""
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:
nmax = 20.0
else:
nmax = e["note_max"]
if keep_numeric:
note_max[evaluation_id] = nmax
row_note_max[evaluation_id] = nmax
else:
note_max[evaluation_id] = "/ %s" % nmax
row_note_max[evaluation_id] = "/ %s" % nmax
if nb_notes > 0:
moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes)
moys[
row_moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes)
row_moys[
"_" + str(evaluation_id) + "_help"
] = "moyenne sur %d notes (%s le %s)" % (
nb_notes,
@ -614,7 +681,7 @@ def _add_eval_columns(
e["jour"],
)
else:
moys[evaluation_id] = ""
row_moys[evaluation_id] = ""
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
@ -628,15 +695,29 @@ def _add_eval_columns(
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(
formsemestre_id,
e,
moduleimpl_id,
rows,
columns_ids,
titles,
coefs,
note_max,
moys,
note_sur_20,
row_coefs,
row_poids,
row_note_max,
row_moys,
is_apc,
keep_numeric,
):
"""Ajoute la colonne moymod à rows"""
@ -647,23 +728,61 @@ def _add_moymod_column(
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
for row in rows:
etudid = row["etudid"]
val = nt.get_etud_mod_moy(
e["moduleimpl_id"], etudid
) # note sur 20, ou 'NA','NI'
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if not isinstance(val, str):
notes.append(val)
nb_notes = nb_notes + 1
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:
note_max[col_id] = 20.0
row_note_max[col_id] = 20.0
else:
note_max[col_id] = "/ 20"
row_note_max[col_id] = "/ 20"
titles[col_id] = "Moyenne module"
columns_ids.append(col_id)
if nb_notes > 0:
moys[col_id] = "%.3g" % (sum_notes / nb_notes)
moys["_" + col_id + "_help"] = "moyenne des moyennes"
row_moys[col_id] = "%.3g" % (sum_notes / nb_notes)
row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
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))
raise InvalidNoteValue()
else:
nb_changed, nb_suppress, existing_decisions = _notes_add(
nb_changed, nb_suppress, existing_decisions = notes_add(
authuser, evaluation_id, L, comment
)
# news
@ -345,7 +345,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
)
# ok
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
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_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()]
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
)
msg = (
@ -425,7 +425,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
# 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"
)
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()
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
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.
Nota:
- 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(
*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
if do_it:
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)
)
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):
existing_decisions.append(etudid)
except:
log("*** exception in _notes_add")
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
# inval cache
@ -1265,7 +1265,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
else:
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
if L:
nbchanged, _, existing_decisions = _notes_add(
nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True
)
sco_news.add(

View File

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

View File

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

View File

@ -1040,6 +1040,14 @@ td.colcomment, span.colcomment {
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 {
margin-top: 2px;
font-size: 130%;

View File

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

View File

@ -3,13 +3,14 @@ Test modèles évaluations avec poids BUT
"""
import numpy as np
import pandas as pd
from app.models.etudiants import Identite
from tests.unit import sco_fake_gen
from app import db
from app import models
from app.comp import moy_mod
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
"""
@ -175,28 +176,29 @@ def _setup_module_evaluation(ue_coefs=(1.0, 2.0, 3.0)):
coefficient=0,
)
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):
"""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
nb_ues = 3 # 3 UEs dans ce test
nb_mods = 1 # 1 seul module
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
evaluation = models.Evaluation.query.get(evaluation_id)
evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2})
assert evaluation.get_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, ue3.id: p3}
# 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(
formation_id, semestre_idx
)
assert isinstance(modules_coefficients, pd.DataFrame)
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 len(ues) == nb_ues
assert all(evals_poids.dtypes == np.float64)
assert evals_poids.shape == (nb_evals, nb_ues)
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
(calcul bas niveau)
(notes entrées dans un DataFrame sans passer par ScoDoc)
"""
# Repris du notebook CalculNotesBUT.ipynb
data = [ # Les notes de chaque étudiant dans les 2 evals:
@ -240,12 +242,71 @@ def test_module_moy():
{"UE1": 2, "UE2": 5, "UE3": 0},
]
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)
r = etud_moy_module_df.fillna(NAN)
tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
tuple(r.loc["etud3"]) == (13, NAN, NAN)
tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN)
tuple(r.loc["etud5"]) == (0.0, 0.0, NAN)
assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
assert tuple(r.loc["etud3"]) == (13, NAN, NAN)
assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, 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
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"]},
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)
ue_status = nt.get_etud_ue_status(etudid, ue_id)
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)
nt = sco_cache.NotesTableCache.get(formsemestre_id)
ue_status = nt.get_etud_ue_status(etudid, ue_id)
assert ue_status["nb_missing"] == 0
assert ue_status["nb_notes"] == 2
assert ue_status["nb_missing"] == 1 # manque une note
assert ue_status["nb_notes"] == 1
# Moyenne d'UE si l'un des modules est EXC ("NA")
# 2 modules, notes EXC dans le premier, note valide n dans le second