WIP: refactoring calculs

This commit is contained in:
Emmanuel Viennet 2021-12-26 19:15:47 +01:00
parent adaef9fd24
commit 0fe5cdb409
28 changed files with 580 additions and 447 deletions

View File

@ -4,19 +4,17 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Génération bulletin BUT
"""
import datetime import datetime
from flask import url_for, g from flask import url_for, g
import numpy as np
import pandas as pd
from app import db
from app.comp import moy_ue, moy_sem, inscr_mod from app.comp import moy_ue, moy_sem, inscr_mod
from app.models import ModuleImpl
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_utils import jsnan, fmt_note from app.scodoc.sco_utils import fmt_note
from app.comp.res_sem import ResultatsSemestre, NotesTableCompat from app.comp.res_sem import ResultatsSemestre, NotesTableCompat
@ -37,9 +35,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
( (
self.sem_cube, self.sem_cube,
self.modimpls_evals_poids, self.modimpls_evals_poids,
self.modimpls_evals_notes, self.modimpls_results,
modimpls_evaluations,
self.modimpls_evaluations_complete,
) = moy_ue.notes_sem_load_cube(self.formsemestre) ) = moy_ue.notes_sem_load_cube(self.formsemestre)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
@ -74,16 +70,16 @@ class BulletinBUT(ResultatsSemestreBUT):
etud_idx = self.etud_index[etud.id] etud_idx = self.etud_index[etud.id]
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = self.sem_cube[etud_idx] # module x UE etud_moy_module = self.sem_cube[etud_idx] # module x UE
for mi in modimpls: for modimpl in modimpls:
coef = self.modimpl_coefs_df[mi.id][ue.id] coef = self.modimpl_coefs_df[modimpl.id][ue.id]
if coef > 0: if coef > 0:
d[mi.module.code] = { d[modimpl.module.code] = {
"id": mi.id, "id": modimpl.id,
"coef": coef, "coef": coef,
"moyenne": fmt_note( "moyenne": fmt_note(
etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][ etud_moy_module[
ue_idx self.modimpl_coefs_df.columns.get_loc(modimpl.id)
] ][ue_idx]
), ),
} }
return d return d
@ -117,7 +113,7 @@ class BulletinBUT(ResultatsSemestreBUT):
avec évaluations de chacun.""" avec évaluations de chacun."""
d = {} d = {}
# etud_idx = self.etud_index[etud.id] # etud_idx = self.etud_index[etud.id]
for mi in modimpls: for modimpl in modimpls:
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id) # mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
# # moyennes indicatives (moyennes de moyennes d'UE) # # moyennes indicatives (moyennes de moyennes d'UE)
# try: # try:
@ -131,14 +127,15 @@ class BulletinBUT(ResultatsSemestreBUT):
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx]) # moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
# except RuntimeWarning: # all nans in np.nanmean # except RuntimeWarning: # all nans in np.nanmean
# pass # pass
d[mi.module.code] = { modimpl_results = self.modimpls_results[modimpl.id]
"id": mi.id, d[modimpl.module.code] = {
"titre": mi.module.titre, "id": modimpl.id,
"code_apogee": mi.module.code_apogee, "titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee,
"url": url_for( "url": url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=mi.id, moduleimpl_id=modimpl.id,
), ),
"moyenne": { "moyenne": {
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan) # # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
@ -149,16 +146,17 @@ class BulletinBUT(ResultatsSemestreBUT):
}, },
"evaluations": [ "evaluations": [
self.etud_eval_results(etud, e) self.etud_eval_results(etud, e)
for eidx, e in enumerate(mi.evaluations) for e in modimpl.evaluations
if e.visibulletin if e.visibulletin
and self.modimpls_evaluations_complete[mi.id][eidx] and modimpl_results.evaluations_etat[e.id].is_complete
], ],
} }
return d return d
def etud_eval_results(self, etud, e) -> dict: def etud_eval_results(self, etud, e) -> dict:
"dict resultats d'un étudiant à une évaluation" "dict resultats d'un étudiant à une évaluation"
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna() notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
d = { d = {
"id": e.id, "id": e.id,
@ -170,7 +168,7 @@ class BulletinBUT(ResultatsSemestreBUT):
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids}, "poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
"note": { "note": {
"value": fmt_note( "value": fmt_note(
self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id], eval_notes[etud.id],
note_max=e.note_max, note_max=e.note_max,
), ),
"min": fmt_note(notes_ok.min()), "min": fmt_note(notes_ok.min()),
@ -212,8 +210,8 @@ class BulletinBUT(ResultatsSemestreBUT):
"numero": formsemestre.semestre_id, "numero": formsemestre.semestre_id,
"groupes": [], # XXX TODO "groupes": [], # XXX TODO
"absences": { # XXX TODO "absences": { # XXX TODO
"injustifie": 1, "injustifie": -1,
"total": 33, "total": -1,
}, },
} }
semestre_infos.update( semestre_infos.update(

View File

@ -108,8 +108,8 @@ def bulletin_but_xml_compat(
code_ine=etud.code_ine or "", code_ine=etud.code_ine or "",
nom=scu.quote_xml_attr(etud.nom), nom=scu.quote_xml_attr(etud.nom),
prenom=scu.quote_xml_attr(etud.prenom), prenom=scu.quote_xml_attr(etud.prenom),
civilite=scu.quote_xml_attr(etud.civilite_str()), civilite=scu.quote_xml_attr(etud.civilite_str),
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat sexe=scu.quote_xml_attr(etud.civilite_str), # compat
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)), photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
email=scu.quote_xml_attr(etud.get_first_email() or ""), email=scu.quote_xml_attr(etud.get_first_email() or ""),
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""), emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
@ -216,9 +216,9 @@ def bulletin_but_xml_compat(
Element( Element(
"note", "note",
value=scu.fmt_note( value=scu.fmt_note(
results.modimpls_evals_notes[e.moduleimpl_id][ results.modimpls_results[
e.id e.moduleimpl_id
][etud.id] ].evals_notes[e.id][etud.id]
), ),
) )
) )

37
app/comp/aux.py Normal file
View File

@ -0,0 +1,37 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
import numpy as np
"""Quelques classes auxiliaires pour les calculs des notes
"""
class StatsMoyenne:
"""Une moyenne d'un ensemble étudiants sur quelque chose
(moyenne générale d'un semestre, d'un module, d'un groupe...)
et les statistiques associées: min, max, moy, effectif
"""
def __init__(self, vals):
"""Calcul les statistiques.
Les valeurs NAN ou non numériques sont toujours enlevées.
"""
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
def to_dict(self):
"Tous les attributs dans un dict"
return {
"min": self.min,
"max": self.max,
"moy": self.moy,
"size": self.size,
"nb_vals": self.nb_vals,
}

View File

@ -43,7 +43,7 @@ class ModuleCoefsCache(sco_cache.ScoDocCache):
class EvaluationsPoidsCache(sco_cache.ScoDocCache): class EvaluationsPoidsCache(sco_cache.ScoDocCache):
"""Cache for poids evals """Cache for poids evals
Clé: moduleimpl_id Clé: moduleimpl_id
Valeur: DataFrame (df_load_evaluations_poids) Valeur: DataFrame (load_evaluations_poids)
""" """
prefix = "EPC" prefix = "EPC"

View File

@ -31,17 +31,217 @@ Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
moyenne générale d'une UE. moyenne générale d'une UE.
""" """
from dataclasses import dataclass
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from pandas.core.frame import DataFrame
from app import db from app import db
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids 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( @dataclass
class EvaluationEtat:
"""Classe pour stocker quelques infos sur les résultats d'une évaluation"""
evaluation_id: int
nb_attente: int
is_complete: bool
class ModuleImplResultsAPC:
"""Les notes des étudiants d'un moduleimpl.
Les poids des évals sont à part car on a a besoin sans les notes pour les tableaux
de bord.
Les attributs sont tous des objets simples cachables dans Redis;
les caches sont gérés par ResultatsSemestre.
"""
def __init__(self, moduleimpl: ModuleImpl):
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
"liste des étudiants inscrits au SEMESTRE"
self.nb_inscrits_module = None
"nombre d'inscrits (non DEM) au module"
self.evaluations_completes = []
"séquence de booléens, indiquant les évals à prendre en compte."
self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }"
#
self.evals_notes = None
"""DataFrame, colonnes: EVALS, Lignes: etudid
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
"""
self.etuds_moy_module = None
"""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 a des notes)
ne donnent pas de coef vers cette UE.
"""
self.load_notes()
def load_notes(self): # ré-écriture de df_load_modimpl_notes
"""Charge toutes les notes de toutes les évaluations du module.
Dataframe evals_notes
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, NON normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
Évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
Évaluation "attente" (prise en compte dans les calculs, mais il y
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
qui ont des notes ATT.
"""
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
self.etudids = self._etudids()
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
self.etudids
)
self.nb_inscrits_module = len(inscrits_module)
# dataFrame vide, index = tous les inscrits au SEMESTRE
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
# ou évaluaton déclarée "à prise en compte immédiate"
is_complete = (
len(set(eval_df.index).intersection(self.etudids))
== self.nb_inscrits_module
) or evaluation.publish_incomplete # immédiate
self.evaluations_completes.append(is_complete)
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge ne garde que les étudiants inscrits au module
# et met à NULL les notes non présentes
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.)
nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE)
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
)
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int"
)
self.evals_notes = evals_notes
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:
"""Charge les notes de l'évaluation
Resultat: dataframe, index: etudid ayant une note, valeur: note brute.
"""
eval_df = pd.read_sql_query(
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
FROM notes_notes n, notes_moduleimpl_inscription i
WHERE evaluation_id=%(evaluation_id)s
AND n.etudid = i.etudid
AND i.moduleimpl_id = %(moduleimpl_id)s
""",
db.engine,
params={
"evaluation_id": evaluation.id,
"moduleimpl_id": evaluation.moduleimpl.id,
},
index_col="etudid",
)
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
return eval_df
def _etudids(self):
"""L'index du dataframe est la liste des étudiants inscrits au semestre,
sans les démissionnaires.
"""
return [
e.etudid
for e in ModuleImpl.query.get(self.moduleimpl_id).formsemestre.get_inscrits(
include_dem=False
)
]
def compute_module_moy(
self,
evals_poids_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
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 a des notes)
ne donnent pas de coef vers cette UE.
"""
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
evals_coefs = (
np.array(
[e.coefficient for e in moduleimpl.evaluations],
dtype=float,
)
* self.evaluations_completes
).reshape(-1, 1)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
evals_notes = np.where(
self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# 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)
evals_poids_etuds = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Calcule la moyenne pondérée sur les notes disponibles:
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
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)
self.etuds_moy_module = pd.DataFrame(
etuds_moy_module,
index=self.evals_notes.index,
columns=evals_poids_df.columns,
)
return self.etuds_moy_module
def load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0 moduleimpl_id: int, default_poids=1.0
) -> tuple[pd.DataFrame, list]: ) -> 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
@ -55,23 +255,25 @@ def df_load_evaluations_poids(
ues = modimpl.formsemestre.query_ues(with_sport=False).all() ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ue_ids = [ue.id for ue in ues] ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations] evaluation_ids = [evaluation.id for evaluation in evaluations]
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for eval_poids in EvaluationUEPoids.query.join( for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id): ).filter_by(moduleimpl_id=moduleimpl_id):
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
if default_poids is not None: if default_poids is not None:
df.fillna(value=default_poids, inplace=True) evals_poids.fillna(value=default_poids, inplace=True)
return df, ues return evals_poids, ues
def check_moduleimpl_conformity( def moduleimpl_is_conforme(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> bool: ) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes """Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN. au PN.
Un module est dit *conforme* si et seulement si la somme des poids de ses Un module est dit *conforme* si et seulement si la somme des poids de ses
évaluations vers une UE de coefficient non nul est non nulle. évaluations vers une UE de coefficient non nul est non nulle.
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
""" """
nb_evals, nb_ues = evals_poids.shape nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0: if nb_evals == 0:
@ -79,160 +281,10 @@ def check_moduleimpl_conformity(
if nb_ues == 0: if nb_ues == 0:
return False # situation absurde (pas d'UE) return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues: if len(modules_coefficients) != nb_ues:
raise ValueError("check_moduleimpl_conformity: nb ue incoherent") raise ValueError("moduleimpl_is_conforme: nb ue incoherent")
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all( check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0) (modules_coefficients[moduleimpl.module_id].to_numpy() != 0)
== module_evals_poids == module_evals_poids
) )
return check return check
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
Résultat: (evals_notes, liste de évaluations du moduleimpl,
liste de booleens indiquant si l'évaluation est "complete")
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, non normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
L'évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
N'utilise pas de cache ScoDoc.
"""
# L'index du dataframe est la liste des étudiants inscrits au semestre,
# sans les démissionnaires
etudids = [
e.etudid
for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits(
include_dem=False
)
]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
# --- Calcul nombre d'inscrits pour détermnier si évaluation "complete":
if evaluations:
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {
ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions
}.intersection(etudids)
nb_inscrits_module = len(inscrits_module)
else:
nb_inscrits_module = 0
# empty df with all students:
evals_notes = pd.DataFrame(index=etudids, dtype=float)
evaluations_completes = []
for evaluation in evaluations:
eval_df = pd.read_sql_query(
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
FROM notes_notes n, notes_moduleimpl_inscription i
WHERE evaluation_id=%(evaluation_id)s
AND n.etudid = i.etudid
AND i.moduleimpl_id = %(moduleimpl_id)s
ORDER BY n.etudid
""",
db.engine,
params={
"evaluation_id": evaluation.id,
"moduleimpl_id": evaluation.moduleimpl.id,
},
index_col="etudid",
)
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
is_complete = (
len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module
) or evaluation.publish_incomplete
evaluations_completes.append(is_complete)
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge met à NULL les élements non présents
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int64"
)
return evals_notes, evaluations, evaluations_completes
def compute_module_moy(
evals_notes_df: pd.DataFrame,
evals_poids_df: pd.DataFrame,
evaluations: list,
evaluations_completes: list,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
- evaluations: séquence d'évaluations (utilisées pour le coef et
le barème)
- evaluations_completes: séquence de booléens indiquant les
évals à prendre en compte.
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)
ne donnent pas de coef vers cette UE.
"""
nb_etuds, nb_evals = evals_notes_df.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
evals_coefs = (
np.array(
[e.coefficient for e in evaluations],
dtype=float,
)
* evaluations_completes
).reshape(-1, 1)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
evals_notes = np.where(
evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0
) / [e.note_max / 20.0 for e in evaluations]
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# 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)
evals_poids_etuds = np.where(
np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Calcule la moyenne pondérée sur les notes disponibles:
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
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_df = pd.DataFrame(
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
)
return etuds_moy_module_df

View File

@ -34,7 +34,6 @@ from app import db
from app import models from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
@ -134,34 +133,21 @@ def notes_sem_load_cube(formsemestre):
Resultat: Resultat:
sem_cube : ndarray (etuds x modimpls x UEs) sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids } modimpls_evals_poids dict { modimpl.id : evals_poids }
modimpls_evals_notes dict { modimpl.id : evals_notes } modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
modimpls_evaluations dict { modimpl.id : liste des évaluations }
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
""" """
modimpls_results = {}
modimpls_evals_poids = {} modimpls_evals_poids = {}
modimpls_evals_notes = {}
modimpls_evaluations = {}
modimpls_evaluations_complete = {}
modimpls_notes = [] modimpls_notes = []
for modimpl in formsemestre.modimpls: for modimpl in formsemestre.modimpls:
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
modimpl.id evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
) etuds_moy_module = mod_results.compute_module_moy(evals_poids)
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id) modimpls_results[modimpl.id] = mod_results
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_evals_notes[modimpl.id] = evals_notes
modimpls_evaluations[modimpl.id] = evaluations
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
modimpls_notes.append(etuds_moy_module) modimpls_notes.append(etuds_moy_module)
return ( return (
notes_sem_assemble_cube(modimpls_notes), notes_sem_assemble_cube(modimpls_notes),
modimpls_evals_poids, modimpls_evals_poids,
modimpls_evals_notes, modimpls_results,
modimpls_evaluations,
modimpls_evaluations_complete,
) )

