WIP: moyennes modules, evals, bulletins.

This commit is contained in:
Emmanuel Viennet 2021-12-14 23:03:59 +01:00
parent 4d6d7ad168
commit 25a441f7f2
9 changed files with 101 additions and 19 deletions

View File

@ -37,13 +37,14 @@ class ResultatsSemestreBUT:
"modimpls_evals_notes", "modimpls_evals_notes",
"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
self.ues = formsemestre.query_ues().all() self.ues = formsemestre.query_ues().all()
self.modimpls = formsemestre.modimpls.all() self.modimpls = formsemestre.modimpls.all()
self.etuds = self.formsemestre.etuds.all() self.etuds = self.formsemestre.get_inscrits(include_dem=False)
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)} self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
self.saes = [ self.saes = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
@ -77,7 +78,8 @@ class ResultatsSemestreBUT:
self.sem_cube, self.sem_cube,
self.modimpls_evals_poids, self.modimpls_evals_poids,
self.modimpls_evals_notes, self.modimpls_evals_notes,
_, 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(
@ -151,10 +153,17 @@ class ResultatsSemestreBUT:
for mi in modimpls: for mi 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:
moyennes_etuds = np.nan_to_num( moyennes_etuds = np.nan_to_num(
np.nanmean(self.sem_cube[:, mod_idx, :], axis=1), np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
copy=False, copy=False,
) )
except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides)
pass
try:
moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
except RuntimeWarning: # all nans in np.nanmean
pass
d[mi.module.code] = { d[mi.module.code] = {
"id": mi.id, "id": mi.id,
"titre": mi.module.titre, "titre": mi.module.titre,
@ -166,15 +175,16 @@ class ResultatsSemestreBUT:
), ),
"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)
"value": fmt_note(np.nanmean(self.sem_cube[etud_idx, mod_idx])), "value": fmt_note(moy_indicative_mod),
"min": fmt_note(moyennes_etuds.min()), "min": fmt_note(moyennes_etuds.min()),
"max": fmt_note(moyennes_etuds.max()), "max": fmt_note(moyennes_etuds.max()),
"moy": fmt_note(moyennes_etuds.mean()), "moy": fmt_note(moyennes_etuds.mean()),
}, },
"evaluations": [ "evaluations": [
self.etud_eval_results(etud, e) self.etud_eval_results(etud, e)
for e in mi.evaluations for eidx, e in enumerate(mi.evaluations)
if e.visibulletin if e.visibulletin
and self.modimpls_evaluations_complete[mi.id][eidx]
], ],
} }
return d return d

View File

@ -89,7 +89,7 @@ def check_moduleimpl_conformity(
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
"""Construit un dataframe avec toutes les notes des évaluations du module. """Construit un dataframe avec toutes les notes de toutes les évaluations du module.
colonnes: le nom de la colonne est l'evaluation_id (int) colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int) index (lignes): etudid (int)
@ -111,9 +111,12 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
N'utilise pas de cache ScoDoc. N'utilise pas de cache ScoDoc.
""" """
# L'index du dataframe est la liste des étudiants inscrits au semestre: # L'index du dataframe est la liste des étudiants inscrits au semestre, sans les démissionnaires
etudids = [ etudids = [
e.etudid for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.inscriptions 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() evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
if evaluations: if evaluations:
@ -138,9 +141,10 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
index_col="etudid", index_col="etudid",
dtype=np.float64, dtype=np.float64,
) )
evaluations_completes.append( is_complete = (
len(eval_df) == nb_inscrits_module or evaluation.publish_incomplete len(eval_df) == nb_inscrits_module or evaluation.publish_incomplete
) )
evaluations_completes.append(is_complete)
# NULL en base => ABS (= -999) # NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge met à NULL les élements non présents # Ce merge met à NULL les élements non présents

View File

@ -136,10 +136,12 @@ def notes_sem_load_cube(formsemestre):
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_evals_notes dict { modimpl.id : evals_notes }
modimpls_evaluations dict { modimpl.id : liste des évaluations } modimpls_evaluations dict { modimpl.id : liste des évaluations }
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
""" """
modimpls_evals_poids = {} modimpls_evals_poids = {}
modimpls_evals_notes = {} modimpls_evals_notes = {}
modimpls_evaluations = {} 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( evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
@ -152,12 +154,14 @@ def notes_sem_load_cube(formsemestre):
modimpls_evals_poids[modimpl.id] = evals_poids modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_evals_notes[modimpl.id] = evals_notes modimpls_evals_notes[modimpl.id] = evals_notes
modimpls_evaluations[modimpl.id] = evaluations 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_evals_notes,
modimpls_evaluations, modimpls_evaluations,
modimpls_evaluations_complete,
) )

View File

