1
0
forked from ScoDoc/ScoDoc

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

View File

@ -108,8 +108,8 @@ def bulletin_but_xml_compat(
code_ine=etud.code_ine or "",
nom=scu.quote_xml_attr(etud.nom),
prenom=scu.quote_xml_attr(etud.prenom),
civilite=scu.quote_xml_attr(etud.civilite_str()),
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat
civilite=scu.quote_xml_attr(etud.civilite_str),
sexe=scu.quote_xml_attr(etud.civilite_str), # compat
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
email=scu.quote_xml_attr(etud.get_first_email() or ""),
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
@ -216,9 +216,9 @@ def bulletin_but_xml_compat(
Element(
"note",
value=scu.fmt_note(
results.modimpls_evals_notes[e.moduleimpl_id][
e.id
][etud.id]
results.modimpls_results[
e.moduleimpl_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):
"""Cache for poids evals
Clé: moduleimpl_id
Valeur: DataFrame (df_load_evaluations_poids)
Valeur: DataFrame (load_evaluations_poids)
"""
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
moyenne générale d'une UE.
"""
from dataclasses import dataclass
import numpy as np
import pandas as pd
from pandas.core.frame import DataFrame
from app import db
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
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
) -> tuple[pd.DataFrame, list]:
"""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()
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for eval_poids in EvaluationUEPoids.query.join(
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).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:
df.fillna(value=default_poids, inplace=True)
return df, ues
evals_poids.fillna(value=default_poids, inplace=True)
return evals_poids, ues
def check_moduleimpl_conformity(
def moduleimpl_is_conforme(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN.
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.
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:
@ -79,160 +281,10 @@ def check_moduleimpl_conformity(
if nb_ues == 0:
return False # situation absurde (pas d'UE)
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
check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
(modules_coefficients[moduleimpl.module_id].to_numpy() != 0)
== module_evals_poids
)
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.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours
@ -134,34 +133,21 @@ def notes_sem_load_cube(formsemestre):
Resultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
modimpls_evals_notes dict { modimpl.id : evals_notes }
modimpls_evaluations dict { modimpl.id : liste des évaluations }
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
"""
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_evals_notes = {}
modimpls_evaluations = {}
modimpls_evaluations_complete = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
modimpl.id
)
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
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
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results
modimpls_notes.append(etuds_moy_module)
return (
notes_sem_assemble_cube(modimpls_notes),
modimpls_evals_poids,
modimpls_evals_notes,
modimpls_evaluations,
modimpls_evaluations_complete,
modimpls_results,
)

View File

@ -8,6 +8,8 @@ from collections import defaultdict
from functools import cached_property
import numpy as np
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.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -28,14 +30,20 @@ class ResultatsSemestre:
"modimpl_coefs_df",
"etud_moy_ue",
"modimpls_evals_poids",
"modimpls_evals_notes",
"modimpls_results",
"etud_moy_gen",
"etud_moy_gen_ranks",
"modimpls_evaluations_complete",
)
def __init__(self, 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
def load_cached(self) -> bool:
@ -49,7 +57,6 @@ class ResultatsSemestre:
def store(self):
"Cache our data"
"Cache our dataframes"
ResultatsSemestreCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
@ -58,7 +65,7 @@ class ResultatsSemestre:
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes
TODO
raise NotImplementedError()
@cached_property
def etuds(self):
@ -78,9 +85,22 @@ class ResultatsSemestre:
@cached_property
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.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
@cached_property
@ -96,32 +116,6 @@ class ResultatsSemestre:
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
class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO
@ -158,11 +152,22 @@ class NotesTableCompat(ResultatsSemestre):
ues.append(d)
return ues
def get_modimpls(self):
return [m.to_dict() for m in self.results.modimpls]
def get_modimpls_dict(self, ue_id=None):
"""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):
return self.results.etud_moy_gen[etudid]
def get_etud_moy_gen(self, etudid): # -> float | str
"""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):
return [] # XXX TODO

View File

@ -4,7 +4,9 @@
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 models
@ -53,14 +55,24 @@ class Identite(db.Model):
def __repr__(self):
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):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personnes ne souhaitant pas d'affichage).
"""
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
def nom_disp(self):
"nom à afficher"
def nom_disp(self) -> str:
"Nom à afficher"
if self.nom_usuel:
return (
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
@ -68,8 +80,33 @@ class Identite(db.Model):
else:
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:
"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
def to_dict_bul(self, include_urls=True):
@ -120,6 +157,42 @@ class Identite(db.Model):
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):
"""Adresse d'un étudiant
(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:
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)
return evaluations_poids
@ -69,7 +69,7 @@ class ModuleImpl(db.Model):
return True
from app.comp import moy_mod
return moy_mod.check_moduleimpl_conformity(
return moy_mod.moduleimpl_is_conforme(
self,
self.get_evaluations_poids(),
self.module.formation.get_module_coefs(self.module.semestre_id),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ from flask_mail import Message
from app import email
from app import log
from app.models.etudiants import make_etud_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
@ -87,6 +87,8 @@ def force_uppercase(s):
def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
DEPRECATED: utiliser Identite.nomprenom
"""
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"])
@ -99,7 +101,9 @@ def format_nomprenom(etud, reverse=False):
def format_prenom(s):
"Formatte prenom etudiant pour affichage"
"""Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str
"""
if not s:
return ""
frags = s.split()
@ -590,35 +594,6 @@ etudident_edit = _etudidentEditor.edit
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():
"""Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
etud_args = make_etud_args(raise_exc=False)

View File

@ -240,7 +240,7 @@ def _make_table_notes(
if is_apc:
modimpl = ModuleImpl.query.get(moduleimpl_id)
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:
is_apc = False
else:

View File

@ -36,6 +36,7 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.models.etudiants import make_etud_args
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
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
# voir html_sidebar.sidebar()
g.etudid = etudid
args = sco_etud.make_etud_args(etudid=etudid)
args = make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args)
if not etuds:
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
modimpls = nt.get_modimpls() # recupération des modules
modimpls = nt.get_modimpls_dict() # recupération des modules
modules = []
rangs = []
for ue in ues: # on parcourt chaque UE

View File

@ -302,10 +302,8 @@ def make_formsemestre_recapcomplet(
sem = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id}
)[0]
nt = sco_cache.NotesTableCache.get(
formsemestre_id
) # > 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()
nt = sco_cache.NotesTableCache.get(formsemestre_id)
modimpls = nt.get_modimpls_dict()
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
#
if formsemestre.formation.is_apc():

View File

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

View File

@ -3,96 +3,92 @@
<div class="sidebar">
{# sidebar_common #}
<a class="scodoc_title" href="{{
url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
}}">ScoDoc 9.1</a>
url_for("scodoc.index", scodoc_dept=g.scodoc_dept) }}">ScoDoc 9.2a</a>
<div id="authuser"><a id="authuserlink" href="{{
url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a>
<br/><a id="deconnectlink" href="{{url_for("auth.logout")}}">déconnexion</a>
url_for("users.user_info_page", scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a>
<br /><a id="deconnectlink" href="{{url_for("auth.logout")}}">déconnexion</a>
</div>
{% block sidebar_dept %}
<h2 class="insidebar">Dépt. {{ sco.prefs["DeptName"] }}</h2>
<a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br />
{% if sco.prefs["DeptIntranetURL"] %}
<a href="{{ sco.prefs["DeptIntranetURL"] }}" class="sidebar">
<a href="{{ sco.prefs[" DeptIntranetURL"] }}" class="sidebar">
{{ sco.prefs["DeptIntranetTitle"] }}</a>
{% endif %}
<br />
<br>
{% endblock %}
<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("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("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("absences.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br>
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
or current_user.has_permission(sco.Permission.ScoUsersView)
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
or current_user.has_permission(sco.Permission.ScoUsersView)
%}
<a href="{{url_for("users.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Utilisateurs</a> <br/>
<a href="{{url_for("users.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Utilisateurs</a> <br />
{% endif %}
{% if current_user.has_permission(sco.Permission.ScoChangePreferences) %}
<a href="{{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}}"
class="sidebar">Paramétrage</a> <br/>
<a href="{{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Paramétrage</a> <br>
{% endif %}
{# /sidebar_common #}
<div class="box-chercheetud">Chercher étudiant:<br/>
<form method="get" id="form-chercheetud"
action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}">
<div>
<input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false"/>
</div>
</form>
<div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud"
action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}">
<div>
<input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false" />
</div>
</form>
</div>
<div class="etud-insidebar">
{% if sco.etud %}
<h2 id="insidebar-etud"><a href="{{url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id
)}}" class="sidebar">
<span class="fontred">{{sco.etud.civilite_str()}} {{sco.etud.nom_disp()}}</span></a>
</h2>
<b>Absences</b>
{% if sco.etud %}
<h2 id="insidebar-etud"><a href="{{url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar">
<span class="fontred">{{sco.etud.nomprenom}}</span></a>
</h2>
<b>Absences</b>
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
au {{ sco.etud_cur_sem['date_fin'] }}">(1/2 j.)
<br/>{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span>
<br />{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span>
{% endif %}
<ul>
{% if current_user.has_permission(sco.Permission.ScoAbsChange) %}
<li><a href="{{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept,
<ul>
{% if current_user.has_permission(sco.Permission.ScoAbsChange) %}
<li><a href="{{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Ajouter</a></li>
<li><a href="{{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept,
<li><a href="{{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Justifier</a></li>
<li><a href="{{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept,
<li><a href="{{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Supprimer</a></li>
{% if sco.prefs["handle_billets_abs"] %}
<li><a href="{{ url_for('absences.listeBilletsEtud', scodoc_dept=g.scodoc_dept,
{% if sco.prefs["handle_billets_abs"] %}
<li><a href="{{ url_for('absences.listeBilletsEtud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Billets</a></li>
{% endif %}
{% endif %}
<li><a href="{{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept,
{% endif %}
{% endif %}
<li><a href="{{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Calendrier</a></li>
<li><a href="{{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept,
<li><a href="{{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Liste</a></li>
</ul>
{% endif %}
</div> {# /etud-insidebar #}
</ul>
{% endif %}
</div> {# /etud-insidebar #}
{# LOGO #}
<div class="logo-insidebar">
<div class="sidebar-bottom"><a href="{{ url_for( 'scodoc.about',
scodoc_dept=g.scodoc_dept ) }}" class="sidebar">À propos</a>
<br/>
<a href="{{ sco.scu.SCO_USER_MANUAL }}" target="_blank" class="sidebar">Aide</a>
<br />
<a href="{{ sco.scu.SCO_USER_MANUAL }}" target="_blank" class="sidebar">Aide</a>
</div>
</div>
<div class="logo-logo">
<a href="{{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }}">
{{ sco.scu.icontag("scologo_img", no_size=True) | safe}}</a>
{{ sco.scu.icontag("scologo_img", no_size=True) | safe}}</a>
</div>
</div>
<!-- end of sidebar -->

View File

@ -431,7 +431,7 @@ def SignaleAbsenceGrHebdo(
modimpls_list = []
ues = nt.get_ues_stat_dict()
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 = ""
for modimpl in modimpls_list:
@ -599,7 +599,7 @@ def SignaleAbsenceGrSemestre(
modimpls_list = []
ues = nt.get_ues_stat_dict()
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 = ""
for modimpl in modimpls_list:

View File

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

View File

@ -19,3 +19,5 @@ ignored-classes=Permission,
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
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 -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.1.16"
SCOVERSION = "9.2.0a"
SCONAME = "ScoDoc"

View File

@ -4,6 +4,8 @@ et calcul moyennes modules
"""
import numpy as np
import pandas as pd
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from tests.unit import setup
from app import db
@ -135,70 +137,72 @@ def test_module_conformity(test_client):
)
assert isinstance(modules_coefficients, pd.DataFrame)
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 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(
assert not moy_mod.moduleimpl_is_conforme(
evaluation.moduleimpl, evals_poids, modules_coefficients
)
def test_module_moy_elem(test_client):
"""Vérification calcul moyenne d'un module
(notes entrées dans un DataFrame sans passer par ScoDoc)
"""
# Création de deux évaluations:
e1 = Evaluation(note_max=20.0, coefficient=1.0)
e2 = Evaluation(note_max=20.0, coefficient=1.0)
db.session.add(e1)
db.session.add(e2)
db.session.commit()
# Repris du notebook CalculNotesBUT.ipynb
data = [ # Les notes de chaque étudiant dans les 2 evals:
{
e1.id: 11.0,
e2.id: 16.0,
},
{
e1.id: None, # une absence
e2.id: 17.0,
},
{
e1.id: 13.0,
e2.id: NOTES_NEUTRALISE, # une abs EXC
},
{
e1.id: 14.0,
e2.id: 19.0,
},
{
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"]
)
# Poids des évaluations (1 ligne / évaluation)
data = [
{"UE1": 1, "UE2": 0, "UE3": 0},
{"UE1": 2, "UE2": 5, "UE3": 0},
]
evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float)
evaluations = [e1, e2]
etuds_moy_module_df = moy_mod.compute_module_moy(
evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True]
)
NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
r = etuds_moy_module_df.fillna(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
# En ScoDoc 9.2 test ne peut plus exister car compute_module_moy
# est maintenant incorporé dans la classe ModuleImplResultsAPC
# def test_module_moy_elem(test_client):
# """Vérification calcul moyenne d'un module
# (notes entrées dans un DataFrame sans passer par ScoDoc)
# """
# # Création de deux évaluations:
# e1 = Evaluation(note_max=20.0, coefficient=1.0)
# e2 = Evaluation(note_max=20.0, coefficient=1.0)
# db.session.add(e1)
# db.session.add(e2)
# db.session.flush()
# # Repris du notebook CalculNotesBUT.ipynb
# data = [ # Les notes de chaque étudiant dans les 2 evals:
# {
# e1.id: 11.0,
# e2.id: 16.0,
# },
# {
# e1.id: None, # une absence
# e2.id: 17.0,
# },
# {
# e1.id: 13.0,
# e2.id: NOTES_NEUTRALISE, # une abs EXC
# },
# {
# e1.id: 14.0,
# e2.id: 19.0,
# },
# {
# 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"]
# )
# # Poids des évaluations (1 ligne / évaluation)
# data = [
# {"UE1": 1, "UE2": 0, "UE3": 0},
# {"UE1": 2, "UE2": 5, "UE3": 0},
# ]
# evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float)
# evaluations = [e1, e2]
# etuds_moy_module_df = moy_mod.compute_module_moy(
# evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True]
# )
# NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
# r = etuds_moy_module_df.fillna(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):
@ -237,7 +241,7 @@ def test_module_moy(test_client):
nb_evals = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count()
assert nb_evals == 2
nb_ues = 3
modimpl = ModuleImpl.query.get(moduleimpl_id)
# --- Change les notes et recalcule les moyennes du module
# (rappel: on a deux évaluations: evaluation1, evaluation2, et un seul étudiant)
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, evaluation2.id, [(etudid, n2)])
# 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)
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
moduleimpl_id
)
assert evals_notes[evaluations[0].id].dtype == np.float64
assert evaluation1.id == evaluations[0].id
assert evaluation2.id == evaluations[1].id
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
evals_notes = mod_results.evals_notes
assert evals_notes[evaluation1.id].dtype == np.float64
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
return etuds_moy_module
# --- 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, evaluation2.id, [(etudid, n2)])
# 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()
etud_moy_ue = moy_ue.compute_ue_moys(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
@ -112,7 +112,7 @@ def test_ue_moy(test_client):
exception_raised = True
assert exception_raised
# 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()
etud_moy_ue = moy_ue.compute_ue_moys(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df