View File

@ -8,6 +8,8 @@ from collections import defaultdict
from functools import cached_property from functools import cached_property
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from app.comp.aux import StatsMoyenne
from app.models import ModuleImpl
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
@ -28,14 +30,20 @@ class ResultatsSemestre:
"modimpl_coefs_df", "modimpl_coefs_df",
"etud_moy_ue", "etud_moy_ue",
"modimpls_evals_poids", "modimpls_evals_poids",
"modimpls_evals_notes", "modimpls_results",
"etud_moy_gen", "etud_moy_gen",
"etud_moy_gen_ranks", "etud_moy_gen_ranks",
"modimpls_evaluations_complete",
) )
def __init__(self, formsemestre): def __init__(self, formsemestre):
self.formsemestre = formsemestre self.formsemestre = formsemestre
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis pas les sous-classes
# ResultatsSemestreBUT ou ResultatsSemestreStd
self.etud_moy_ue = {}
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
# TODO # TODO
def load_cached(self) -> bool: def load_cached(self) -> bool:
@ -49,7 +57,6 @@ class ResultatsSemestre:
def store(self): def store(self):
"Cache our data" "Cache our data"
"Cache our dataframes"
ResultatsSemestreCache.set( ResultatsSemestreCache.set(
self.formsemestre.id, self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs}, {attr: getattr(self, attr) for attr in self._cached_attrs},
@ -58,7 +65,7 @@ class ResultatsSemestre:
def compute(self): def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes" "Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes # voir ce qui est chargé / calculé ici et dans les sous-classes
TODO raise NotImplementedError()
@cached_property @cached_property
def etuds(self): def etuds(self):
@ -78,9 +85,22 @@ class ResultatsSemestre:
@cached_property @cached_property
def modimpls(self): def modimpls(self):
"Liste des modimpls du semestre (triée par numéro de module)" """Liste des modimpls du semestre
- triée par numéro de module en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.formsemestre.modimpls.all() modimpls = self.formsemestre.modimpls.all()
modimpls.sort(key=lambda m: m.module.numero) if self.is_apc:
modimpls.sort(key=lambda m: (m.module.numero, m.module.code))
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls return modimpls
@cached_property @cached_property
@ -96,32 +116,6 @@ class ResultatsSemestre:
return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE] return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE]
class StatsMoyenne:
"""Une moyenne d'un ensemble étudiants sur quelque chose
(moyenne générale d'un semestre, d'un module, d'un groupe...)
et les statistiques associées: min, max, moy, effectif
"""
def __init__(self, vals):
"""Calcul les statistiques.
Les valeurs NAN ou non numériques sont toujours enlevées.
"""
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
def to_dict(self):
return {
"min": self.min,
"max": self.max,
"moy": self.moy,
"size": self.size,
"nb_vals": self.nb_vals,
}
# Pour raccorder le code des anciens codes qui attendent une NoteTable # Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre): class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO """Implementation partielle de NotesTable WIP TODO
@ -158,11 +152,22 @@ class NotesTableCompat(ResultatsSemestre):
ues.append(d) ues.append(d)
return ues return ues
def get_modimpls(self): def get_modimpls_dict(self, ue_id=None):
return [m.to_dict() for m in self.results.modimpls] """Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation)
"""
if ue_id is None:
return [m.to_dict() for m in self.modimpls]
else:
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
def get_etud_moy_gen(self, etudid): def get_etud_moy_gen(self, etudid): # -> float | str
return self.results.etud_moy_gen[etudid] """Moyenne générale de cet etudiant dans ce semestre.
Prend en compte les UE capitalisées. (TODO)
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_moduleimpls_attente(self): def get_moduleimpls_attente(self):
return [] # XXX TODO return [] # XXX TODO

View File

@ -4,7 +4,9 @@
et données rattachées (adresses, annotations, ...) et données rattachées (adresses, annotations, ...)
""" """
from flask import g, url_for from functools import cached_property
from flask import abort, url_for
from flask import g, request
from app import db from app import db
from app import models from app import models
@ -53,14 +55,24 @@ class Identite(db.Model):
def __repr__(self): def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>" return f"<Etud {self.id} {self.nom} {self.prenom}>"
@classmethod
def from_request(cls, etudid=None, code_nip=None):
"""Etudiant à partir de l'etudid ou du code_nip, soit
passés en argument soit retrouvés directement dans la requête web.
Erreur 404 si inexistant.
"""
args = make_etud_args(etudid=etudid, code_nip=code_nip)
return Identite.query.filter_by(**args).first_or_404()
@property
def civilite_str(self): def civilite_str(self):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre, """returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personnes ne souhaitant pas d'affichage). personnes ne souhaitant pas d'affichage).
""" """
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
def nom_disp(self): def nom_disp(self) -> str:
"nom à afficher" "Nom à afficher"
if self.nom_usuel: if self.nom_usuel:
return ( return (
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
@ -68,8 +80,33 @@ class Identite(db.Model):
else: else:
return self.nom return self.nom
@cached_property
def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
"""
nom = self.nom_usuel or self.nom
prenom = self.prenom_str
if reverse:
fields = (nom, prenom)
else:
fields = (self.civilite_str, prenom, nom)
return " ".join([x for x in fields if x])
@property
def prenom_str(self):
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
if not self.prenom:
return ""
frags = self.prenom.split()
r = []
for frag in frags:
fields = frag.split("-")
r.append("-".join([x.lower().capitalize() for x in fields]))
return " ".join(r)
def get_first_email(self, field="email") -> str: def get_first_email(self, field="email") -> str:
"le mail associé à la première adrese de l'étudiant, ou None" "Le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None return self.adresses[0].email or None if self.adresses.count() > 0 else None
def to_dict_bul(self, include_urls=True): def to_dict_bul(self, include_urls=True):
@ -120,6 +157,42 @@ class Identite(db.Model):
return False return False
def make_etud_args(
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
) -> dict:
"""forme args dict pour requete recherche etudiant
On peut specifier etudid
ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine
(dans cet ordre).
Résultat: dict avec soit "etudid", soit "code_nip", soit "code_ine"
"""
args = None
if etudid:
args = {"etudid": etudid}
elif code_nip:
args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global)
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
if "etudid" in vals:
args = {"etudid": int(vals["etudid"])}
elif "code_nip" in vals:
args = {"code_nip": str(vals["code_nip"])}
elif "code_ine" in vals:
args = {"code_ine": str(vals["code_ine"])}
if not args:
if abort_404:
abort(404, "pas d'étudiant sélectionné")
elif raise_exc:
raise ValueError("make_etud_args: pas d'étudiant sélectionné !")
return args
class Adresse(db.Model): class Adresse(db.Model):
"""Adresse d'un étudiant """Adresse d'un étudiant
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule) (le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)

View File

@ -50,7 +50,7 @@ class ModuleImpl(db.Model):
if evaluations_poids is None: if evaluations_poids is None:
from app.comp import moy_mod from app.comp import moy_mod
evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id) evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id)
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids) df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
return evaluations_poids return evaluations_poids
@ -69,7 +69,7 @@ class ModuleImpl(db.Model):
return True return True
from app.comp import moy_mod from app.comp import moy_mod
return moy_mod.check_moduleimpl_conformity( return moy_mod.moduleimpl_is_conforme(
self, self,
self.get_evaluations_poids(), self.get_evaluations_poids(),
self.module.formation.get_module_coefs(self.module.semestre_id), self.module.formation.get_module_coefs(self.module.semestre_id),

View File

@ -296,7 +296,7 @@ class NotesTable:
for ue in self._ues: for ue in self._ues:
is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"] is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]
for modimpl in self.get_modimpls(): for modimpl in self.get_modimpls_dict():
val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if is_cap[modimpl["module"]["ue_id"]]: if is_cap[modimpl["module"]["ue_id"]]:
t.append("-c-") t.append("-c-")
@ -428,8 +428,8 @@ class NotesTable:
else: else:
return [ue for ue in self._ues if ue["type"] != UE_SPORT] return [ue for ue in self._ues if ue["type"] != UE_SPORT]
def get_modimpls(self, ue_id=None): def get_modimpls_dict(self, ue_id=None):
"liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
if ue_id is None: if ue_id is None:
r = self._modimpls r = self._modimpls
else: else:
@ -564,7 +564,7 @@ class NotesTable:
Si non inscrit, moy == 'NI' et sum_coefs==0 Si non inscrit, moy == 'NI' et sum_coefs==0
""" """
assert ue_id assert ue_id
modimpls = self.get_modimpls(ue_id) modimpls = self.get_modimpls_dict(ue_id)
nb_notes = 0 # dans cette UE nb_notes = 0 # dans cette UE
sum_notes = 0.0 sum_notes = 0.0
sum_coefs = 0.0 sum_coefs = 0.0
@ -921,7 +921,7 @@ class NotesTable:
return infos return infos
def get_etud_moy_gen(self, etudid): def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne generale de cet etudiant dans ce semestre. """Moyenne generale de cet etudiant dans ce semestre.
Prend en compte les UE capitalisées. Prend en compte les UE capitalisées.
Si pas de notes: 'NA' Si pas de notes: 'NA'

View File

@ -32,6 +32,8 @@ import datetime
from flask import url_for, g, request, abort from flask import url_for, g, request, abort
from app import log
from app.models import Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
@ -46,7 +48,6 @@ from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -71,8 +72,8 @@ def doSignaleAbsence(
etudid: etudiant concerné. Si non spécifié, cherche dans etudid: etudiant concerné. Si non spécifié, cherche dans
les paramètres de la requête courante. les paramètres de la requête courante.
""" """
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = Identite.from_request(etudid)
etudid = etud["etudid"]
if not moduleimpl_id: if not moduleimpl_id:
moduleimpl_id = None moduleimpl_id = None
description_abs = description description_abs = description
@ -82,7 +83,7 @@ def doSignaleAbsence(
for jour in dates: for jour in dates:
if demijournee == 2: if demijournee == 2:
sco_abs.add_absence( sco_abs.add_absence(
etudid, etud.id,
jour, jour,
False, False,
estjust, estjust,
@ -90,7 +91,7 @@ def doSignaleAbsence(
moduleimpl_id, moduleimpl_id,
) )
sco_abs.add_absence( sco_abs.add_absence(
etudid, etud.id,
jour, jour,
True, True,
estjust, estjust,
@ -100,7 +101,7 @@ def doSignaleAbsence(
nbadded += 2 nbadded += 2
else: else:
sco_abs.add_absence( sco_abs.add_absence(
etudid, etud.id,
jour, jour,
demijournee, demijournee,
estjust, estjust,
@ -113,27 +114,27 @@ def doSignaleAbsence(
J = "" J = ""
else: else:
J = "NON " J = "NON "
M = "" indication_module = ""
if moduleimpl_id and moduleimpl_id != "NULL": if moduleimpl_id and moduleimpl_id != "NULL":
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = mod["formsemestre_id"] formsemestre_id = mod["formsemestre_id"]
nt = sco_cache.NotesTableCache.get(formsemestre_id) nt = sco_cache.NotesTableCache.get(formsemestre_id)
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
for ue in ues: for ue in ues:
modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["moduleimpl_id"] == moduleimpl_id: if modimpl["moduleimpl_id"] == moduleimpl_id:
M = "dans le module %s" % modimpl["module"]["code"] indication_module = "dans le module %s" % modimpl["module"]["code"]
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Signalement d'une absence pour %(nomprenom)s" % etud, page_title=f"Signalement d'une absence pour {etud.nomprenom}",
), ),
"""<h2>Signalement d'absences</h2>""", """<h2>Signalement d'absences</h2>""",
] ]
if dates: if dates:
H.append( H.append(
"""<p>Ajout de %d absences <b>%sjustifiées</b> du %s au %s %s</p>""" """<p>Ajout de %d absences <b>%sjustifiées</b> du %s au %s %s</p>"""
% (nbadded, J, datedebut, datefin, M) % (nbadded, J, datedebut, datefin, indication_module)
) )
else: else:
H.append( H.append(
@ -142,11 +143,18 @@ def doSignaleAbsence(
) )
H.append( H.append(
"""<ul><li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Autre absence pour <b>%(nomprenom)s</b></a></li> f"""<ul>
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li> <li><a href="{url_for("absences.SignaleAbsenceEtud",
scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Autre absence pour <b>{etud.nomprenom}</b></a>
</li>
<li><a href="{url_for("absences.CalAbs",
scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Calendrier de ses absences</a>
</li>
</ul> </ul>
<hr>""" <hr>
% etud """
) )
H.append(sco_find_etud.form_search_etud()) H.append(sco_find_etud.form_search_etud())
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -200,7 +208,7 @@ def SignaleAbsenceEtud(): # etudid implied
menu_module += """<option value="" selected>(Module)</option>""" menu_module += """<option value="" selected>(Module)</option>"""
for ue in ues: for ue in ues:
modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
for modimpl in modimpls: for modimpl in modimpls:
menu_module += ( menu_module += (
"""<option value="%(modimpl_id)s">%(modname)s</option>\n""" """<option value="%(modimpl_id)s">%(modname)s</option>\n"""

View File

@ -456,7 +456,7 @@ class ApoEtud(dict):
return VOID_APO_RES return VOID_APO_RES
# Elements Modules # Elements Modules
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
module_code_found = False module_code_found = False
for modimpl in modimpls: for modimpl in modimpls:
if code in modimpl["module"]["code_apogee"].split(","): if code in modimpl["module"]["code_apogee"].split(","):
@ -978,7 +978,7 @@ class ApoData(object):
s.add(code) s.add(code)
continue continue
# associé à un module: # associé à un module:
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
for modimpl in modimpls: for modimpl in modimpls:
if code in modimpl["module"]["code_apogee"].split(","): if code in modimpl["module"]["code_apogee"].split(","):
s.add(code) s.add(code)

View File

@ -219,7 +219,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
# --- Notes # --- Notes
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
moy_gen = nt.get_etud_moy_gen(etudid) moy_gen = nt.get_etud_moy_gen(etudid)
I["nb_inscrits"] = len(nt.etud_moy_gen_ranks) I["nb_inscrits"] = len(nt.etud_moy_gen_ranks)
I["moy_gen"] = scu.fmt_note(moy_gen) I["moy_gen"] = scu.fmt_note(moy_gen)
@ -352,7 +352,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
etudid, etudid,
formsemestre_id, formsemestre_id,
ue_status["capitalized_ue_id"], ue_status["capitalized_ue_id"],
nt_cap.get_modimpls(), nt_cap.get_modimpls_dict(),
nt_cap, nt_cap,
version, version,
) )

View File

@ -154,7 +154,7 @@ def formsemestre_bulletinetud_published_dict(
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid) partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks) nbetuds = len(nt.etud_moy_gen_ranks)
mg = scu.fmt_note(nt.get_etud_moy_gen(etudid)) mg = scu.fmt_note(nt.get_etud_moy_gen(etudid))
if ( if (

View File

@ -152,7 +152,7 @@ def make_xml_formsemestre_bulletinetud(
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks) nbetuds = len(nt.etud_moy_gen_ranks)
mg = scu.fmt_note(nt.get_etud_moy_gen(etudid)) mg = scu.fmt_note(nt.get_etud_moy_gen(etudid))
if ( if (

View File

@ -38,7 +38,7 @@ from flask_mail import Message
from app import email from app import email
from app import log from app import log
from app.models.etudiants import make_etud_args
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.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
@ -87,6 +87,8 @@ def force_uppercase(s):
def format_nomprenom(etud, reverse=False): def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont" """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité. Si reverse, "Dupont Pierre", sans civilité.
DEPRECATED: utiliser Identite.nomprenom
""" """
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"] nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"]) prenom = format_prenom(etud["prenom"])
@ -99,7 +101,9 @@ def format_nomprenom(etud, reverse=False):
def format_prenom(s): def format_prenom(s):
"Formatte prenom etudiant pour affichage" """Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str
"""
if not s: if not s:
return "" return ""
frags = s.split() frags = s.split()
@ -590,35 +594,6 @@ etudident_edit = _etudidentEditor.edit
etudident_create = _etudidentEditor.create etudident_create = _etudidentEditor.create
def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True):
"""forme args dict pour requete recherche etudiant
On peut specifier etudid
ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine
(dans cet ordre).
"""
args = None
if etudid:
args = {"etudid": etudid}
elif code_nip:
args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global)
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
if "etudid" in vals:
args = {"etudid": int(vals["etudid"])}
elif "code_nip" in vals:
args = {"code_nip": str(vals["code_nip"])}
elif "code_ine" in vals:
args = {"code_ine": str(vals["code_ine"])}
if not args and raise_exc:
raise ValueError("getEtudInfo: no parameter !")
return args
def log_unknown_etud(): def log_unknown_etud():
"""Log request: cas ou getEtudInfo n'a pas ramene de resultat""" """Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
etud_args = make_etud_args(raise_exc=False) etud_args = make_etud_args(raise_exc=False)

View File

@ -240,7 +240,7 @@ def _make_table_notes(
if is_apc: if is_apc:
modimpl = ModuleImpl.query.get(moduleimpl_id) modimpl = ModuleImpl.query.get(moduleimpl_id)
is_conforme = modimpl.check_apc_conformity() is_conforme = modimpl.check_apc_conformity()
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id) evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
if not ues: if not ues:
is_apc = False is_apc = False
else: else:

View File

@ -36,6 +36,7 @@ from flask_login import current_user
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.models.etudiants import make_etud_args
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud from app.scodoc import sco_archives_etud
@ -156,7 +157,7 @@ def ficheEtud(etudid=None):
# la sidebar est differente s'il y a ou pas un etudid # la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar() # voir html_sidebar.sidebar()
g.etudid = etudid g.etudid = etudid
args = sco_etud.make_etud_args(etudid=etudid) args = make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args) etuds = sco_etud.etudident_list(cnx, args)
if not etuds: if not etuds:
log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args)) log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args))

View File

@ -75,7 +75,7 @@ def etud_get_poursuite_info(sem, etud):
] ]
# Moyennes et rang des modules # Moyennes et rang des modules
modimpls = nt.get_modimpls() # recupération des modules modimpls = nt.get_modimpls_dict() # recupération des modules
modules = [] modules = []
rangs = [] rangs = []
for ue in ues: # on parcourt chaque UE for ue in ues: # on parcourt chaque UE

View File

@ -302,10 +302,8 @@ def make_formsemestre_recapcomplet(
sem = sco_formsemestre.do_formsemestre_list( sem = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
)[0] )[0]
nt = sco_cache.NotesTableCache.get( nt = sco_cache.NotesTableCache.get(formsemestre_id)
formsemestre_id modimpls = nt.get_modimpls_dict()
) # > get_modimpls, get_ues_stat_dict, get_table_moyennes_triees, get_etud_decision_sem, get_etud_etat, get_etud_rang, get_nom_short, get_mod_stats, nt.moy_moy, get_etud_decision_sem,
modimpls = nt.get_modimpls()
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
# #
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():

View File

@ -270,7 +270,7 @@ def get_etud_tagged_modules(etudid, tagname):
R = [] R = []
for sem in etud["sems"]: for sem in etud["sems"]:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
modimpls = nt.get_modimpls() modimpls = nt.get_modimpls_dict()
for modimpl in modimpls: for modimpl in modimpls:
tags = module_tag_list(module_id=modimpl["module_id"]) tags = module_tag_list(module_id=modimpl["module_id"])
if tagname in tags: if tagname in tags:

View File

@ -3,11 +3,9 @@
<div class="sidebar"> <div class="sidebar">
{# sidebar_common #} {# sidebar_common #}
<a class="scodoc_title" href="{{ <a class="scodoc_title" href="{{
url_for("scodoc.index", scodoc_dept=g.scodoc_dept) url_for("scodoc.index", scodoc_dept=g.scodoc_dept) }}">ScoDoc 9.2a</a>
}}">ScoDoc 9.1</a>
<div id="authuser"><a id="authuserlink" href="{{ <div id="authuser"><a id="authuserlink" href="{{
url_for("users.user_info_page", url_for("users.user_info_page", scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a> }}">{{current_user.user_name}}</a>
<br /><a id="deconnectlink" href="{{url_for("auth.logout")}}">déconnexion</a> <br /><a id="deconnectlink" href="{{url_for("auth.logout")}}">déconnexion</a>
</div> </div>
@ -19,13 +17,13 @@
<a href="{{ sco.prefs[" DeptIntranetURL"] }}" class="sidebar"> <a href="{{ sco.prefs[" DeptIntranetURL"] }}" class="sidebar">
{{ sco.prefs["DeptIntranetTitle"] }}</a> {{ sco.prefs["DeptIntranetTitle"] }}</a>
{% endif %} {% endif %}
<br /> <br>
{% endblock %} {% endblock %}
<h2 class="insidebar">Scolarité</h2> <h2 class="insidebar">Scolarité</h2>
<a href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br/> <a href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for("notes.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br/> <a href="{{url_for("notes.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for("absences.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br/> <a href="{{url_for("absences.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br>
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin) {% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
or current_user.has_permission(sco.Permission.ScoUsersView) or current_user.has_permission(sco.Permission.ScoUsersView)
@ -34,12 +32,11 @@
{% endif %} {% endif %}
{% if current_user.has_permission(sco.Permission.ScoChangePreferences) %} {% if current_user.has_permission(sco.Permission.ScoChangePreferences) %}
<a href="{{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}}" <a href="{{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Paramétrage</a> <br>
class="sidebar">Paramétrage</a> <br/>
{% endif %} {% endif %}
{# /sidebar_common #} {# /sidebar_common #}
<div class="box-chercheetud">Chercher étudiant:<br/> <div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud" <form method="get" id="form-chercheetud"
action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}"> action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}">
<div> <div>
@ -51,9 +48,8 @@
<div class="etud-insidebar"> <div class="etud-insidebar">
{% if sco.etud %} {% if sco.etud %}
<h2 id="insidebar-etud"><a href="{{url_for( <h2 id="insidebar-etud"><a href="{{url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar">
)}}" class="sidebar"> <span class="fontred">{{sco.etud.nomprenom}}</span></a>
<span class="fontred">{{sco.etud.civilite_str()}} {{sco.etud.nom_disp()}}</span></a>
</h2> </h2>
<b>Absences</b> <b>Absences</b>
{% if sco.etud_cur_sem %} {% if sco.etud_cur_sem %}

View File

@ -431,7 +431,7 @@ def SignaleAbsenceGrHebdo(
modimpls_list = [] modimpls_list = []
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
for ue in ues: for ue in ues:
modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"]) modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"])
menu_module = "" menu_module = ""
for modimpl in modimpls_list: for modimpl in modimpls_list:
@ -599,7 +599,7 @@ def SignaleAbsenceGrSemestre(
modimpls_list = [] modimpls_list = []
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
for ue in ues: for ue in ues:
modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"]) modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"])
menu_module = "" menu_module = ""
for modimpl in modimpls_list: for modimpl in modimpls_list:

View File

@ -41,6 +41,7 @@ from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField from wtforms import SubmitField
from app import log
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
scodoc7func, scodoc7func,
@ -50,12 +51,12 @@ from app.decorators import (
login_required, login_required,
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.etudiants import make_etud_args
from app.views import scolar_bp as bp from app.views import scolar_bp as bp
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.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
@ -455,7 +456,7 @@ def etud_info(etudid=None, format="xml"):
if not format in ("xml", "json"): if not format in ("xml", "json"):
raise ScoValueError("format demandé non supporté par cette fonction.") raise ScoValueError("format demandé non supporté par cette fonction.")
t0 = time.time() t0 = time.time()
args = sco_etud.make_etud_args(etudid=etudid) args = make_etud_args(etudid=etudid)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
etuds = sco_etud.etudident_list(cnx, args) etuds = sco_etud.etudident_list(cnx, args)
if not etuds: if not etuds:

View File

@ -19,3 +19,5 @@ ignored-classes=Permission,
# and thus existing member attributes cannot be deduced by static analysis). It # and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching. # supports qualified module names, as well as Unix pattern matching.
ignored-modules=entreprises ignored-modules=entreprises
good-names=d,e,f,i,j,k,t,u,v,x,y,z,H,F,ue

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.1.16" SCOVERSION = "9.2.0a"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -4,6 +4,8 @@ et calcul moyennes modules
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from tests.unit import setup from tests.unit import setup
from app import db from app import db
@ -135,70 +137,72 @@ def test_module_conformity(test_client):
) )
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, ues = moy_mod.df_load_evaluations_poids(evaluation.moduleimpl_id) evals_poids, ues = moy_mod.load_evaluations_poids(evaluation.moduleimpl_id)
assert isinstance(evals_poids, pd.DataFrame) assert isinstance(evals_poids, pd.DataFrame)
assert len(ues) == nb_ues 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.moduleimpl_is_conforme(
evaluation.moduleimpl, evals_poids, modules_coefficients evaluation.moduleimpl, evals_poids, modules_coefficients
) )
def test_module_moy_elem(test_client): # En ScoDoc 9.2 test ne peut plus exister car compute_module_moy
"""Vérification calcul moyenne d'un module # est maintenant incorporé dans la classe ModuleImplResultsAPC
(notes entrées dans un DataFrame sans passer par ScoDoc) # def test_module_moy_elem(test_client):
""" # """Vérification calcul moyenne d'un module
# Création de deux évaluations: # (notes entrées dans un DataFrame sans passer par ScoDoc)
e1 = Evaluation(note_max=20.0, coefficient=1.0) # """
e2 = Evaluation(note_max=20.0, coefficient=1.0) # # Création de deux évaluations:
db.session.add(e1) # e1 = Evaluation(note_max=20.0, coefficient=1.0)
db.session.add(e2) # e2 = Evaluation(note_max=20.0, coefficient=1.0)
db.session.commit() # db.session.add(e1)
# Repris du notebook CalculNotesBUT.ipynb # db.session.add(e2)
data = [ # Les notes de chaque étudiant dans les 2 evals: # db.session.flush()
{ # # Repris du notebook CalculNotesBUT.ipynb
e1.id: 11.0, # data = [ # Les notes de chaque étudiant dans les 2 evals:
e2.id: 16.0, # {
}, # e1.id: 11.0,
{ # e2.id: 16.0,
e1.id: None, # une absence # },
e2.id: 17.0, # {
}, # e1.id: None, # une absence
{ # e2.id: 17.0,
e1.id: 13.0, # },
e2.id: NOTES_NEUTRALISE, # une abs EXC # {
}, # e1.id: 13.0,
{ # e2.id: NOTES_NEUTRALISE, # une abs EXC
e1.id: 14.0, # },
e2.id: 19.0, # {
}, # e1.id: 14.0,
{ # e2.id: 19.0,
e1.id: NOTES_ATTENTE, # une ATT (traitée comme EXC) # },
e2.id: None, # et une ABS # {
}, # e1.id: NOTES_ATTENTE, # une ATT (traitée comme EXC)
] # e2.id: None, # et une ABS
evals_notes_df = pd.DataFrame( # },
data, index=["etud1", "etud2", "etud3", "etud4", "etud5"] # ]
) # evals_notes_df = pd.DataFrame(
# Poids des évaluations (1 ligne / évaluation) # data, index=["etud1", "etud2", "etud3", "etud4", "etud5"]
data = [ # )
{"UE1": 1, "UE2": 0, "UE3": 0}, # # Poids des évaluations (1 ligne / évaluation)
{"UE1": 2, "UE2": 5, "UE3": 0}, # data = [
] # {"UE1": 1, "UE2": 0, "UE3": 0},
evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float) # {"UE1": 2, "UE2": 5, "UE3": 0},
evaluations = [e1, e2] # ]
etuds_moy_module_df = moy_mod.compute_module_moy( # evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float)
evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True] # evaluations = [e1, e2]
) # etuds_moy_module_df = moy_mod.compute_module_moy(
NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN) # evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True]
r = etuds_moy_module_df.fillna(NAN) # )
assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN) # NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN) # r = etuds_moy_module_df.fillna(NAN)
assert tuple(r.loc["etud3"]) == (13, NAN, NAN) # assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN) # assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
assert tuple(r.loc["etud5"]) == (0.0, 0.0, NAN) # assert tuple(r.loc["etud3"]) == (13, NAN, NAN)
# note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls # 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): def test_module_moy(test_client):
@ -237,7 +241,7 @@ def test_module_moy(test_client):
nb_evals = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count() nb_evals = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count()
assert nb_evals == 2 assert nb_evals == 2
nb_ues = 3 nb_ues = 3
modimpl = ModuleImpl.query.get(moduleimpl_id)
# --- Change les notes et recalcule les moyennes du module # --- Change les notes et recalcule les moyennes du module
# (rappel: on a deux évaluations: evaluation1, evaluation2, et un seul étudiant) # (rappel: on a deux évaluations: evaluation1, evaluation2, et un seul étudiant)
def change_notes(n1, n2): def change_notes(n1, n2):
@ -245,17 +249,14 @@ def test_module_moy(test_client):
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
# Calcul de la moyenne du module # Calcul de la moyenne du module
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id) evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
assert evals_poids.shape == (nb_evals, nb_ues) assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
moduleimpl_id mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
) evals_notes = mod_results.evals_notes
assert evals_notes[evaluations[0].id].dtype == np.float64 assert evals_notes[evaluation1.id].dtype == np.float64
assert evaluation1.id == evaluations[0].id
assert evaluation2.id == evaluations[1].id etuds_moy_module = mod_results.compute_module_moy(evals_poids)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
return etuds_moy_module return etuds_moy_module
# --- Notes ordinaires: # --- Notes ordinaires:

View File

@ -69,7 +69,7 @@ def test_ue_moy(test_client):
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
# Recalcul des moyennes # Recalcul des moyennes
sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
etuds = formsemestre.etuds.all() etuds = formsemestre.etuds.all()
etud_moy_ue = moy_ue.compute_ue_moys( etud_moy_ue = moy_ue.compute_ue_moys(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
@ -112,7 +112,7 @@ def test_ue_moy(test_client):
exception_raised = True exception_raised = True
assert exception_raised assert exception_raised
# Recalcule les notes: # Recalcule les notes:
sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
etuds = formsemestre.etuds.all() etuds = formsemestre.etuds.all()
etud_moy_ue = moy_ue.compute_ue_moys( etud_moy_ue = moy_ue.compute_ue_moys(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df