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",
"etud_moy_gen",
"etud_moy_gen_ranks",
"modimpls_evaluations_complete",
)
def __init__(self, formsemestre):
self.formsemestre = formsemestre
self.ues = formsemestre.query_ues().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.saes = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
@ -77,7 +78,8 @@ class ResultatsSemestreBUT:
self.sem_cube,
self.modimpls_evals_poids,
self.modimpls_evals_notes,
_,
modimpls_evaluations,
self.modimpls_evaluations_complete,
) = 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(
@ -151,10 +153,17 @@ class ResultatsSemestreBUT:
for mi in modimpls:
mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
# moyennes indicatives (moyennes de moyennes d'UE)
try:
moyennes_etuds = np.nan_to_num(
np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
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] = {
"id": mi.id,
"titre": mi.module.titre,
@ -166,15 +175,16 @@ class ResultatsSemestreBUT:
),
"moyenne": {
# 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()),
"max": fmt_note(moyennes_etuds.max()),
"moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": [
self.etud_eval_results(etud, e)
for e in mi.evaluations
for eidx, e in enumerate(mi.evaluations)
if e.visibulletin
and self.modimpls_evaluations_complete[mi.id][eidx]
],
}
return d

View File

@ -89,7 +89,7 @@ def check_moduleimpl_conformity(
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)
index (lignes): etudid (int)
@ -111,9 +111,12 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
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 = [
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()
if evaluations:
@ -138,9 +141,10 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
index_col="etudid",
dtype=np.float64,
)
evaluations_completes.append(
is_complete = (
len(eval_df) == 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

View File

@ -136,10 +136,12 @@ def notes_sem_load_cube(formsemestre):
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_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(
@ -152,12 +154,14 @@ def notes_sem_load_cube(formsemestre):
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)
return (
notes_sem_assemble_cube(modimpls_notes),
modimpls_evals_poids,
modimpls_evals_notes,
modimpls_evaluations,
modimpls_evaluations_complete,
)

View File

@ -5,6 +5,7 @@
"""
from app import db
from app import models
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
@ -98,6 +99,18 @@ class Identite(db.Model):
]
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):
"""Adresse d'un étudiant

View File

@ -240,6 +240,15 @@ class FormSemestre(db.Model):
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
notes_formsemestre_responsables = db.Table(
@ -386,10 +395,11 @@ class FormsemestreInscription(db.Model):
id = db.Column(db.Integer, primary_key=True)
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(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
etud = db.relationship(
Identite,
@ -400,7 +410,7 @@ class FormsemestreInscription(db.Model):
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
)
# 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 = 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_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
class ModuleType(IntEnum):
"""Code des types de module."""

View File

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