WIP: prise en compte des UE capitalisées (! calculs erronés/en cours)

This commit is contained in:
Emmanuel Viennet 2022-02-07 16:32:04 +01:00
parent 844a90ed62
commit a9df233c2e
7 changed files with 215 additions and 17 deletions

View File

@ -34,6 +34,13 @@ class ValidationsSemestre(ResultatsCache):
"""Décisions sur des UEs dans ce semestre: """Décisions sur des UEs dans ce semestre:
{ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
""" """
self.ue_capitalisees: pd.DataFrame = None
"""DataFrame, index etudid
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE, moy_ue : note enregistrée,
event_date : date de la validation (jury)."""
if not self.load_cached(): if not self.load_cached():
self.compute() self.compute()

View File

@ -12,6 +12,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
@ -93,11 +94,14 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative: # Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative) # donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc( self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df self.etud_moy_ue, self.modimpl_coefs_df
) )
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements: # --- Classements:
self.compute_rangs() self.compute_rangs()
@ -110,3 +114,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
etud_idx = self.etud_index[etudid] etud_idx = self.etud_index[etudid]
# moyenne sur les UE: # moyenne sur les UE:
return self.sem_cube[etud_idx, mod_idx].mean() return self.sem_cube[etud_idx, mod_idx].mean()
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE.
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
"""
return self.modimpl_coefs_df.loc[ue.id].sum()

View File

@ -6,15 +6,24 @@
"""Résultats semestres classiques (non APC) """Résultats semestres classiques (non APC)
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from sqlalchemy.sql import text
from flask import g, url_for
from app import db
from app import log
from app.comp import moy_mod, moy_ue, inscr_mod from app.comp import moy_mod, moy_ue, inscr_mod
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -104,6 +113,9 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.bonus = ( self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
) )
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements: # --- Classements:
self.compute_rangs() self.compute_rangs()
@ -132,6 +144,42 @@ class ResultatsSemestreClassic(NotesTableCompat):
), ),
} }
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
"""
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
if c is not None: # inscrit à au moins un module de cette UE
return c
# arfff: aucun moyen de déterminer le coefficient de façon sûre
log(
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
% (self.formsemestre.id, etudid, ue)
)
etud: Identite = Identite.query.get(etudid)
raise ScoValueError(
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
</div>
"""
% (
ue.acronyme,
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
etud.nom_disp(),
url_for(
"notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre.id,
err_ue_id=ue["ue_id"],
),
)
)
return 0.0 # ?
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]: def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
"""Calcule la matrice des notes du semestre """Calcule la matrice des notes du semestre
@ -165,3 +213,29 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
modimpls_notes = np.stack(modimpls_notes_arr) modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud) à (etud x mod) # passe de (mod x etud) à (etud x mod)
return modimpls_notes.T return modimpls_notes.T
def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
"""Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
ou None s'il n'y a aucun module.
"""
# comme l'ancien notes_table.comp_etud_sum_coef_modules_ue
# mais en raw sqlalchemy et la somme en SQL
sql = text(
"""
SELECT sum(mod.coefficient)
FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
WHERE mod.id = mi.module_id
and ins.etudid = :etudid
and ins.moduleimpl_id = mi.id
and mi.formsemestre_id = :formsemestre_id
and mod.ue_id = :ue_id
"""
)
cursor = db.session.execute(
sql, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}
)
r = cursor.fetchone()
if r is None:
return None
return r[0]

View File

@ -8,17 +8,22 @@ from collections import defaultdict, Counter
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 flask import g
from app.comp.aux_stats import StatsMoyenne from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer # Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis): # - ce qui est caché de façon persistente (via redis):
@ -53,6 +58,7 @@ class ResultatsSemestre(ResultatsCache):
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
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"
@ -155,6 +161,115 @@ class ResultatsSemestre(ResultatsCache):
""" """
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE) return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
def apply_capitalisation(self):
"""Recalcule la moyenne générale pour prendre en compte d'éventuelles
UE capitalisées.
"""
# Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
ue_by_code = {}
for etudid in ue_capitalisees.index:
recompute_mg = False
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
# for ue_code in ue_codes:
# ue = ue_by_code.get(ue_code)
# if ue is None:
# ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code)
# ue_by_code[ue_code] = ue
# Quand il y a une capitalisation, vérifie toutes les UEs
sum_notes_ue = 0.0
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
if recompute_mg and sum_coefs_ue > 0.0:
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
def _get_etud_ue_cap(self, etudid, ue):
""""""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
else:
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
Result: dict
"""
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if ue_cap is not None and not ue_cap.empty:
if ue_cap["moy_ue"] > cur_moy_ue:
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
if is_capitalized:
coef_ue = 1.0
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": is_capitalized,
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue,
"cur_moy_ue": cur_moy_ue,
"moy": moy_ue,
"event_date": ue_cap["event_date"] if is_capitalized else None,
"ue": ue.to_dict(),
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
}
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
injectée dans le semestre courant.
ue : ue du semestre courant
ue_cap = resultat de formsemestre_get_etud_capitalisation
{ 'ue_id' (dans le semestre source),
'ue_code', 'moy', 'event_date','formsemestre_id' }
"""
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
ue_coef_db = FormSemestreUECoef.query.filter_by(
formsemestre_id=self.formsemestre.id, ue_id=ue.id
).first()
if ue_coef_db is not None:
return ue_coef_db.coefficient
# En APC: somme des coefs des modules vers cette UE
# En classique: Capitalisation UE externe: quel coef appliquer ?
# En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine
# ici si l'étudiant est inscrit dans le semestre courant,
# somme des coefs des modules de l'UE auxquels il est inscrit
return self.compute_etud_ue_coef(etudid, ue)
# 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):
@ -189,7 +304,6 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA" self.moy_moy = "NA"
self.expr_diagnostics = "" self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours() self.parcours = self.formsemestre.formation.get_parcours()
self.validations = None
def get_etudids(self, sorted=False) -> list[int]: def get_etudids(self, sorted=False) -> list[int]:
"""Liste des etudids inscrits, incluant les démissionnaires. """Liste des etudids inscrits, incluant les démissionnaires.
@ -353,16 +467,6 @@ class NotesTableCompat(ResultatsSemestre):
"ects_fond": 0.0, # not implemented (anciennemment pour école ingé) "ects_fond": 0.0, # not implemented (anciennemment pour école ingé)
} }
def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": False, # XXX TODO
"is_external": False, # XXX TODO
"coef_ue": coef_ue, # XXX TODO
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
"moy": self.etud_moy_ue[ue_id][etudid],
}
def get_etud_rang(self, etudid: int): def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX

View File

@ -427,10 +427,12 @@ class FormSemestreUECoef(db.Model):
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
index=True,
) )
ue_id = db.Column( ue_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_ue.id"), db.ForeignKey("notes_ue.id"),
index=True,
) )
coefficient = db.Column(db.Float, nullable=False) coefficient = db.Column(db.Float, nullable=False)

View File

@ -703,11 +703,12 @@ class NotesTable:
ue_status = { ue_status = {
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE 'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
'moy' : moyenne, avec capitalisation eventuelle 'moy' : moyenne, avec capitalisation eventuelle
'capitalized_ue_id' : id de l'UE capitalisée
'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale 'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
(la somme des coefs des modules, ou le coef d'UE capitalisée, (la somme des coefs des modules, ou le coef d'UE capitalisée,
ou encore le coef d'UE si l'option use_ue_coefs est active) ou encore le coef d'UE si l'option use_ue_coefs est active)
'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation) 'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
'cur_coef_ue': coefficient de l'UE courante 'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
'is_capitalized' : True|False, 'is_capitalized' : True|False,
'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), 'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon, 'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,

View File

@ -165,10 +165,7 @@ def _comp_ects_capitalises_by_ue_code(nt, etudid):
for ue in ues: for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status["is_capitalized"]: if ue_status["is_capitalized"]:
try: ects_val = float(ue_status["ue"]["ects"] or 0.0)
ects_val = float(ue_status["ue"]["ects"])
except (ValueError, TypeError):
ects_val = 0.0
ects_by_ue_code[ue["ue_code"]] = ects_val ects_by_ue_code[ue["ue_code"]] = ects_val
return ects_by_ue_code return ects_by_ue_code