diff --git a/app/comp/jury.py b/app/comp/jury.py
index bb7cd0d4e..6581a2fb0 100644
--- a/app/comp/jury.py
+++ b/app/comp/jury.py
@@ -34,6 +34,13 @@ class ValidationsSemestre(ResultatsCache):
"""Décisions sur des UEs dans ce semestre:
{ 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():
self.compute()
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index b9be879b0..c5267596a 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -12,6 +12,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
+from app.models.ues import UniteEns
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)
# 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)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
)
+ # --- UE capitalisées
+ self.apply_capitalisation()
+
# --- Classements:
self.compute_rangs()
@@ -110,3 +114,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
etud_idx = self.etud_index[etudid]
# moyenne sur les UE:
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()
diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py
index 9fdb1be29..0d8f32423 100644
--- a/app/comp/res_classic.py
+++ b/app/comp/res_classic.py
@@ -6,15 +6,24 @@
"""Résultats semestres classiques (non APC)
"""
+
import numpy as np
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.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
+from app.models.etudiants import Identite
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_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
@@ -104,6 +113,9 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
+ # --- UE capitalisées
+ self.apply_capitalisation()
+
# --- Classements:
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(
+ """
+ """
+ % (
+ 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]:
"""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)
# passe de (mod x etud) à (etud x mod)
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]
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index 69300d998..09d3ed75f 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -8,17 +8,22 @@ from collections import defaultdict, Counter
from functools import cached_property
import numpy as np
import pandas as pd
+
+from flask import g
+
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
+from app.models import FormSemestreUECoef
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc import sco_evaluations
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
+from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - 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) }"
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
+ self.validations = None
def compute(self):
"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)
+ 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
class NotesTableCompat(ResultatsSemestre):
@@ -189,7 +304,6 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA"
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours()
- self.validations = None
def get_etudids(self, sorted=False) -> list[int]:
"""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é)
}
- 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):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index e4b8fc8c8..932e3fecb 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -427,10 +427,12 @@ class FormSemestreUECoef(db.Model):
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
+ index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
+ index=True,
)
coefficient = db.Column(db.Float, nullable=False)
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
index b9b4630ce..e5077543b 100644
--- a/app/scodoc/notes_table.py
+++ b/app/scodoc/notes_table.py
@@ -703,11 +703,12 @@ class NotesTable:
où ue_status = {
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
'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
(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)
'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,
'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,
diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py
index ecf2d4887..4479409d0 100644
--- a/app/scodoc/sco_pvjury.py
+++ b/app/scodoc/sco_pvjury.py
@@ -165,10 +165,7 @@ def _comp_ects_capitalises_by_ue_code(nt, etudid):
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status["is_capitalized"]:
- try:
- ects_val = float(ue_status["ue"]["ects"])
- except (ValueError, TypeError):
- ects_val = 0.0
+ ects_val = float(ue_status["ue"]["ects"] or 0.0)
ects_by_ue_code[ue["ue_code"]] = ects_val
return ects_by_ue_code