@ -5,6 +5,7 @@
""" """
from app import db from app import db
from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
@ -98,6 +99,18 @@ class Identite(db.Model):
] ]
return r[0] if r else None return r[0] if r else None
def etat_inscription(self, formsemestre_id):
"""etat de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
"""
# voir si ce n'est pas trop lent:
ins = models.FormsemestreInscription.query.filter_by(
etudid=self.id, formsemestre_id=formsemestre_id
).first()
if ins:
return ins.etat
return False
class Adresse(db.Model): class Adresse(db.Model):
"""Adresse d'un étudiant """Adresse d'un étudiant

View File

@ -240,6 +240,15 @@ class FormSemestre(db.Model):
etudid, self.date_debut.isoformat(), self.date_fin.isoformat() etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
) )
def get_inscrits(self, include_dem=False) -> list:
"""Liste des étudiants inscrits à ce semestre
Si all, tous les étudiants, avec les démissionnaires.
"""
if include_dem:
return [ins.etud for ins in self.inscriptions]
else:
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table( notes_formsemestre_responsables = db.Table(
@ -386,10 +395,11 @@ class FormsemestreInscription(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
formsemestre_inscription_id = db.synonym("id") formsemestre_inscription_id = db.synonym("id")
etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
index=True,
) )
etud = db.relationship( etud = db.relationship(
Identite, Identite,
@ -400,7 +410,7 @@ class FormsemestreInscription(db.Model):
backref=db.backref("inscriptions", cascade="all, delete-orphan"), backref=db.backref("inscriptions", cascade="all, delete-orphan"),
) )
# I inscrit, D demission en cours de semestre, DEF si "defaillant" # I inscrit, D demission en cours de semestre, DEF si "defaillant"
etat = db.Column(db.String(CODE_STR_LEN)) etat = db.Column(db.String(CODE_STR_LEN), index=True)
# etape apogee d'inscription (experimental 2020) # etape apogee d'inscription (experimental 2020)
etape = db.Column(db.String(APO_CODE_STR_LEN)) etape = db.Column(db.String(APO_CODE_STR_LEN))

View File

@ -71,6 +71,12 @@ NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes
NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_SUPPRESS = -1001.0 # note a supprimer
NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee)
# ---- CODES INSCRIPTION AUX SEMESTRES
# (champ etat de FormsemestreInscription)
INSCRIT = "I"
DEMISSION = "D"
DEF = "DEF"
# Types de modules # Types de modules
class ModuleType(IntEnum): class ModuleType(IntEnum):
"""Code des types de module.""" """Code des types de module."""

View File

@ -7,7 +7,7 @@
--couleurFondTitresUE: rgb(206,255,235); --couleurFondTitresUE: rgb(206,255,235);
--couleurFondTitresRes: rgb(125, 170, 255); --couleurFondTitresRes: rgb(125, 170, 255);
--couleurFondTitresSAE: rgb(255, 190, 69); --couleurFondTitresSAE: rgb(255, 190, 69);
--couleurSecondaire: #0c9; --couleurSecondaire: #fec;
--couleurIntense: #c09; --couleurIntense: #c09;
--couleurSurlignage: rgba(232, 255, 132, 0.47); --couleurSurlignage: rgba(232, 255, 132, 0.47);
} }
@ -132,7 +132,7 @@ section>div:nth-child(1){
} }
.enteteSemestre{ .enteteSemestre{
color: var(--couleurSecondaire); color: black;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
margin-bottom: 4px; margin-bottom: 4px;
@ -167,6 +167,9 @@ section>div:nth-child(1){
margin: 4px 0 2px 0; margin: 4px 0 2px 0;
overflow: auto; overflow: auto;
} }
.sae .module, .sae .module h3 {
background: #d3ffff;
}
h3{ h3{
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -0,0 +1,32 @@
"""index in FormsemestreInscription
Revision ID: 4f98a8b02c89
Revises: a57a6ee2e3cb
Create Date: 2021-12-14 16:56:11.034680
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4f98a8b02c89'
down_revision = 'a57a6ee2e3cb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(op.f('ix_notes_formsemestre_inscription_etat'), 'notes_formsemestre_inscription', ['etat'], unique=False)
op.create_index(op.f('ix_notes_formsemestre_inscription_etudid'), 'notes_formsemestre_inscription', ['etudid'], unique=False)
op.create_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), 'notes_formsemestre_inscription', ['formsemestre_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), table_name='notes_formsemestre_inscription')
op.drop_index(op.f('ix_notes_formsemestre_inscription_etudid'), table_name='notes_formsemestre_inscription')
op.drop_index(op.f('ix_notes_formsemestre_inscription_etat'), table_name='notes_formsemestre_inscription')
# ### end Alembic commands ###

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